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 org.mmbase.module.Module;
013: import org.mmbase.module.core.MMBase;
014: import org.mmbase.module.core.MMBaseContext;
015:
016: import javax.servlet.ServletContext;
017: import javax.servlet.ServletException;
018:
019: import javax.servlet.http.HttpServlet;
020: import javax.servlet.http.HttpServletRequest;
021: import javax.servlet.http.HttpServletResponse;
022:
023: import java.io.IOException;
024: import java.io.PrintWriter;
025:
026: import java.util.*;
027:
028: import org.w3c.dom.*;
029: import org.xml.sax.InputSource;
030: import org.mmbase.util.logging.Logging;
031: import org.mmbase.util.logging.Logger;
032: import org.mmbase.util.xml.DocumentReader;
033:
034: /**
035: * MMBaseServlet is a base class for other MMBase servlets (like ImageServlet). Its main goal is to
036: * store a MMBase instance for all its descendants, but it can also be used as a serlvet itself, to
037: * show MMBase version information.
038: *
039: * @version $Id: MMBaseServlet.java,v 1.71 2007/11/28 17:10:04 michiel Exp $
040: * @author Michiel Meeuwissen
041: * @since MMBase-1.6
042: */
043: public class MMBaseServlet extends HttpServlet implements MMBaseStarter {
044:
045: private static final Logger log = Logging
046: .getLoggerInstance(MMBaseServlet.class);
047: /**
048: * MMBase reference. While null, servlet does not accept request.
049: */
050: protected MMBase mmbase = null;
051: // private static String context;
052:
053: // ----------------
054: // members needed for refcount functionality.
055:
056: /**
057: * To keep track of the currently running servlets, you can use this logger.
058: *
059: */
060: private static final Logger servletsLog = Logging
061: .getLoggerInstance("org.mmbase.SERVLETS");
062:
063: private static int servletCount; // Number of running servlets
064: /**
065: * Lock to sync add and remove of threads
066: */
067: private static final Object servletCountLock = new Object();
068: /**
069: * Map containing currently running servlets
070: */
071: private static Map<MMBaseServlet, ServletReferenceCount> runningServlets = new HashMap<MMBaseServlet, ServletReferenceCount>();
072: /**
073: * Toggle to print running servlets to log.
074: * @javadoc Not clear, I don't understand it.
075: */
076: private static int printCount;
077:
078: private static int servletInstanceCount = 0;
079: // servletname -> servletmapping
080: // obtained from web.xml
081: private static Map<String, List<String>> servletMappings = new HashMap<String, List<String>>();
082: // topic -> servletname
083: // set by isntantiated servlets
084: private static Map<String, ServletEntry> associatedServlets = new HashMap<String, ServletEntry>();
085: // topic -> servletmapping
086: // set by instantiated servlets
087: private static Map<String, ServletEntry> associatedServletMappings = new HashMap<String, ServletEntry>();
088: // mapping to servlet instance
089: private static Map<String, HttpServlet> mapToServlet = new HashMap<String, HttpServlet>();
090:
091: private long start = System.currentTimeMillis();
092:
093: /**
094: * Boolean indicating whether MMBase has been started. Used by {@link #checkInited}, set to true {@link #by setMMBase}.
095: * @since MMBase-1.7
096: */
097: private static boolean mmbaseInited = false;
098:
099: /**
100: * If MMBase has not been started, a 503 is given, with this value for the 'Retry-After' header.
101: * See <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.4">rfc 2616, section 10.5.4</a>.
102: * Defaults to 60 seconds, can be configured in web.xml with the 'retry-after' propery on the servlets.
103: * @since MMBase-1.7.2
104: */
105: protected int retryAfter = 60;
106:
107: /**
108: * Thread starting MMBase
109: */
110: private Thread initThread;
111:
112: /**
113: * On default, servlets are not associated with any function.
114: *
115: * This function is called in the init method.
116: *
117: * @return A map of Strings (function) -> Integer (priority). Never null.
118: */
119:
120: protected Map<String, Integer> getAssociations() {
121: return new HashMap<String, Integer>();
122: }
123:
124: /**
125: * Used in association map
126: */
127: private static class ServletEntry {
128: ServletEntry(String n) {
129: this (n, null);
130: }
131:
132: ServletEntry(String n, Integer p) {
133: name = n;
134: if (p == null) {
135: priority = 0;
136: } else {
137: priority = p.intValue();
138: }
139: }
140:
141: String name;
142: int priority;
143: }
144:
145: /**
146: * Returns the MMBase instance.
147: * @since MMBase-1.7
148: */
149: public MMBase getMMBase() {
150: return mmbase;
151: }
152:
153: /**
154: * Sets the mmbase member. Can be overriden to implement extra initalization for the servlet which needs a running MMBase.
155: * @since MMBase-1.7
156: */
157: public void setMMBase(MMBase mmb) {
158: Calendar cal = Calendar.getInstance();
159: cal.setTime(new java.util.Date(System.currentTimeMillis()
160: - start));
161: if (!mmbaseInited) {
162: log
163: .info("MMBase servlets are ready to receive requests, started in "
164: + cal.get(Calendar.MINUTE)
165: + " min "
166: + cal.get(Calendar.SECOND) + " sec.");
167: }
168:
169: mmbase = mmb;
170: mmbaseInited = true;
171: initThread = null;
172: }
173:
174: /**
175: * Used in checkInited.
176: */
177: private static ServletException initException = null;
178:
179: /**
180: * Called by MMBaseStartThread, if something went wrong during
181: * initialization of MMBase. It will be thrown by checkInited
182: * then.
183: * @since MMBase-1.7
184: */
185: public void setInitException(ServletException e) {
186: initException = e;
187: }
188:
189: /**
190: * The init of an MMBaseServlet checks if MMBase is running. It not then it is started.
191: */
192: public void init() throws ServletException {
193:
194: ServletContext servletContext = getServletConfig()
195: .getServletContext();
196:
197: String retryAfterParameter = servletContext
198: .getInitParameter("retry-after");
199: if (retryAfterParameter == null) {
200: // default: one minute
201: retryAfter = 60;
202: } else {
203: retryAfter = Integer.valueOf(retryAfterParameter);
204: }
205:
206: if (!MMBaseContext.isInitialized()) {
207: MMBaseContext.init(servletContext);
208: MMBaseContext.initHtmlRoot();
209: }
210:
211: log.info("Init of servlet " + getServletName() + ".");
212: boolean initialize = false;
213: // for retrieving servletmappings, determine status
214: synchronized (servletMappings) {
215: initialize = (servletInstanceCount == 0);
216: servletInstanceCount++;
217: }
218: if (initialize) {
219: // used to determine the accurate way to access a servlet
220: try {
221:
222: MMBaseContext.initHtmlRoot();
223: // get config and do stuff.
224: java.net.URL url;
225: try {
226: url = getServletConfig().getServletContext()
227: .getResource("/WEB-INF/web.xml");
228: } catch (NoSuchMethodError nsme) {
229: // for old app-servers.
230: log.error(nsme);
231: url = (new java.io.File(getServletConfig()
232: .getServletContext().getRealPath(
233: "/WEB-INF/web.xml"))).toURL();
234: }
235: if (url == null) {
236: log.warn("No web.xml found");
237: } else {
238: InputSource path = new InputSource(url.openStream());
239: log.service("Reading servlet mappings from " + url);
240: DocumentReader webDotXml = new DocumentReader(path,
241: false);
242:
243: for (Element mapping : webDotXml.getChildElements(
244: "web-app", "servlet-mapping")) {
245: Element servName = webDotXml
246: .getElementByPath(mapping,
247: "servlet-mapping.servlet-name");
248: String name = webDotXml
249: .getElementValue(servName);
250: if (!(name.equals(""))) {
251: Element urlPattern = webDotXml
252: .getElementByPath(mapping,
253: "servlet-mapping.url-pattern");
254: String pattern = webDotXml
255: .getElementValue(urlPattern);
256: if (!(pattern.equals(""))) {
257: List<String> ls = servletMappings
258: .get(name);
259: if (ls == null) {
260: ls = new ArrayList<String>();
261: servletMappings.put(name, ls);
262: }
263: ls.add(pattern);
264: }
265: }
266: }
267: }
268: } catch (Exception e) {
269: log.error(e.getMessage(), e);
270: }
271: log.debug("Loaded servlet mappings");
272: }
273: log.debug("Associating this servlet with functions");
274: for (Map.Entry<String, Integer> e : getAssociations()
275: .entrySet()) {
276: associate(e.getKey(), getServletName(), e.getValue());
277: }
278: log.debug("Associating this servlet with mappings");
279: for (String mapping : getServletMappings(getServletConfig()
280: .getServletName())) {
281: mapToServlet.put(mapping, this );
282: }
283:
284: String hold = servletContext.getInitParameter("stall-server");
285: if ("yes".equals(hold) || "true".equals(hold)) {
286: log.service(getServletName()
287: + ": Waiting until MMBase is started");
288: Runnable starter = new MMBaseStartThread.Job(this );
289: starter.run();
290: log.service(getServletName()
291: + ": Ready to receive requests.");
292: } else {
293: // stuff that can take indefinite amount of time (database down and so on) is done in separate thread
294: initThread = new MMBaseStartThread(this );
295: initThread.start();
296: }
297: }
298:
299: /**
300: * Gets the servlet that belongs to the given mapping
301: *
302: * @param mapping the mapping used to access the servlet
303: * @return the Servlet that handles the mapping
304: */
305: public static HttpServlet getServletByMapping(String mapping) {
306: return mapToServlet.get(mapping);
307: }
308:
309: /**
310: * Gets all the mappings for a given servlet. So, this is a method to obtain info from web.xml.
311: *
312: * @param servletName the name of the servlet
313: * @return an unmodifiable list of servlet mappings for this servlet
314: */
315: public static List<String> getServletMappings(String servletName) {
316: List<String> ls = servletMappings.get(servletName);
317: if (ls == null) {
318: return Collections.emptyList();
319: } else {
320: return Collections.unmodifiableList(ls);
321: }
322: }
323:
324: /**
325: * Gets all the mappings for a given association.
326: *
327: * Use this to find out how to call a servlet to handle a certain
328: * type of operation or data (i.e 'images', 'attachments').
329: *
330: *
331: * @param function the function that identifies the type of association
332: * @return an unmodifiable list of servlet mappings associated with the function
333: */
334: public static List<String> getServletMappingsByAssociation(
335: String function) {
336: // check if any mappings were explicitly set for this function
337: // if so, return that list.
338: ServletEntry mapping = associatedServletMappings.get(function);
339: if (mapping != null) {
340: List<String> mappings = new ArrayList<String>();
341: mappings.add(mapping.name);
342: return mappings;
343: }
344: // otherwise, get the associated servet
345: String name = getServletByAssociation(function);
346: if (name != null) {
347: return getServletMappings(name);
348: } else {
349: return Collections.emptyList();
350: }
351: }
352:
353: /**
354: * Gets the name of the servlet that performs actions associated with the
355: * the given function.
356: *
357: * Use this to find a servlet to handle a certain type of
358: * operation or data (i.e 'imageservlet', 'myimageservlet',
359: * 'images');
360: *
361: * @param function the function that identifies the type of association
362: * @return the name of the servlet associated with the function, or null if there is none
363: */
364: public static String getServletByAssociation(String function) {
365: ServletEntry e = associatedServlets.get(function);
366: if (e != null) {
367: return e.name;
368: } else {
369: return null;
370: }
371: }
372:
373: /**
374: * @since MMBase-1.8.5
375: */
376: public static String getBasePath(String function) {
377: List<String> ls = MMBaseServlet
378: .getServletMappingsByAssociation(function);
379: if (ls.size() == 0)
380: return null;
381: String baseUrl = ls.get(0);
382: int pos = baseUrl.lastIndexOf("*");
383: if (pos > 0) {
384: baseUrl = baseUrl.substring(0, pos);
385: }
386: pos = baseUrl.indexOf("*");
387: if (pos == 0) {
388: baseUrl = baseUrl.substring(pos + 1);
389: }
390: return baseUrl;
391: }
392:
393: /**
394: * Associate a given servlet with the given function.
395: * Use this to set a servlet to handle a certain type of operation or data (i.e 'image-processing');
396: * For now, only one servlet can be registered.
397: * @param function the function that deidentifies the type of association
398: * @param servletname name of the servlet to associate with the function
399: * @param priority priority of this association, the association only occurs if no servlet or servletmapping
400: * with higher priority for the same function is present already
401: */
402: private static synchronized void associate(String function,
403: String servletName, Integer priority) {
404: if (priority == null)
405: priority = 0;
406: ServletEntry m = associatedServletMappings.get(function);
407: if (m != null && (priority.intValue() < m.priority))
408: return;
409: ServletEntry e = associatedServlets.get(function);
410: if (e != null && (priority.intValue() < e.priority))
411: return;
412: log.service("Associating function '"
413: + function
414: + "' with servlet name "
415: + servletName
416: + (e == null ? "" : " (previous assocation was with "
417: + e.name + ")")
418: + (m == null ? "" : " (previous assocation was with "
419: + m.name + ")"));
420: associatedServlets.put(function, new ServletEntry(servletName,
421: priority));
422: if (m != null) {
423: associatedServletMappings.remove(function);
424: }
425: }
426:
427: /**
428: * Associate a given servletmapping with the given function.
429: * Use this to set a servletmapping to call for a certain type of operation or data (i.e 'image-processing');
430: * For now, only one servletmapping can be registered.
431: * @param function the function that identifies the type of association
432: * @param servletMapping mapping of the servlet to associate with the function
433: * @param priority priority of this association, the association only occurs if no servlet or servletmapping
434: * with higher priority for the same function is present already
435: */
436: protected static synchronized void associateMapping(
437: String function, String servletMapping, Integer priority) {
438: if (priority == null)
439: priority = 0;
440: ServletEntry m = associatedServletMappings.get(function);
441: if (m != null && (priority.intValue() < m.priority))
442: return;
443: ServletEntry e = associatedServlets.get(function);
444: if (e != null && (priority.intValue() < e.priority))
445: return;
446: log.service("Associating function '"
447: + function
448: + "' with servlet mapping "
449: + servletMapping
450: + (e == null ? "" : " (previous assocation was with "
451: + e.name + ")")
452: + (m == null ? "" : " (previous assocation was with "
453: + m.name + ")"));
454: associatedServletMappings.put(function, new ServletEntry(
455: servletMapping, priority));
456: if (e != null) {
457: associatedServlets.remove(function);
458: }
459: }
460:
461: /**
462: * Serves MMBase version information. This doesn't do much usefull
463: * yet, but one could image lots of cool stuff here. Any other
464: * MMBase servlet will probably override this method.
465: */
466: protected void doGet(HttpServletRequest req, HttpServletResponse res)
467: throws ServletException, IOException {
468: res.setContentType("text/plain");
469: PrintWriter pw = res.getWriter();
470: pw.print(org.mmbase.Version.get());
471: String q = req.getQueryString();
472: if ("starttime".equals(q)) {
473: res.setHeader("Cache-Control", "no-cache");
474: pw.print("\nUp since "
475: + new Date((long) MMBase.startTime * 1000));
476: } else if ("uptime".equals(q)) {
477: res.setHeader("Cache-Control", "no-cache");
478: int seconds = (int) (System.currentTimeMillis() / 1000)
479: - MMBase.startTime;
480: int days = seconds / (60 * 60 * 24);
481: seconds %= 60 * 60 * 24;
482: int hours = seconds / (60 * 60);
483: seconds %= 60 * 60;
484: int minutes = seconds / 60;
485: seconds %= 60;
486: pw.print("\nUptime: "
487: + (days == 1 ? "1 day" : (days > 1 ? "" + days
488: + " days" : ""))
489: + (hours > 0 || days > 0 ? " "
490: + (hours == 1 ? "1 hour" : "" + hours
491: + " hours") : "")
492: + (minutes > 0 || hours > 0 ? " "
493: + (minutes == 1 ? "1 minute" : "" + minutes
494: + " minutes") : "")
495: + (seconds > 0 || minutes > 0 ? " "
496: + (seconds == 1 ? "1 second" : "" + seconds
497: + " seconds") : ""));
498:
499: } else if ("server".equals(q)) {
500: String appserver = System.getProperty("catalina.base"); // to do: similar arrangment for
501: // other ap-servers.
502: String root = "" + getServletContext().getResource("/");
503: pw.print("\n"
504: + getServletContext().getServerInfo()
505: + " "
506: + System.getProperty("java.version")
507: + " ("
508: + System.getProperty("java.vendor")
509: + ") "
510: + (appserver == null ? "" : appserver)
511: + "@"
512: + java.net.InetAddress.getLocalHost()
513: .getCanonicalHostName() + " "
514: + System.getProperty("os.name") + " "
515: + System.getProperty("os.version") + " "
516: + System.getProperty("os.arch") + "\n" + root);
517:
518: }
519: pw.close();
520: }
521:
522: /**
523: * This methods can be (and is) called in the beginning of
524: * service. It sends an UNAVAILABLE error if MMBase has not bee
525: * started, or throws an exeption if that was unsuccessful.
526: * @return A boolean. If false, then service must return immediately (because mmbase has not been inited yet).
527: * @since MMBase-1.7.2
528: */
529: protected boolean checkInited(HttpServletResponse res)
530: throws ServletException, IOException {
531: if (initException != null) {
532: throw initException;
533: }
534:
535: if (!mmbaseInited) {
536: res.setHeader("Retry-After", "" + retryAfter);
537: res
538: .sendError(
539: HttpServletResponse.SC_SERVICE_UNAVAILABLE,
540: "MMBase not yet, or not successfully initialized (check mmbase log)");
541: }
542: return mmbaseInited;
543: }
544:
545: /**
546: * The service method is extended with calls for the refCount
547: * functionality (for performance related debugging). So you can
548: * simply override doGet in extension classes, and this stays
549: * working, without having to think about it.
550: */
551: public void service(HttpServletRequest req, HttpServletResponse res)
552: throws ServletException, IOException {
553: if (!checkInited(res)) {
554: return;
555: }
556: incRefCount(req);
557: try {
558: super .service(req, res);
559: } finally { // whatever happens, decrease the refcount:
560: decRefCount(req);
561: }
562: }
563:
564: /**
565: * Returns information about this servlet. Don't forget to override it.
566: */
567:
568: public String getServletInfo() {
569: return "Serves MMBase version information";
570: }
571:
572: // ----------------------
573: // functions needed for refcount functionality.
574:
575: /**
576: * Return URI with QueryString appended
577: * @param req The HttpServletRequest.
578: */
579: protected static String getRequestURL(HttpServletRequest req) {
580: String result = req.getRequestURI();
581: String queryString = req.getQueryString();
582: if (queryString != null)
583: result += "?" + queryString;
584: return result;
585: }
586:
587: /**
588: * Decrease the reference count of the servlet
589: * @param req The HttpServletRequest.
590: */
591:
592: protected void decRefCount(HttpServletRequest req) {
593: if (servletsLog.isDebugEnabled()) {
594: String url = getRequestURL(req) + " " + req.getMethod();
595: synchronized (servletCountLock) {
596: servletCount--;
597: ServletReferenceCount s = runningServlets.get(this );
598: if (s != null) {
599: if (s.refCount == 0) {
600: runningServlets.remove(this );
601: } else {
602: s.refCount--;
603: int i = s.uris.indexOf(url);
604: if (i >= 0)
605: s.uris.remove(i);
606: }
607:
608: }// s!=null
609: }//sync
610: }// if (logServlets)
611: }
612:
613: /**
614: * Increase the reference count of the servlet (for debugging)
615: * and send running servlets to log once every 32 requests
616: * @param req The HttpServletRequest.
617: * @scope private
618: * @bad-constant 31 should be configurable.
619: */
620:
621: protected void incRefCount(HttpServletRequest req) {
622: if (servletsLog.isDebugEnabled()) {
623: String url = getRequestURL(req) + " " + req.getMethod();
624: int curCount;
625: synchronized (servletCountLock) {
626: servletCount++;
627: curCount = servletCount;
628: printCount++;
629: ServletReferenceCount s = runningServlets.get(this );
630: if (s == null) {
631: runningServlets.put(this ,
632: new ServletReferenceCount(url, 0));
633: } else {
634: s.refCount++;
635: s.uris.add(url);
636: }
637: }// sync
638:
639: if ((printCount & 31) == 0) { // Why not (printCount % <configurable number>) == 0?
640: if (curCount > 0) {
641: synchronized (servletCountLock) {
642: servletsLog.debug("Running servlets: "
643: + curCount);
644: for (Object element : runningServlets.values())
645: servletsLog.debug(element);
646: }
647:
648: }// curCount>0
649: }
650: }
651: }
652:
653: public void destroy() {
654: log.info("Servlet " + getServletName()
655: + " is taken out of service");
656: if (initThread != null) {
657: initThread.interrupt();
658: } else {
659: log.debug(" " + getServletName() + " was not initialized");
660: }
661: log.debug("Disassociating this servlet with mappings");
662: for (String mapping : getServletMappings(getServletConfig()
663: .getServletName())) {
664: mapToServlet.remove(mapping);
665: }
666: super .destroy();
667: // for retrieving servletmappings, determine status
668: synchronized (servletMappings) {
669:
670: servletInstanceCount--;
671: if (servletInstanceCount == 0) {
672: try {
673: log.info("Unloaded servlet mappings");
674: associatedServlets.clear();
675: servletMappings.clear();
676: log
677: .info("No MMBase servlets left; modules can be shut down");
678: MMBase.getMMBase().shutdown();
679: Module.shutdownModules();
680: } catch (Throwable t) {
681: log.error(t.getMessage(), t);
682: }
683: try {
684: ThreadGroup threads = MMBaseContext
685: .getThreadGroup();
686: log.service("Send interrupt to "
687: + threads.activeCount() + " threads in "
688: + threads + " of " + threads.getParent());
689: threads.interrupt();
690: Thread.yield();
691: } catch (Throwable t) {
692: log.error(t.getMessage(), t);
693: }
694: try {
695: org.mmbase.util.FileWatcher.shutdown();
696: } catch (Throwable t) {
697: log.error(t.getMessage(), t);
698: }
699: try {
700: org.mmbase.cache.CacheManager.shutdown();
701: } catch (Throwable t) {
702: log.error(t.getMessage(), t);
703: }
704: try {
705: Logging.shutdown();
706: } catch (Throwable t) {
707: System.err.println(t.getMessage());
708: }
709: }
710: }
711: }
712:
713: /**
714: * This class maintains current state information for a running servlet.
715: * It contains a reference count, as well as a list of URI's being handled by the servlet.
716: */
717: private class ServletReferenceCount {
718: /**
719: * List of URIs that call the servlet
720: */
721: final List<String> uris = new ArrayList<String>();
722: /**
723: * Nr. of references
724: */
725: int refCount;
726:
727: /**
728: * Create a new ReferenceCountServlet using the jamesServlet
729: */
730: ServletReferenceCount(String uri, int refCount) {
731: uris.add(uri);
732: this .refCount = refCount;
733: }
734:
735: /**
736: * Return a description containing servlet info and URI's
737: */
738: public String toString() {
739: return "servlet(" + MMBaseServlet.this + "), refcount("
740: + (refCount + 1) + "), uri's(" + uris + ")";
741: }
742: }
743:
744: }
|