001: /*
002: * Enhydra Java Application Server Project
003: *
004: * The contents of this file are subject to the Enhydra Public License
005: * Version 1.1 (the "License"); you may not use this file except in
006: * compliance with the License. You may obtain a copy of the License on
007: * the Enhydra web site ( http://www.enhydra.org/ ).
008: *
009: * Software distributed under the License is distributed on an "AS IS"
010: * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
011: * the License for the specific terms governing rights and limitations
012: * under the License.
013: *
014: * The Initial Developer of the Enhydra Application Server is Lutris
015: * Technologies, Inc. The Enhydra Application Server and portions created
016: * by Lutris Technologies, Inc. are Copyright Lutris Technologies, Inc.
017: * All Rights Reserved.
018: *
019: * Contributor(s):
020: *
021: * $Id: HttpPresentationManager.java,v 1.3 2007-10-19 10:05:39 sinisa Exp $
022: */
023:
024: package com.lutris.appserver.server.httpPresentation;
025:
026: //import com.lutris.http.*;
027: import java.io.IOException;
028: import java.io.InputStream;
029: import java.util.Enumeration;
030:
031: import javax.servlet.Servlet;
032: import javax.servlet.ServletContext;
033: import javax.servlet.ServletException;
034: import javax.servlet.ServletRequest;
035: import javax.servlet.http.Cookie;
036: import javax.servlet.http.HttpServletRequest;
037: import javax.servlet.http.HttpServletResponse;
038:
039: import com.lutris.appserver.server.Application;
040: import com.lutris.appserver.server.ApplicationException;
041: import com.lutris.appserver.server.StandardAppUtil;
042: import com.lutris.appserver.server.session.Session;
043: import com.lutris.appserver.server.user.User;
044: import com.lutris.appserver.server.user.UserImpl;
045: import com.lutris.logging.LogChannel;
046: import com.lutris.logging.Logger;
047: import com.lutris.util.ConfigException;
048:
049: /**
050: * Presentation manager class. Handles loading and execution of
051: * presentation objects.
052: *
053: * @version $Revision: 1.3 $
054: * @author Mark Diekhans
055: */
056: public class HttpPresentationManager {
057:
058: /**
059: * Maximum number of times to go through page redirect/error
060: * handling loops.
061: */
062: private static final int maxRedirectErrorLoops = 25;
063:
064: /**
065: * Presentation and resource loader/cache.
066: */
067: private PresentationLoader loader;
068:
069: /**
070: * Application that this presentation manager is associate with.
071: */
072: private Application application;
073:
074: /**
075: * Applications log channel. No logging is done if null.
076: */
077: private LogChannel logChannel;
078:
079: /**
080: * Is DEBUG logging enabled?
081: */
082: private boolean debugLoggingEnabled;
083:
084: /**
085: * Request id incremented on each request.
086: */
087: private long nextRequestId = 0;
088:
089: /**
090: * The Servlet we are running in. Will be an instance of
091: * <CODE>
092: * com.lutris.appserver.server.httpPresenation.HttpPresentationServlet
093: * </CODE>.
094: */
095: private Servlet servlet;
096:
097: /**
098: * The ServletContext that was used to initialize the Servlet we are
099: * running in. This is needed to, for example, get the other servlets
100: * that are our peers in the server.
101: */
102: private ServletContext servletContext;
103:
104: private long limitMilis = -11;
105:
106: private boolean initSessionUser = false;
107:
108: private String PARAM_LIMIT_MILIS = "PresentationManager.RequestExecutionLimit";
109:
110: private String PARAM_SESSION_USER = "PresentationManager.InitSessionUser";
111:
112: /**
113: * Construct a HTTP presentation manager.
114: *
115: * @param appPresentationPrefix Prefix added to the file name portion
116: * of the URL. The resulting names is used to search for classes and
117: * files on the class path.
118: * @param presApplication Application object that this presentation is
119: * associated with.
120: * @param applicationClassLoader
121: * the class loader to use for the application.
122: * @param cacheClasses Enables or disables caching of presentation object
123: * classes in memory.
124: * @param cacheFiles Enables or disables caching of files (html, gif, etc)
125: * that are servered as part of the application.
126: */
127: public HttpPresentationManager(String appPresentationPrefix,
128: Application presApplication,
129: ClassLoader applicationClassLoader, boolean cacheClasses,
130: boolean cacheFiles) throws HttpPresentationException {
131:
132: application = presApplication;
133: logChannel = application.getLogChannel();
134: if (logChannel != null) {
135: debugLoggingEnabled = logChannel.isEnabled(Logger.DEBUG);
136: }
137: loader = new PresentationLoader(appPresentationPrefix,
138: applicationClassLoader, cacheClasses, cacheFiles,
139: logChannel);
140: }
141:
142: /**
143: * The method is invoked to fulfill an HTTP request. This function is
144: * thread-safe and does not need synchronization. It is expected that the
145: * protocol layer invoking this function provide multi-threading to handle
146: * requests.
147: *
148: * The alogrithm for handling a request is:
149: * <PRE>
150: * presObj = requestUrl
151: * while request-not-handled {
152: * if not error-handler-redirect
153: * invoke-application-request-preprocessor presObj
154: * load-presentation-object-class
155: * invoke-presentation-object
156: * if not error-handler-redirect
157: * invoke-application-request-postprocessor presObj
158: * if (client-side-page-redirect-exception) {
159: * send-redirect-to-client
160: * break
161: * }
162: * if (server-side-page-redirect-exception) {
163: * (Not implemented yet)
164: * set-request-to-redirectUrl
165: * presObj = redirectUrl
166: * continue
167: * }
168: * if (401-unauthorized-exception) {
169: * set-header-and-response-code
170: * break
171: * }
172: * if (other-exception) {
173: * presObj = find-error-handler
174: * }
175: * }
176: * </PRE>
177: *
178: * @param request Request object for the protocol invoking the manager.
179: * @param response Response object for the protocol invoking the manager.
180: * @exception HttpPresentationException All exceptions will be encapsulated
181: * in an exception of this class.
182: */
183: public void Run(HttpPresentationRequest request,
184: HttpPresentationResponse response)
185: throws HttpPresentationException, IOException {
186:
187: long requestId;
188:
189: synchronized (this ) {
190: requestId = nextRequestId++;
191: }
192:
193: //FIXME: Error handle has gotten very complex and needs to be
194: // revisited (again).
195: HttpPresentationComms comms = new HttpPresentationComms(
196: request, response, application);
197:
198: /*
199: * Loop calling the presentation and then PageRedirect or ErrorHandlers
200: * until the request is handled. This is a state machine that is either
201: * loading request page or an error handler. Server side redirect can
202: * cause a state transition back to the request page state.
203: * The application can also handle the request.
204: */
205:
206: /*
207: * N.B. getPresentationObjectPathInfo may return null on an invalid
208: * URL or empty, at least with servlet runner and using just the
209: * name of the application. Handled by runPresObj.
210: */
211: String presObjPath = request
212: .getPresentationObjectRelativePath();
213:
214: if (presObjPath == null) {
215: // If presObj is null, it could be an invalid URL that just
216: // references the application. See what happens.
217: presObjPath = "/";
218: }
219:
220: String requestedPrseObjPath = presObjPath;
221: int loopCount = 0;
222:
223: // When this variable is not-null, an exception is being handled.
224: Throwable exception = null;
225:
226: presLoop: while (true) {
227: logPresObjRun(comms, presObjPath, exception,
228: requestedPrseObjPath, requestId);
229: loopCount++;
230: try {
231: if (exception == null) {
232: if (runRequestPreprocessor(comms, requestId,
233: presObjPath)) {
234: break presLoop; // Finished!
235: }
236: }
237: runPresentationObj(comms, presObjPath, exception,
238: application);
239: break presLoop; // Finished!`
240: } catch (PageRedirectException except) {
241: presObjPath = PageRedirect.handler(comms, presObjPath,
242: except, logChannel, requestId);
243: if (presObjPath == null) {
244: break presLoop; // Finished!
245: }
246: exception = null; // Server redirected, No longer handling an exception.
247: } catch (PageUnauthorizedException except) {
248: ExceptionHandler.sendUnauthorizedPage(comms,
249: logChannel, requestId, application,
250: presObjPath, except);
251:
252: break presLoop; // Finished!
253: } catch (Throwable except) {
254: logChannel.write(Logger.DEBUG, "RID:" + requestId
255: + ": exception running presentation"
256: + presObjPath, except);
257: // Don't execute error handler on client disconnect.
258: if (HttpPresentationIOException
259: .isClientIOException(except)) {
260: // We don't except this to actually display anything, just
261: // log.
262: ExceptionHandler.logDisplayException(comms,
263: logChannel, requestId, application, except,
264: request.getPathInfo());
265: // We always run the post processor
266: try {
267: runRequestPostProcessor(comms, requestId,
268: presObjPath);
269: } catch (Throwable e) {
270: // FIX - OK? - should log details of error.
271: logChannel
272: .write(
273: Logger.DEBUG,
274: "RID:"
275: + requestId
276: + ": Request Post Proecessor exception: "
277: + presObjPath + ": "
278: + e);
279: }
280: return; // Finished, but don't break to second flush.
281: }
282:
283: // Look for the next error handler.
284: String exceptpresObjPath = presObjPath;
285: presObjPath = ExceptionHandler.getErrorHandler(
286: presObjPath, requestedPrseObjPath,
287: (exception != null), loader, logChannel,
288: requestId);
289: if (presObjPath == null) {
290: // Top of tree, no more places to check.
291: ExceptionHandler.processUnhandledException(comms,
292: logChannel, requestId, application,
293: requestedPrseObjPath, except);
294: break presLoop; // Finished!
295: }
296: exception = except; // Now handling this exception.
297: }
298: if (loopCount > maxRedirectErrorLoops) {
299: ExceptionHandler.maxRedirectsReached(comms, logChannel,
300: requestId, maxRedirectErrorLoops, application,
301: presObjPath);
302: break presLoop; // Finished!
303: }
304: }
305:
306: try {
307: response.flush();
308: runRequestPostProcessor(comms, requestId, presObjPath);
309: } catch (Throwable except) {
310: // We don't except this to actually display anything, just
311: // log.
312: ExceptionHandler.logDisplayException(comms, logChannel,
313: requestId, application, except, request
314: .getPathInfo());
315: }
316: }
317:
318: /**
319: * Log the execution of a presentation object.
320: */
321: private void logPresObjRun(HttpPresentationComms comms,
322: String presObjPath, Throwable exception,
323: String requestedPrseObjPath, long requestId)
324: throws HttpPresentationException {
325: if (debugLoggingEnabled) {
326: if (exception != null) {
327: logChannel.write(Logger.DEBUG, "RID:" + requestId
328: + ": presentation run exception handler: "
329: + presObjPath + " for:\n" + " "
330: + exception.getClass().getName() + ": "
331: + exception.getMessage()
332: + "\nwhile accessing: " + requestedPrseObjPath);
333: } else if (comms.request.getMethod().equals("HEAD")) {
334: logChannel.write(Logger.DEBUG, "RID:" + requestId
335: + ": presentation head : " + presObjPath);
336: } else {
337: logChannel.write(Logger.DEBUG, "RID:" + requestId
338: + ": presentation run: " + presObjPath);
339: }
340: }
341: }
342:
343: /**
344: * This is an internal use only hook, see the description in
345: * Application.java. The call is passed through to the application.
346: */
347: public boolean servletRequestPreprocessor(Servlet me,
348: ServletContext context, HttpServletRequest request,
349: HttpServletResponse response) throws ServletException,
350: IOException {
351: if (application != null)
352: return application.servletRequestPreprocessor(me, context,
353: request, response);
354: else
355: return false;
356: }
357:
358: /**
359: * Run the request preprocessor.
360: */
361: private boolean runRequestPreprocessor(HttpPresentationComms comms,
362: long requestId, String presObjPath) throws Exception {
363: //DACHA patch: Do not allow "*.class" resource to be accesible !
364: String checkPath = presObjPath.trim();
365: if (checkPath.length() > 6) { // ".class" length = 6
366: checkPath = checkPath.substring(checkPath.length() - 6,
367: checkPath.length());
368: if (checkPath.equalsIgnoreCase(".class")) {
369: // Send ERROR message
370: comms.response
371: .sendError(501,
372: "Enhydra application: Access forbidden, *.class resource not alowed!");
373: return true;
374: }
375: }
376: //END DACHA
377: if (application.requestPreprocessor(comms)) {
378: if (debugLoggingEnabled) {
379: logChannel
380: .write(
381: Logger.DEBUG,
382: "RID:"
383: + requestId
384: + ": application.requestPreprocessor finished request: "
385: + presObjPath);
386: }
387: return true;
388: }
389: return false;
390: }
391:
392: /**
393: * Run the request post-processor.
394: */
395: private void runRequestPostProcessor(HttpPresentationComms comms,
396: long requestId, String presObjPath) throws Exception {
397: application.requestPostProcessor(comms);
398: }
399:
400: /**
401: * Load and run a presentation object (including error handlers).
402: * A return without exception indicates that the presentation has been
403: * processed. Will rethrow an exception if one is being handled and
404: * a handler is not found.
405: *
406: * @param comms
407: * comms object.
408: * @param urlPath
409: * the path of the url.
410: * @param exception
411: * if not null, it indicates that an exception or error is being handled.
412: * @param application
413: * the application
414: */
415: private void runPresentationObj(HttpPresentationComms comms,
416: String urlPath, Throwable exception, Application application)
417: throws Throwable {
418:
419: HttpPresentation presObj;
420: long milis = 0;
421: comms.exception = exception;
422:
423: try {
424: initSessionUser = application.getConfig().getBoolean(
425: PARAM_SESSION_USER, false);
426: } catch (ConfigException ex) {
427: logChannel.write(Logger.WARNING, "Couldn't get "
428: + PARAM_SESSION_USER + " parameter", ex);
429: }
430: if (limitMilis < -9) {
431: try {
432: limitMilis = application.getConfig().getInt(
433: PARAM_LIMIT_MILIS, -1);
434: } catch (ConfigException e) {
435: logChannel.write(Logger.WARNING, "Couldn't get "
436: + PARAM_LIMIT_MILIS + " parameter", e);
437: } finally {
438: if (limitMilis < 0)
439: limitMilis = -1;
440: }
441: }
442:
443: // Get the presentation object. If a class is not found and we
444: // where handling an error, rethrow that error.
445: try {
446: presObj = loader.loadPresentation(urlPath);
447: } catch (ClassNotFoundException noClass) {
448: if (exception != null) {
449: throw exception; // ErrorHandler was not found.
450: } else {
451: throw noClass;
452: }
453: }
454:
455: if (0 <= limitMilis) {
456: milis = System.currentTimeMillis();
457: }
458:
459: presObj.run(comms);
460:
461: if (initSessionUser) {
462: //Adding Request Session User to request session
463: try {
464: User userObj = comms.session.getUser();
465: String userName = null;
466: if (null == userObj) {
467: userName = comms.request.getRemoteUser();
468: if (null == userName) {
469: userName = comms.request
470: .getHeader(":remoteuser");
471: }
472: if (null == userName) {
473: userName = "ANONYMOUS";
474: }
475: userObj = new UserImpl(userName);
476: comms.session.setUser(userObj);
477: }
478: } catch (NullPointerException ex) {
479: }
480: }
481:
482: if (0 <= limitMilis) {
483:
484: milis = System.currentTimeMillis() - milis;
485: //String user = comms.request.getHeader("REMOTE_USER");
486:
487: String user = comms.request.getRemoteUser();
488: if (null == user)
489: user = comms.request.getHeader(":remoteuser");
490:
491: Cookie[] c = comms.request.getCookies();
492: if (null == user)
493: user = "ANONYMOUS";
494: user += "(";
495: StringBuffer userBuffer = new StringBuffer(user);
496: if (c != null) {
497: for (int n = 0; n < c.length; ++n) {
498: // user += c[n].getName()+"="+c[n].getValue();
499: userBuffer.append(c[n].getName() + "="
500: + c[n].getValue());
501: if (c.length - 1 != n) {
502: // user += ", ";
503: userBuffer.append(", ");
504: }
505: }
506: }
507: user = userBuffer.toString();
508: user += ")";
509: if (0 == limitMilis || milis > limitMilis) {
510: StringBuffer paramsBuffer = new StringBuffer("");
511: for (Enumeration e = comms.request.getParameterNames(); e
512: .hasMoreElements();) {
513: String pName = (String) e.nextElement();
514: paramsBuffer.append("&" + pName + "="
515: + comms.request.getParameter(pName));
516: }
517: logChannel.write(Logger.WARNING, comms.request
518: .getMethod()
519: + " "
520: + urlPath
521: + paramsBuffer.toString()
522: + " requested by "
523: + user
524: + " from "
525: + comms.request.getRemoteHost()
526: + "["
527: + comms.request.getRemoteAddr()
528: + "] took "
529: + milis + "ms to run");
530: }
531:
532: }
533: }
534:
535: /**
536: * Determine if the PO cache is enabled.
537: *
538: * @return <code>true</code> is the cache is enabled.
539: */
540: public boolean isPOCacheEnabled() {
541: return loader.isPOCacheEnabled();
542: }
543:
544: /**
545: * Return the number of entries in the PO cache.
546: *
547: * @return the number of entries in the cache or 0 is disabled.
548: */
549: public int sizeofPOCache() {
550: return loader.sizeofPOCache();
551: }
552:
553: /**
554: * Determine if the file resource class cache is enabled.
555: *
556: * @return <code>true</code> is the cache is enabled.
557: */
558: public boolean isResourceCacheEnabled() {
559: return loader.isResourceCacheEnabled();
560: }
561:
562: /**
563: * Return the number of entries in the resource cache.
564: *
565: * @return the number of entries in the cache or 0 is disabled.
566: */
567: public int sizeofResourceCache() {
568: return loader.sizeofResourceCache();
569: }
570:
571: /**
572: * Get a file associated with the application. The file is located
573: * in the same manner as presentation objects are located. That is,
574: * prepending the <CODE>presentationPrefix</CODE> to the specified
575: * path and searching the class path for a directory or JAR containing
576: * the file.
577: *
578: * @param appfileName The file name relative to the
579: * to the application's <CODE>presentationPrefix</CODE>.
580: * @return An input stream associated with the file.
581: * @exception IOException If the file can not be found or opened.
582: */
583: public InputStream getAppFileAsStream(String appFileName)
584: throws IOException, HttpPresentationException {
585: return loader.getAppFileAsStream(appFileName);
586: }
587:
588: /**
589: * Determine if a request URL references a presentation object.
590: *
591: * @param request The request for the presentation.
592: * @return true if the URL has a presentation object MIME type.
593: */
594: public boolean isPresentationRequest(HttpPresentationRequest request)
595: throws HttpPresentationException {
596: return loader.isPresentationRequest(request);
597: }
598:
599: /**
600: * Looks up the session object (if any) that would be used to
601: * process the request. This is will not used normally, because the
602: * session is give to the application's preprocessor method and the
603: * presentation objects. Also, it is not normal to have a raw
604: * ServletRequest. The debugger uses this to examine the session data
605: * before and after each request. Consider this an internal use onlyu
606: * method.
607: *
608: * @param request
609: * The (raw) request that would be sent in to this application.
610: * @return
611: * The session object that would be associated with the request.
612: * Returns null if none is found; a new session is not created.
613: */
614: public Session getSession(ServletRequest request) {
615: if (application == null)
616: return null;
617: Session s = null;
618: try {
619: s = StandardAppUtil.getRequestSession(request, application);
620: } catch (ApplicationException e) {
621: }
622: return s;
623: }
624:
625: /*
626: * The next 3 functions were added to that applications can
627: * get their neghbor servlets. In the beginning we were prepared
628: * to leave servlets behind and move on to a new api if they
629: * didn't catch on. They caught on. So in the future we could
630: * simplify the archetecture some by not hiding the servlets
631: * from the applications with an abstraction layer.
632: */
633:
634: /**
635: * Notify the presentation manager which servlet we are running in.
636: * This will be done immediatly after this object is created, if
637: * this ojbject is created by an HttpPresentationServlet (currently
638: * the only creator). In the future, if this class is used in non-servlet
639: * situations, this need not be called.
640: *
641: * @param servlet
642: * The servlet we are running in.
643: * @param servletContext
644: * The ServletContext used to initialize the servlet we are running in.
645: */
646: public void setServletAndContext(Servlet servlet,
647: ServletContext servletContext) {
648: this .servlet = servlet;
649: this .servletContext = servletContext;
650: }
651:
652: /**
653: * Get the servlet we are running in. Currently this class is only
654: * used in a servlet (HttpPresentationServlet), so this value will
655: * always be set. But in the future this class may be used in other
656: * non-servlet enviornments, in which case this method will return
657: * null.
658: *
659: * @return
660: * The servlet we are running in, if any.
661: */
662: public Servlet getServlet() {
663: return this .servlet;
664: }
665:
666: /**
667: * Get the servlet context used to initialize the servlet we are running
668: * in. Currently this class is only used in a servlet
669: * (HttpPresentationServlet), so this value will always be set. But in
670: * the future this class may be used in other non-servlet enviornments,
671: * in which case this method will return null.
672: *
673: * @return
674: * The servlet context used to initialize the servlet we are
675: * running in, if any.
676: */
677: public ServletContext getServletContext() {
678: return this .servletContext;
679: }
680:
681: /**
682: * Flush the presentation object and resource caches.
683: */
684: public void flushCache() {
685: loader.flushCache();
686: }
687:
688: /** Add a new mime type to extension mapping. */
689: public void addMimeType(String mimeType, String extension) {
690: loader.addMimeType(mimeType, extension);
691: }
692:
693: /**
694: * Get the application class loader.
695: *
696: * @return The application class loader.
697: */
698: public ClassLoader getAppClassLoader() {
699: return loader.getAppClassLoader();
700: }
701: }
|