001: /*
002:
003: This software is OSI Certified Open Source Software.
004: OSI Certified is a certification mark of the Open Source Initiative.
005:
006: The license (Mozilla version 1.0) can be read at the MMBase site.
007: See http://www.MMBase.org/license
008:
009: */
010: package org.mmbase.module.builders;
011:
012: import java.util.*;
013: import java.util.regex.*;
014:
015: import org.mmbase.servlet.MMBaseServlet;
016: import org.mmbase.servlet.BridgeServlet;
017: import javax.servlet.http.HttpServletRequest;
018: import org.mmbase.module.core.*;
019: import org.mmbase.util.*;
020: import org.mmbase.util.logging.*;
021: import org.mmbase.util.magicfile.MagicFile;
022: import org.mmbase.util.functions.*;
023: import org.mmbase.bridge.*;
024: import org.mmbase.security.Rank;
025:
026: /**
027: * Some builders are associated with a servlet. Think of images and attachments.
028: *
029: * There is some common functionality for those kind of builders, which is collected here.
030: *
031: *
032: * @author Michiel Meeuwissen
033: * @version $Id: AbstractServletBuilder.java,v 1.53 2008/02/25 12:35:05 michiel Exp $
034: * @since MMBase-1.6
035: */
036: public abstract class AbstractServletBuilder extends MMObjectBuilder {
037:
038: private static final Logger log = Logging
039: .getLoggerInstance(AbstractServletBuilder.class);
040:
041: public static final String FIELD_MIMETYPE = "mimetype";
042: public static final String FIELD_FILENAME = "filename";
043: public static final String FIELD_HANDLE = "handle";
044:
045: /**
046: * Can be used to construct a List for executeFunction argument
047: * (new Parameters(GUI_ARGUMENTS))
048: */
049: public final static Parameter[] GUI_PARAMETERS = { new Parameter.Wrapper(
050: MMObjectBuilder.GUI_PARAMETERS) // example, does not make too much sense :-)
051: };
052:
053: public final static Parameter[] FORMAT_PARAMETERS = {};
054: public final static Parameter[] MIMETYPE_PARAMETERS = {};
055:
056: /**
057: * In this string the path to the servlet is stored.
058: */
059: private String servletPath = null;
060: /**
061: * Whether {@link #servletPath} represents an absolute URL (starting with http:)
062: * @since MMBase-1.7.4
063: */
064: private boolean servletPathAbsolute;
065:
066: /**
067: * If this builder is association with a bridge servlet. If not, it should not put the
068: * 'session=' in the url to the servlet (because the serlvet probably is servdb, which does not
069: * understand that).
070: */
071: protected boolean usesBridgeServlet = false;
072:
073: private static final int FILENAME_ADD = 1;
074: private static final int FILENAME_DONTADD = 0;
075: private static final int FILENAME_IFSENSIBLE = -1;
076: private static final int FILENAME_CHECKSETTING = -2;
077: /**
078: * -2: check init, based on existance of filename field.
079: * -1: based on existance of filename field
080: * 0 : no
081: * 1 : yes
082: * @since MMBase-1.7.4
083: */
084: protected int addsFileName = FILENAME_CHECKSETTING;
085:
086: /**
087: * This functions should return a string identifying where it is
088: * for. This is used when communicating with MMBaseServlet, to
089: * find the right servlet.
090: *
091: * For example 'images' or 'attachments'.
092: *
093: */
094: abstract protected String getAssociation();
095:
096: /**
097: * If no servlet path can be found via the association (if the
098: * servlet did not 'associate' itself with something, like
099: * servdb), then the getServletPath function will fall back to
100: * this.
101: *
102: * For example 'img.db' or 'attachment.db'.
103: *
104: */
105: abstract protected String getDefaultPath();
106:
107: /**
108: * @param association e.g. 'images' or 'attachments'
109: * @param root Path to root of appliciation (perhaps relative).
110: */
111:
112: private String getServletPathWithAssociation(String association,
113: String root) {
114: if (MMBaseContext.isInitialized()) {
115: javax.servlet.ServletContext sx = MMBaseContext
116: .getServletContext();
117: if (sx != null) {
118: String res = sx.getInitParameter("mmbase.servlet."
119: + association + ".url");
120: if (res != null && !res.equals("")) {
121: return res;
122: }
123: }
124: }
125: String result = MMBaseServlet.getBasePath(association);
126: if (result != null) {
127: usesBridgeServlet = MMBaseServlet
128: .getServletByMapping(result) instanceof BridgeServlet;
129: } else {
130: result = getDefaultPath();
131: }
132:
133: if (result.startsWith("/")) {
134: // if it not starts with / then no use adding context.
135: if (root != null) {
136: if (root.endsWith("/")) {
137: result = root + result.substring(1);
138: } else {
139: result = root + result;
140: }
141: }
142: }
143: return result;
144: }
145:
146: /**
147: * Get a servlet path. Takes away the ? and the * which possibly
148: * are present in the servlet-mappings. You can put the argument(s)
149: * directly after this string.
150: *
151: * @param root The path to the application's root.
152: */
153:
154: protected String getServletPath(String root) {
155: if (servletPath == null) {
156: servletPath = getServletPathWithAssociation(
157: getAssociation(), "");
158: if (log.isServiceEnabled()) {
159: log.service(getAssociation() + " are served on: "
160: + servletPath + " root: " + root);
161: }
162: servletPathAbsolute = servletPath.startsWith("http:")
163: || servletPath.startsWith("https");
164: }
165:
166: String result;
167: if (servletPathAbsolute) {
168: result = servletPath;
169: } else if (root.endsWith("/") && servletPath.startsWith("/")) {
170: result = root + servletPath.substring(1);
171: } else {
172: result = root + servletPath;
173: }
174:
175: if (!MMBaseContext.isInitialized()) {
176: servletPath = null;
177: }
178: // add '?' if it wasn't already there (only needed if not terminated with /)
179: if (!result.endsWith("/"))
180: result += "?";
181: return result;
182: }
183:
184: protected String getServletPath() {
185: return getServletPath(MMBaseContext.getHtmlRootUrlPath());
186: }
187:
188: /**
189: * Returns the Mime-type associated with this node
190: */
191: protected String getMimeType(MMObjectNode node) {
192: String mimeType = node.getStringValue(FIELD_MIMETYPE);
193: if (mimeType == null || mimeType.equals("")) {
194: if (log.isServiceEnabled()) {
195: log
196: .service("Mimetype of attachment '"
197: + node.getNumber()
198: + "' was not set. Using magic to determine it automaticly.");
199: }
200: byte[] handle = node.getByteValue(FIELD_HANDLE);
201:
202: String extension = null;
203: if (hasField(FIELD_FILENAME)) {
204: String filename = node.getStringValue(FIELD_FILENAME);
205: int dotIndex = filename.lastIndexOf(".");
206: if (dotIndex > -1) {
207: extension = filename.substring(dotIndex + 1);
208: }
209: }
210:
211: MagicFile magic = MagicFile.getInstance();
212: try {
213: if (extension == null) {
214: mimeType = magic.getMimeType(handle);
215: } else {
216: mimeType = magic.getMimeType(handle, extension);
217: }
218: log.service("Found mime-type: " + mimeType);
219: node.setValue(FIELD_MIMETYPE, mimeType);
220: } catch (Throwable e) {
221: log.warn("Exception in MagicFile for " + node);
222: mimeType = "application/octet-stream";
223: node.setValue(FIELD_MIMETYPE, mimeType);
224: }
225:
226: }
227: return mimeType;
228: }
229:
230: /**
231: * Tries to fill all fields which are dependend on the 'handle' field.
232: * They will be filled automaticly if not still null.
233: */
234: protected void checkHandle(MMObjectNode node) {
235: if (getField(FIELD_MIMETYPE) != null) {
236: getMimeType(node);
237: }
238:
239: }
240:
241: /**
242: * Returns the fields which tell something about the 'handle' field, and can be calculated from it.
243: */
244:
245: abstract protected Set<String> getHandleFields();
246:
247: public int insert(String owner, MMObjectNode node) {
248: if (log.isDebugEnabled()) {
249: log.debug("Inserting node " + node.getNumber()
250: + " memory: " + SizeOf.getByteSize(node));
251: }
252: checkHandle(node);
253: int result = super .insert(owner, node);
254: if (log.isDebugEnabled()) {
255: log.debug("After handle unload, memory: "
256: + SizeOf.getByteSize(node));
257: }
258: return result;
259: }
260:
261: public boolean commit(MMObjectNode node) {
262: Collection<String> changed = node.getChanged();
263: if (log.isDebugEnabled()) {
264: log.debug("Committing node " + node.getNumber()
265: + " memory: " + SizeOf.getByteSize(node)
266: + " fields " + changed);
267: }
268:
269: if (changed.contains(FIELD_HANDLE)) {
270: // set those fields to null, which are not changed too:
271: Collection<String> cp = new ArrayList<String>();
272: cp.addAll(getHandleFields());
273: cp.removeAll(changed);
274: Iterator<String> i = cp.iterator();
275: while (i.hasNext()) {
276: String f = i.next();
277: if (node.getBuilder().hasField(f)) {
278: node.setValue(f, null);
279: }
280: }
281: }
282: checkHandle(node);
283: boolean result = super .commit(node);
284: if (log.isDebugEnabled()) {
285: log.debug("After commit node " + node.getNumber()
286: + " memory: " + SizeOf.getByteSize(node));
287: }
288: return result;
289: }
290:
291: /**
292: * 'Servlet' builders need a way to transform security to the servlet, in the gui functions, so
293: * they have to implement the 'SGUIIndicators'
294: */
295:
296: abstract protected String getSGUIIndicator(MMObjectNode node,
297: Parameters a);
298:
299: /**
300: * Gets the GUI indicator of the super class of this class, to avoid circular references in
301: * descendants, which will occur if they want to call super.getGUIIndicator().
302: */
303:
304: final protected String getSuperGUIIndicator(String field,
305: MMObjectNode node) {
306: return super .getGUIIndicator(field, node);
307: }
308:
309: final public String getGUIIndicator(MMObjectNode node,
310: Parameters pars) {
311: String field = (String) pars.get("field");
312: if (field == null || "".equals(field)
313: || FIELD_HANDLE.equals(field)) {
314: return getSGUIIndicator(node, pars);
315: } else {
316: return super .getGUIIndicator(node, pars);
317: }
318:
319: }
320:
321: /**
322: * This is final, because getSGUIIndicator has to be overridden in stead
323: */
324:
325: final public String getGUIIndicator(String field, MMObjectNode node) { // final, override getSGUIIndicator
326: return getSGUIIndicator(node, new Parameters(GUI_PARAMETERS)
327: .set("field", field));
328: }
329:
330: protected static final Pattern legalizeFileName = Pattern
331: .compile("[%\\/\\:\\;\\\\ \\?\\&]+");
332:
333: /**
334: * @since MMBase-1.8
335: */
336: protected String getDefaultFileName() {
337: return getSingularName("en");
338: }
339:
340: /**
341: * @since MMBase-1.8
342: */
343: protected StringBuilder getFileName(MMObjectNode node,
344: StringBuilder buf) {
345: String fileName = hasField(FIELD_FILENAME) ? node
346: .getStringValue(FIELD_FILENAME) : "";
347: if (fileName.equals("")) {
348: String fileTitle;
349: if (hasField("title")) {
350: fileTitle = node.getStringValue("title");
351: } else if (hasField("name")) {
352: fileTitle = node.getStringValue("name");
353: } else {
354: fileTitle = "";
355: }
356: if (fileTitle.equals("")) {
357: fileTitle = getDefaultFileName();
358: }
359: fileName = fileTitle + "."
360: + node.getFunctionValue("format", null);
361: }
362: int backSlash = fileName.lastIndexOf("\\");
363: // if uploaded in MSIE, then the path may be in the fileName
364: // this is also fixed in the set-processor, but if that is or was missing, be gracefull here.
365: if (backSlash > -1) {
366: fileName = fileName.substring(backSlash + 1);
367: }
368: buf.append(legalizeFileName.matcher(fileName).replaceAll("_"));
369: return buf;
370: }
371:
372: /**
373: * Adds a filename to the path to a servlet, unless this does not make sense (not filename can
374: * be determined) or it was explicitely set not to, using the servlet context init parameter
375: * 'mmbase.servlet.<association>addfilename.
376: * @since MMBase-1.8
377: */
378: protected boolean addFileName(MMObjectNode node, String servlet) {
379: if (addsFileName == FILENAME_CHECKSETTING) {
380: javax.servlet.ServletContext sx = MMBaseContext
381: .getServletContext();
382: if (sx != null) {
383: String res = sx.getInitParameter("mmbase.servlet."
384: + getAssociation() + ".addfilename");
385: if (res == null)
386: res = "";
387: res = res.toLowerCase();
388: log.trace("res " + res);
389: if ("no".equals(res) || "false".equals(res)) {
390: addsFileName = FILENAME_DONTADD;
391: } else if ("yes".equals(res) || "true".equals(res)) {
392: addsFileName = FILENAME_ADD;
393: } else {
394: log.debug("Found " + res + " for mmbase.servlet."
395: + getAssociation() + ".addfilename");
396: addsFileName = FILENAME_IFSENSIBLE;
397: }
398: }
399: }
400: log.debug("addsFileName " + addsFileName);
401:
402: String fileName = hasField(FIELD_FILENAME) ? node
403: .getStringValue(FIELD_FILENAME) : "";
404: return addsFileName == FILENAME_ADD
405: || (addsFileName == FILENAME_IFSENSIBLE
406: && (!servlet.endsWith("?")) && (!""
407: .equals(fileName)));
408:
409: }
410:
411: /**
412: * @since MMBase-1.8.1
413: */
414: protected String getSession(Parameters a, int nodeNumber) {
415: String session = a.getString("session");
416: if (session == null) {
417: Cloud cloud = a.get(Parameter.CLOUD);
418: log.debug("No session given for " + cloud);
419: if (cloud != null
420: && !cloud.getUser().getRank()
421: .equals(Rank.ANONYMOUS)) {
422: log.debug("not anonymous");
423: // the user is not anonymous!
424: // Need to check if node is readable by anonymous.
425: // in that case URLs can be simpler
426: // two situations are anticipated:
427: // - node not readable by anonymous
428: // - no anonymous user defined
429: try {
430: String cloudName;
431: if (cloud instanceof Transaction) {
432: cloudName = ((Transaction) cloud)
433: .getCloudName();
434: } else {
435: cloudName = cloud.getName();
436: }
437: Cloud anonymousCloud = cloud.getCloudContext()
438: .getCloud(cloudName);
439: if (!anonymousCloud.mayRead(nodeNumber)) {
440: session = (String) cloud
441: .getProperty(Cloud.PROP_SESSIONNAME);
442: log
443: .debug("Anonymous may not read, setting session to "
444: + session);
445:
446: }
447: } catch (org.mmbase.security.SecurityException se) {
448: log.debug(se.getMessage());
449: session = (String) cloud
450: .getProperty(Cloud.PROP_SESSIONNAME);
451: }
452: }
453: if ("".equals(session))
454: session = null;
455: }
456:
457: return session;
458: }
459:
460: {
461: // you can of course even implement it anonymously.
462: addFunction(new NodeFunction<String>(
463: "servletpath",
464: new Parameter[] {
465: new Parameter<String>("session", String.class), // For read-protection
466: new Parameter<String>("field", String.class), // The field to use as argument, defaults to number unless 'argument' is specified.
467: new Parameter<String>("context", String.class), // Path to the context root, defaults to "/" (but can specify something relative).
468: new Parameter<String>("argument", String.class), // Parameter to use for the argument, overrides 'field'
469: Parameter.REQUEST, Parameter.CLOUD },
470: ReturnType.STRING) {
471: {
472: setDescription("Returns the path associated with this builder or node.");
473: }
474:
475: protected StringBuilder getServletPath(Parameters a) {
476: StringBuilder servlet = new StringBuilder();
477: // third argument, the servlet context, can use a relative path here, as an argument
478: String context = (String) a.get("context");
479:
480: if (context == null) {
481: // no path to context-root specified explitiely, try to determin:
482: HttpServletRequest request = a
483: .get(Parameter.REQUEST);
484: if (request == null) {
485: // no request object given as well, hopefully it worked on servlet's initalizations (it would, in most servlet containers, like tomcat)
486: servlet.append(AbstractServletBuilder.this
487: .getServletPath()); // use 'absolute' path (starting with /)
488: } else {
489: servlet.append(AbstractServletBuilder.this
490: .getServletPath(request
491: .getContextPath()));
492: }
493: } else {
494: // explicitely specified the path!
495: servlet.append(AbstractServletBuilder.this
496: .getServletPath(context));
497: }
498: return servlet;
499: }
500:
501: public String getFunctionValue(Node node, Parameters a) {
502: StringBuilder servlet = getServletPath(a);
503:
504: String session = getSession(a, node.getNumber());
505: String argument = (String) a.get("argument");
506: // argument representint the node-number
507:
508: if (argument == null) {
509: String fieldName = (String) a.get("field");
510: if (fieldName == null || "".equals(fieldName)) {
511: argument = node.getStringValue("number");
512: } else {
513: if (log.isDebugEnabled()) {
514: log.debug("Getting 'field' '" + fieldName
515: + "'");
516: }
517: argument = node.getStringValue(fieldName);
518: }
519: }
520: MMObjectNode mmnode = node.getNumber() > 0 ? AbstractServletBuilder.this
521: .getNode(node.getNumber())
522: : new MMObjectNode(
523: AbstractServletBuilder.this ,
524: new org.mmbase.bridge.util.NodeMap(node));
525: boolean addFileName = addFileName(mmnode, servlet
526: .toString());
527:
528: log.debug("Using session " + session);
529:
530: if (usesBridgeServlet && session != null
531: && !"".equals(session)) {
532: servlet.append("session=" + session + "+");
533: }
534:
535: if (!addFileName) {
536: return servlet.append(argument).toString();
537: } else {
538: servlet.append(argument).append('/');
539: getFileName(mmnode, servlet);
540: return servlet.toString();
541: }
542: }
543:
544: public String getFunctionValue(Parameters a) {
545: return getServletPath(a).toString();
546: }
547: });
548:
549: }
550:
551: {
552: /**
553: * @since MMBase-1.8
554: */
555: addFunction(new NodeFunction<String>("iconurl",
556: new Parameter[] {
557: Parameter.REQUEST,
558: new Parameter<String>("iconroot", String.class,
559: "/mmbase/style/icons/"),
560: new Parameter<String>("absolute", String.class,
561: "false") }, ReturnType.STRING) {
562: {
563: setDescription("Returns an URL for an icon for this blob");
564: }
565:
566: public String getFunctionValue(Node n, Parameters parameters) {
567: String mimeType = AbstractServletBuilder.this
568: .getMimeType(getCoreNode(
569: AbstractServletBuilder.this , n));
570: ResourceLoader webRoot = ResourceLoader.getWebRoot();
571: HttpServletRequest request = parameters
572: .get(Parameter.REQUEST);
573: String absolute = parameters.getString("absolute");
574: String root;
575: if (request != null) {
576: root = request.getContextPath();
577: } else {
578: root = MMBaseContext.getHtmlRootUrlPath();
579: }
580:
581: if ("true".equals(absolute) && request != null) {
582: int port = request.getServerPort();
583: root = request.getScheme() + "://"
584: + request.getServerName()
585: + (port == 80 ? "" : ":" + port) + root;
586: }
587: String iconRoot = (String) parameters.get("iconroot");
588: if (root.endsWith("/") && iconRoot.startsWith("/"))
589: iconRoot = iconRoot.substring(1);
590:
591: if (!iconRoot.endsWith("/"))
592: iconRoot = iconRoot + '/';
593:
594: String resource = iconRoot + mimeType + ".gif";
595: try {
596: if (!webRoot.getResource(resource).openConnection()
597: .getDoInput()) {
598: resource = iconRoot
599: + "application/octet-stream.gif";
600: }
601: } catch (java.io.IOException ioe) {
602: log.warn(ioe.getMessage(), ioe);
603: resource = iconRoot
604: + "application/octet-stream.gif";
605: }
606: return root + resource;
607: }
608:
609: });
610: }
611:
612: /**
613: * Overrides the executeFunction of MMObjectBuilder with a function to get the servletpath
614: * associated with this builder. The field can optionally be the number field to obtain a full
615: * path to the served object.
616: *
617: *
618: */
619:
620: protected Object executeFunction(MMObjectNode node,
621: String function, List<?> args) {
622: if (log.isDebugEnabled()) {
623: log.debug("executefunction of abstractservletbuilder for "
624: + node.getNumber() + "." + function + " " + args);
625: }
626: if (function.equals("info")) {
627: List<Object> empty = new ArrayList<Object>();
628: Map<String, String> info = (Map<String, String>) super
629: .executeFunction(node, function, empty);
630: info
631: .put("servletpathof",
632: "(function) Returns the servletpath associated with a certain function");
633: info.put("format", "bla bla");
634: info.put("mimetype",
635: "Returns the mimetype associated with this object");
636: info.put("gui", "" + GUI_PARAMETERS
637: + "Gui representation of this object.");
638:
639: if (args == null || args.size() == 0) {
640: return info;
641: } else {
642: return info.get(args.get(0));
643: }
644: } else if (function.equals("servletpath")) {
645:
646: } else if (function.equals("servletpathof")) {
647: // you should not need this very often, only when you want to serve a node with the 'wrong' servlet this can come in handy.
648: return getServletPathWithAssociation((String) args.get(0),
649: MMBaseContext.getHtmlRootUrlPath());
650: } else if (function.equals("format")) { // don't issue a warning, builders can override this.
651: // images e.g. return jpg or gif
652: } else if (function.equals("mimetype")) { // don't issue a warning, builders can override this.
653: // images, attachments and so on
654: } else if (function.equals("gui")) {
655: if (log.isDebugEnabled()) {
656: log.debug("GUI of servlet builder with " + args);
657: }
658: if (args == null || args.size() == 0) {
659: return getGUIIndicator(node);
660: } else {
661: Parameters a;
662: if (args instanceof Parameters) {
663: a = (Parameters) args;
664: } else {
665: a = new Parameters(GUI_PARAMETERS, args);
666: }
667:
668: String rtn = getSGUIIndicator(node, a);
669: if (rtn == null)
670: return super.executeFunction(node, function, args);
671: return rtn;
672: }
673: } else {
674: return super.executeFunction(node, function, args);
675: }
676: return null;
677: }
678:
679: }
|