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.servlet;
011:
012: import java.io.IOException;
013: import java.util.regex.*;
014:
015: import javax.servlet.ServletException;
016: import javax.servlet.http.*;
017:
018: import org.mmbase.bridge.*;
019: import org.mmbase.util.logging.*;
020:
021: /**
022: * BridgeServlet is an MMBaseServlet with a bridge Cloud in it. Extending from this makes it easy to
023: * implement servlet implemented with the MMBase bridge interfaces.
024: *
025: * An advantage of this is that security is used, which means that you cannot unintentionly serve
026: * content to the whole world which should actually be protected by the security mechanism.
027: *
028: * Another advantage is that implementation using the bridge is easier/clearer.
029: *
030: * The query of a bridge servlet can possible start with session=<session-variable-name> in which case the
031: * cloud is taken from that session attribute with that name. Otherewise 'cloud_mmbase' is
032: * supposed. All this is only done if there was a session active at all. If not, or the session
033: * variable was not found, that an anonymous cloud is used.
034: *
035: * Object can only be accessed by alias if a mapping on query string is used (so not e.g. /images/*,
036: * but /img.db). Normally this is no problem, because the alias is resolved by the image-tag. But if
037: * for some reason you need aliases to be working on the URL, you must map to URL's with a question mark.
038: *
039: * @version $Id: BridgeServlet.java,v 1.33 2007/02/10 16:22:37 nklasens Exp $
040: * @author Michiel Meeuwissen
041: * @since MMBase-1.6
042: */
043: public abstract class BridgeServlet extends MMBaseServlet {
044: public static final String MESSAGE_ATTRIBUTE = "org.mmbase.servlet.error.message"; // javax.servlet.error.message is a bit short normally
045:
046: /**
047: * Pattern used for the 'filename' part of the request. The a node-identifying string may be
048: * present in it, and it the one capturing group.
049: * It is a digit optionially followed by +.* (used in ImageServlet for url-triggered icache production)
050: */
051:
052: public static final Pattern FILE_PATTERN = Pattern
053: .compile(".*?\\D((?:session=.*?\\+)?\\d+(?:\\+.+?)?)(/.*)?");
054: // some examples captured by this regexp:
055: // /mmbase/images/session=mmbasesession+1234+s(100)/image.jpg
056: // /mmbase/images/1234+s(100)/image.jpg
057: // /mmbase/images/1234/image.jpg
058: // /mmbase/images/1234
059: // /mmbase/images?1234 (1234 not captured by regexp, but is in query!)
060:
061: // may not be digits in servlet mapping itself!
062:
063: private static Logger log;
064:
065: /**
066: * This is constant after init.
067: */
068: private static int contextPathLength = -1;
069:
070: private String lastModifiedField = null;
071:
072: /**
073: * The name of the mmbase cloud which must be used. At the moment this is not supported (every
074: * mmbase cloud is called 'mmbase').
075: */
076: protected String getCloudName() {
077: return "mmbase";
078: }
079:
080: /**
081: * Creates a QueryParts object which wraps request and response and the parse result of them.
082: * @return A QueryParts or <code>null</code> if something went wrong (in that case an error was sent, using the response).
083: */
084: protected QueryParts readQuery(HttpServletRequest req,
085: HttpServletResponse res) throws IOException {
086: QueryParts qp = (QueryParts) req
087: .getAttribute("org.mmbase.servlet.BridgeServlet$QueryParts");
088: if (qp != null) {
089: log.trace("no need parsing query");
090: if (qp.getResponse() == null && res != null) {
091: qp.setResponse(res);
092: }
093: return qp;
094: }
095: if (log.isTraceEnabled()) {
096: log.trace("parsing query ");
097: }
098:
099: String q = req.getQueryString();
100:
101: if (q == null || "".equals(q)) { // should be null if no query string, but http://issues.apache.org/bugzilla/show_bug.cgi?id=38113, there is version of tomcat in which it isn't.
102: // also possible to use /attachments/[session=abc+]<number>/filename.pdf
103: if (contextPathLength == -1) {
104: contextPathLength = req.getContextPath().length();
105: }
106: String reqString = req.getRequestURI().substring(
107: contextPathLength); // substring needed, otherwise there may not be digits in context path.
108:
109: // some silly application-servers leave jsession id it the requestURI. Take if off again, because we'll be very confused by it.
110: if (req.isRequestedSessionIdFromURL()) {
111: int jsessionid = reqString.indexOf(";jsessionid=");
112: if (jsessionid != -1) {
113: reqString = reqString.substring(0, jsessionid);
114: }
115: }
116:
117: if (log.isDebugEnabled()) {
118: log.debug("using servlet URI " + reqString
119: + " to find node number");
120: }
121:
122: qp = readServletPath(reqString);
123: if (qp == null) {
124: log.debug("Did not match");
125: if (res != null) {
126: res.sendError(HttpServletResponse.SC_BAD_REQUEST,
127: "Malformed URL: '" + reqString
128: + "' does not match '"
129: + FILE_PATTERN.pattern() + "'.");
130: req.setAttribute(MESSAGE_ATTRIBUTE,
131: "Malformed URL: '" + reqString
132: + "' does not match '"
133: + FILE_PATTERN.pattern() + "'.");
134: } else {
135: log.error("Malformed URL: '" + reqString
136: + "' does not match '"
137: + FILE_PATTERN.pattern() + "'.");
138: }
139: } else {
140: if (log.isDebugEnabled()) {
141: log.debug("found " + qp);
142: }
143: }
144: } else {
145: if (log.isDebugEnabled()) {
146: log.debug("using query " + q + " to find node number");
147: }
148: // attachment.db?[session=abc+]number
149: qp = readQuery(q);
150: if (qp == null && res != null) {
151: res
152: .sendError(HttpServletResponse.SC_BAD_REQUEST,
153: "Malformed URL: No node number found after session.");
154: req
155: .setAttribute(MESSAGE_ATTRIBUTE,
156: "Malformed URL: No node number found after session.");
157: }
158:
159: }
160:
161: if (qp == null)
162: return null;
163:
164: qp.setRequest(req);
165: qp.setResponse(res);
166:
167: req.setAttribute("org.mmbase.servlet.BridgeServlet$QueryParts",
168: qp);
169: return qp;
170: }
171:
172: /**
173: *
174: * @since MMBase-1.7.4
175: */
176: public static QueryParts readServletPath(String servletPath) {
177: Matcher m = FILE_PATTERN.matcher(servletPath);
178: if (!m.matches()) {
179: return null;
180: }
181: QueryParts qp = readQuery(m.group(1));
182: qp.setFileName(m.group(2));
183: return qp;
184: }
185:
186: /**
187: *
188: * @since MMBase-1.7.4
189: */
190: public static QueryParts readQuery(String query) {
191: String sessionName = null; // "cloud_" + getCloudName();
192: String nodeIdentifier;
193: if (query.startsWith("session=")) {
194: // indicated the session name in the query: session=<sessionname>+<nodenumber>
195:
196: int plus = query.indexOf("+", 8);
197: if (plus == -1) {
198: sessionName = "";
199: nodeIdentifier = query;
200: } else {
201: sessionName = query.substring(8, plus);
202: nodeIdentifier = query.substring(plus + 1);
203: }
204: } else {
205: nodeIdentifier = query;
206: }
207: return new QueryParts(sessionName, nodeIdentifier);
208:
209: }
210:
211: /**
212: * Obtains a cloud object, using a QueryParts object.
213: * @return A Cloud or <code>null</code> if unsuccessful (this may not be fatal).
214: */
215: final protected Cloud getCloud(QueryParts qp) throws IOException {
216: log.debug("getting a cloud");
217: // trying to get a cloud from the session
218: Cloud cloud = null;
219: HttpSession session = qp.getRequest().getSession(false); // false: do not create a session, only use it
220: if (session != null) { // there is a session
221: log.debug("from session");
222: String sessionName = qp.getSessionName();
223: if (sessionName != null) {
224: cloud = (Cloud) session.getAttribute(sessionName);
225: } else { // desperately searching for a cloud, perhaps someone forgot to specify 'session_name' to enforce using the session?
226: cloud = (Cloud) session.getAttribute("cloud_"
227: + getCloudName());
228: }
229: }
230: return cloud;
231: }
232:
233: /**
234: * Obtains an 'anonymous' cloud.
235: */
236: final protected Cloud getAnonymousCloud() {
237: try {
238: return ContextProvider.getDefaultCloudContext().getCloud(
239: getCloudName());
240: } catch (org.mmbase.security.SecurityException e) {
241: log.debug("could not generate anonymous cloud");
242: // give it up
243: return null;
244: }
245: }
246:
247: /**
248: * Obtains a cloud using 'class' security. If e.g. you authorize org.mmbase.servlet.ImageServlet
249: * by class-security for read all rights, it will be used.
250: * @since MMBase-1.8
251: */
252: protected Cloud getClassCloud() {
253: try {
254: return ContextProvider.getDefaultCloudContext().getCloud(
255: getCloudName(), "class", null); // testing Class Security
256: } catch (java.lang.SecurityException e) {
257: log.debug("could not generate class cloud");
258: // give it up
259: return null;
260: }
261: }
262:
263: /**
264: * Tries to find a Cloud which can read the given node.
265: * @since MMBase-1.8
266: */
267: protected Cloud findCloud(Cloud c, String nodeNumber,
268: QueryParts query) throws IOException {
269:
270: if (c == null || !(c.mayRead(nodeNumber))) {
271: c = getClassCloud();
272: }
273:
274: if (c == null || !(c.mayRead(nodeNumber))) {
275: c = getCloud(query);
276: }
277: if (c == null || !(c.mayRead(nodeNumber))) { // cannot find any cloud what-so-ever,
278: HttpServletResponse res = query.getResponse();
279: if (res != null) {
280: res.sendError(HttpServletResponse.SC_FORBIDDEN,
281: "Permission denied to anonymous for node '"
282: + nodeNumber + "'");
283: }
284: return null;
285: }
286: return c;
287: }
288:
289: /**
290: * Servlets would often need a node. This function provides it.
291: * @param query A QueryParts object, which you must have obtained by {@link #readQuery}
292: */
293:
294: final protected Node getNode(QueryParts query) throws IOException {
295: try {
296: if (log.isDebugEnabled()) {
297: log.debug("query : " + query);
298: }
299:
300: if (query == null) {
301: return null;
302: } else {
303: Node n = query.getNode();
304: if (n != null) {
305: return n;
306: }
307: }
308:
309: Cloud c = getAnonymousCloud(); // first try anonymously always, because then session has not to be used
310:
311: String nodeNumber = java.net.URLDecoder.decode(query
312: .getNodeNumber(), "UTF-8");
313:
314: if (c != null && !c.hasNode(nodeNumber)) {
315: // ok, support for 'title' aliases too....
316: Node desperateNode = desperatelyGetNode(c, nodeNumber);
317: if (desperateNode != null) {
318: query.setNode(desperateNode);
319: return desperateNode;
320: }
321: HttpServletResponse res = query.getResponse();
322: if (res != null) {
323: res.sendError(HttpServletResponse.SC_NOT_FOUND,
324: "Node '" + nodeNumber + "' does not exist");
325: query.getRequest().setAttribute(MESSAGE_ATTRIBUTE,
326: "Node '" + nodeNumber + "' does not exist");
327: }
328: return null;
329: }
330:
331: c = findCloud(c, nodeNumber, query);
332: if (c == null) {
333: return null;
334: }
335:
336: Node n = c.getNode(nodeNumber);
337: query.setNode(n);
338: return n;
339: } catch (Exception e) {
340: HttpServletResponse res = query.getResponse();
341: if (res != null) {
342: query.getResponse().sendError(
343: HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
344: e.toString());
345: }
346: return null;
347: }
348: }
349:
350: /**
351: * Extensions can override this, to produce a node, even if cloud.hasNode failed. ('title aliases' e.g.).
352: * @since MMBase-1.7.5
353: */
354: protected Node desperatelyGetNode(Cloud cloud, String nodeIdentifier) {
355: return null;
356: }
357:
358: /**
359: * If the node associated with the resonse is another node then the node associated with the request.\
360: * (E.g. a icache based on a url with an image node).
361: * @param qp A QueryParts object, which you must have obtained by {@link #readQuery}
362: * @param node The node which is specified on the URL (obtained by {@link #getNode}
363: * @since MMBase-1.7.4
364: */
365: protected Node getServedNode(QueryParts qp, Node node)
366: throws IOException {
367: return node;
368: }
369:
370: /**
371: * The idea is that a 'bridge servlet' on default serves 'nodes', and that there could be
372: * defined a 'last modified' time for nodes. This can't be determined right now, so 'now' is
373: * returned.
374: *
375: * This function is defined in HttpServlet
376: * {@inheritDoc}
377: **/
378: protected long getLastModified(HttpServletRequest req) {
379: if (lastModifiedField == null)
380: return -1;
381: try {
382: QueryParts query = readQuery(req, null);
383: Node node = getServedNode(query, getNode(query));
384: if (node != null) { // && node.getNodeManager().hasField(lastModifiedField)) {
385: return node.getDateValue(lastModifiedField).getTime();
386: } else {
387: return -1;
388: }
389: } catch (IOException ieo) {
390: return -1;
391: }
392: }
393:
394: /**
395: * Inits lastmodifiedField.
396: * {@inheritDoc}
397: */
398:
399: public void init() throws ServletException {
400: super .init();
401: lastModifiedField = getInitParameter("lastmodifiedfield");
402: if ("".equals(lastModifiedField))
403: lastModifiedField = null;
404: log = Logging.getLoggerInstance(BridgeServlet.class);
405: if (lastModifiedField != null) {
406: log.service("Field '" + lastModifiedField
407: + "' will be used to calculate lastModified");
408: }
409: }
410:
411: /**
412: * Keeps track of determined information, to avoid redetermining it.
413: */
414: final static public class QueryParts {
415: private String sessionName;
416: private String nodeIdentifier;
417: private HttpServletRequest req;
418: private HttpServletResponse res;
419: private Node node;
420: private Node servedNode;
421: private String fileName;
422:
423: QueryParts(String sessionName, String nodeIdentifier) {
424: this .sessionName = sessionName;
425: this .nodeIdentifier = nodeIdentifier;
426:
427: }
428:
429: void setNode(Node node) {
430: this .node = node;
431: }
432:
433: Node getNode() {
434: return node;
435: }
436:
437: void setServedNode(Node node) {
438: this .servedNode = node;
439: }
440:
441: Node getServedNode() {
442: return servedNode;
443: }
444:
445: void setFileName(String fn) {
446: fileName = fn;
447: }
448:
449: public String getFileName() {
450: return fileName;
451: }
452:
453: public String getSessionName() {
454: return sessionName;
455: }
456:
457: public String getNodeNumber() {
458: int i = nodeIdentifier.indexOf('+');
459: if (i > 0) {
460: return nodeIdentifier.substring(0, i);
461: } else {
462: return nodeIdentifier;
463: }
464: }
465:
466: void setRequest(HttpServletRequest req) {
467: this .req = req;
468: }
469:
470: void setResponse(HttpServletResponse res) {
471: this .res = res;
472: }
473:
474: HttpServletRequest getRequest() {
475: return req;
476: }
477:
478: HttpServletResponse getResponse() {
479: return res;
480: }
481:
482: /**
483: * @since MMBase-1.7.4
484: */
485: public String getNodeIdentifier() {
486: return nodeIdentifier;
487: }
488:
489: public String toString() {
490: return sessionName == null ? nodeIdentifier : "session="
491: + sessionName + "+" + nodeIdentifier;
492: }
493:
494: }
495:
496: /**
497: * Just to test to damn regexp
498: */
499: public static void main(String[] argv) {
500:
501: Matcher m = FILE_PATTERN.matcher(argv[0]);
502: if (!m.matches()) {
503: System.out.println("Didn't match");
504: } else {
505: System.out.println("Found node " + m.group(1));
506: }
507: }
508:
509: }
|