Java tutorial
/* Copyright (C) 2006-2014 Helge Hess This file is part of Go. Go is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. Go is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Go; see the file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.getobjects.appserver.core; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.util.Collection; import java.util.Formatter; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.getobjects.appserver.elements.WOHTMLDynamicElement; import org.getobjects.appserver.products.GoProductManager; import org.getobjects.appserver.products.WOPackageLinker; import org.getobjects.appserver.publisher.GoClass; import org.getobjects.appserver.publisher.GoClassRegistry; import org.getobjects.appserver.publisher.GoDefaultRenderer; import org.getobjects.appserver.publisher.GoObjectRequestHandler; import org.getobjects.appserver.publisher.GoSecurityException; import org.getobjects.appserver.publisher.IGoAuthenticator; import org.getobjects.appserver.publisher.IGoCallable; import org.getobjects.appserver.publisher.IGoContext; import org.getobjects.appserver.publisher.IGoObject; import org.getobjects.appserver.publisher.IGoObjectRenderer; import org.getobjects.appserver.publisher.IGoObjectRendererFactory; import org.getobjects.appserver.publisher.IGoSecuredObject; import org.getobjects.foundation.INSExtraVariables; import org.getobjects.foundation.NSException; import org.getobjects.foundation.NSJavaRuntime; import org.getobjects.foundation.NSObject; import org.getobjects.foundation.NSSelector; import org.getobjects.foundation.UObject; /** * This is the main entry class for Go web applications. You usually * start writing a Go app by subclassing this class. It then provides all * the setup of the Go infrastructure (creation of session and resource * managers, handling of initial requests, etc etc) * <p> * The default name for the subclass is 'Application', alongside 'Context' * for a WOContext subclass and 'Session' for the app specific WOSession * subclass. * <p> * A typical thing one might want to setup in an Application subclass is a * connection to the database. * <p> * When you host within Jetty, this is a typical main() function for a Go based * web application: * <pre> * public static void main(String[] args) { * new WOJettyRunner(PackBack.class, args).run(); * } * </pre> * FIXME: document it way more.<br> * FIXME: document how it works in a Servlet environment<br> * FIXME: document how properties are located and loaded * * <h3>Differences to WebObjects</h3> * FIXME: document all the diffs ;-) * * <h4>QuerySession</h4> * In addition to Context and Session subclasses, Go has the concept of a * 'QuerySession'. The baseclass is WOQuerySession and an application can * subclass this.<br> * FIXME: document more * <h4>Zope like Object Publishing</h4> * FIXME: document all this. Class registry, product manager, root object, * renderer factory. * <br> * Request handler processing can be turned on and off. * * <h4>pageWithName()</h4> * In Go this supports component specific resource managers, not just the * global one. The WOApplication pageWithName takes this into account, it * is NOT the fallback root lookup (and thus can be used in all contexts). * It first checks the active WOComponent for the resource manager. */ public class WOApplication extends NSObject implements IGoObject, IGoObjectRendererFactory, INSExtraVariables { protected static final Log log = LogFactory.getLog("WOApplication"); protected static final Log pageLog = LogFactory.getLog("WOPages"); protected static final Log profile = LogFactory.getLog("WOProfiling"); protected AtomicInteger requestCounter = new AtomicInteger(0); protected AtomicInteger activeDispatchCount = new AtomicInteger(0); protected WOCORSConfig corsConfig; protected WORequestHandler defaultRequestHandler; protected Map<String, WORequestHandler> requestHandlerRegistry; protected Properties volatileProperties; protected Properties properties; protected WOSessionStore sessionStore; protected WOStatisticsStore statisticsStore; protected GoClassRegistry goClassRegistry; protected GoProductManager goProductManager; protected Class contextClass; protected Class sessionClass; protected Class querySessionClass; protected int pageCacheSize; protected int permanentPageCacheSize; protected String name; /* extra attributes (used when KVC does not resolve to a key) */ protected ConcurrentHashMap<String, Object> extraAttributes = null; /** * The constructor is triggered by WOServletAdaptor. */ public WOApplication() { this.extraAttributes = new ConcurrentHashMap<String, Object>(32); } public void init() { /* at the very beginning, load configuration */ this.loadProperties(); /* add CORS origins */ this.corsConfig = new WOCORSConfig(this.properties); /* page caches */ this.pageCacheSize = UObject.intValue(this.properties.getProperty("WOPageCacheSize", "5")); this.permanentPageCacheSize = UObject .intValue(this.properties.getProperty("WOPermanentPageCacheSize", "5")); /* global objects */ this.goClassRegistry = new GoClassRegistry(this); this.goProductManager = new GoProductManager(this); this.resourceManager = WOPackageLinker.linkApplication(this); this.requestHandlerRegistry = new ConcurrentHashMap<String, WORequestHandler>(4); this.registerInitialRequestHandlers(); this.setupDefaultClasses(); this.sessionStore = WOSessionStore.serverSessionStore(); this.statisticsStore = new WOStatisticsStore(); } /* Note: this is called by WOPackageLinker.linkApplication() */ public void linkDefaultPackages(WOPackageLinker _linker) { _linker.linkFramework(WOHTMLDynamicElement.class.getPackage().getName()); _linker.linkFramework(WOApplication.class.getPackage().getName()); } /** * This method registers the default request handlers, that is: * <ul> * <li>WODirectActionRequestHandler ('wa' and 'x') * <li>WOResourceRequestHandler ('wr', 'WebServerResources', 'Resources') * <li>WOComponentRequestHandler ('wo') * </ul> */ protected void registerInitialRequestHandlers() { WORequestHandler rh; rh = new WODirectActionRequestHandler(this); this.registerRequestHandler(rh, this.directActionRequestHandlerKey()); this.registerRequestHandler(rh, "x"); this.setDefaultRequestHandler(rh); rh = new WOResourceRequestHandler(this); this.registerRequestHandler(rh, this.resourceRequestHandlerKey()); this.registerRequestHandler(rh, "WebServerResources"); this.registerRequestHandler(rh, "Resources"); rh = new WOComponentRequestHandler(this); this.registerRequestHandler(rh, this.componentRequestHandlerKey()); } /** * Configures the WOContext, WOSession and WOQuerySession subclasses, if the * application has such. This is based on the package the WOApplication * subclass lives in. * <p> * Sample: if you have an app called org.packback.Packback, it'll * automagically use org.packback.Context, org.packback.Session and * org.packback.QuerySession as the respective subclasses if such exist. */ protected void setupDefaultClasses() { /* try to find a Context/Session in the application package */ String pkgname = this.getClass().getName(); int idx = pkgname.lastIndexOf('.'); pkgname = (idx == -1) ? "" : pkgname.substring(0, idx + 1); String ctxClassName = this.contextClassName(); if (ctxClassName == null) ctxClassName = "Context"; this.contextClass = NSJavaRuntime.NSClassFromString(pkgname + ctxClassName); this.sessionClass = NSJavaRuntime.NSClassFromString(pkgname + "Session"); this.querySessionClass = NSJavaRuntime.NSClassFromString(pkgname + "QuerySession"); } /* notifications */ /** * This method is called by handleRequest() when the application starts to * process a given request. Since it has no WOContext parameter its rather * useless :-) */ public void awake() { } /** * The balancing method to awake(). Called at the end of the handleRequest(). */ public void sleep() { } /* request handling */ /** * The root object for the Go URL traversal process. Per default we start * at the application object. * We might want to change that, eg rendering an application object is not * that useful. A root folder object might be better in such a scenario. * * @param _ctx - the WOContext the lookup will happen in * @param _path - the path to be looked up * @return the Object where the GoLookup process will start */ public Object rootObjectInContext(final WOContext _ctx, String[] _path) { return this; } /** * If the result of the GoLookup process was not a GoCallable (something which * can be Go-invoked), this method will get called to determine a default * callable. * <p> * If the request is a GET or POST, this will look for a Go slot called * 'default' (Zope uses index_html, and OFS also adds 'index'). * For all other requests (PUT, PROPFIND, etc), the default method name equals * the HTTP verb. * <p> * Notably objects are not required to have default methods. * * @param _object - the result of the Go path lookup * @param _ctx - the context of the whole operation * @return a default method object, or null if there is none */ public IGoCallable lookupDefaultMethod(Object _object, final WOContext _ctx) { // TBD: SOPE also had a feature called 'redirect-to-default'. Eg if you // open /persons/, it might redirect you to /persons/index. // This can be useful to make relative URLs work right (Zope uses // the base-URL in HTML for this) String defaultMethodName = "default"; boolean isPOST = false; if (_object == null) return null; /* figure out default method name, we use Zope2 semantics */ final WORequest rq = _ctx != null ? _ctx.request() : null; if (rq != null) { final String m = rq.method(); if (!"GET".equals(m) && !"POST".equals(m)) defaultMethodName = m; // use HTTP Verb as default name else if ("POST".equals(m)) isPOST = true; } // TBD: Should default methods support acquisition? Maybe, other methods // are acquired too? Object o = null; if (isPOST) { // if POST exists, use it. Otherwise fallback to 'default'. o = IGoSecuredObject.Utility.lookupName(_object, "POST", _ctx, false /* do not acquire? */); } if (o == null) { o = IGoSecuredObject.Utility.lookupName(_object, defaultMethodName, _ctx, false /* do not acquire? */); } if (o == null) { if (log.isInfoEnabled()) { log.info("did not find default method '" + defaultMethodName + "' in " + _object); } return null; } if (o instanceof IGoCallable) { if (log.isDebugEnabled()) log.debug("using default method " + o); return (IGoCallable) o; } if (o instanceof NSException) // runtime exceptions, no throw necessary throw (NSException) o; else if (o instanceof Exception) { Exception e = (Exception) o; log.error("Exception during default method lookup", e); NSException ne = new NSException(e.getMessage()); throw ne; } else { log.warn("Object returned as default method is not a callable: " + o); // TBD: throw an exception or not? return null; } } /** * This method does GoStyle request processing. It is not called by * dispatchRequest (this calls handleRequest on the request handler). * Only here for legacy reasons. * * @param _rq - the WORequest to dispatch * @return the resulting WOResponse */ @Deprecated public WOResponse handleRequest(final WORequest _rq) { return new GoObjectRequestHandler(this).handleRequest(_rq); } /** * We currently support two styles of URL handling. Either the old WO style * where the WORequestHandler is responsible for all the URL decoding etc or * the GoStyle, where the application object splits the URL and performs its * traversal process (note that handlers will still get called when they are * mapped!). * <p> * The default implementation returns 'false', that is, the GoStyle is used * per default. * * @return true if the WORequestHandler should be responsible, false if not */ public boolean useHandlerRequestDispatch() { return false; } /** * The main entry method which is called by the Servlet adaptor. It invokes * the appropriate request handler or does a GoStyle path lookup. * * @param _rq - a WORequest * @return a WOResponse */ public WOResponse dispatchRequest(final WORequest _rq) { WOResponse r = null; final int rqId = this.requestCounter.incrementAndGet(); this.activeDispatchCount.incrementAndGet(); if (profile.isInfoEnabled()) this.logRequestStart(_rq, rqId); /* CORS. This is not the perfect place to do this - the objects themselves * should decide their origin policy. But we have to get started somehow ;-) */ final String origin = _rq.headerForKey("origin"); Map<String, List<String>> corsHeaders = null; if (origin != null && origin.length() > 0) { corsHeaders = this.validateOriginOfRequest(origin, _rq); if (corsHeaders == null) { r = new WOResponse(_rq); r.setStatus(403); r.appendContentString("Origin is not permitted: " + origin); } else _rq._setCORSHeaders(corsHeaders); } /* Catch OPTIONS, doesn't work with CORS and Authentication. Maybe that is * the right thing to do anyways? Or should OPTIONS /missing return a 404? */ if (_rq.method().equals("OPTIONS")) { final WOContext tmpctx = new WOContext(this, _rq); log.debug("Not using object publishing for OPTIONS ..."); r = this.optionsForObjectInContext(null, tmpctx); } /* and here comes the regular processing */ if (r == null) { /* select WORequestHandler to process the request */ final WORequestHandler rh; if (this.useHandlerRequestDispatch()) { /* * This is the regular WO approach, derive a request handler from * the URL and then pass the request on for processing to that handler. */ rh = this.requestHandlerForRequest(_rq); } else { /* This method does the GoStyle request processing. Its called by * dispatchRequest() if requesthandler-processing is turned off. * Otherwise the handleRequest() method of the respective request * handler is called! */ rh = new GoObjectRequestHandler(this); } if (rh == null) { log.error("got no request handler for request: " + _rq); r = null; } else { try { r = rh.handleRequest(_rq); } catch (Exception e) { log.error("WOApplication caught exception", e); r = null; } } } /* CORS, add headers to response */ if (corsHeaders != null && r != null && corsHeaders.size() > 0) { if (r.isStreaming()) // already sets the CORS headers log.info("CORS: cannot add headers, response is streaming ..."); else { for (final String key : corsHeaders.keySet()) r.setHeadersForKey(corsHeaders.get(key), key); } } /* finish up */ if (!isCachingEnabled()) { /* help with debugging weak references */ log.info("running full garbage collection (WOCachingEnabled is off)"); System.gc(); } this.activeDispatchCount.decrementAndGet(); if (profile.isInfoEnabled()) this.logRequestEnd(_rq, rqId, r); return r; } /* CORS */ public WOResponse optionsForObjectInContext(final Object _clientObject, final WOContext _ctx) { // FIXME: This might not be the right place. But calling 'renderObject' // also seems quite wrong. // TBD: Should this check whether the clientObject supports PUT and // such, and enables CORS, etc? // Access-Control-Request-Headers: origin, x-requested-with WOResponse r = new WOResponse(_ctx.request()); r.setStatus(200); // FIXME: add all relevant headers return r; } public Map<String, List<String>> validateOriginOfRequest(final String _origin, final WORequest _rq) { if (this.corsConfig != null) return this.corsConfig.validateOriginOfRequest(_origin, _rq); return null; } /* rendering results */ public static final NSSelector selRendererForObjectInContext = new NSSelector("renderObjectInContext", new Class[] { Object.class, WOContext.class }); /** * This methods determines the renderer for the given object in the given * context. * <ul> * <li>if the object is null, we return null * <li>if the object is a GoSecurityException, we check whether the * authenticator of the exceptions acts as a IGoObjectRendererFactory. * If this returns a result, it is used as the renderer. * <li>next, if there is a context the * IGoObjectRendererFactory.Utility.rendererForObjectInContext() * function is called in an attempt to locate a renderer by traversing * the path, looking for a IGoObjectRendererFactory which can return * a result. * <li>then, the products are checked for appropriate renderers, by * invoking the rendererForObjectInContext() of the product manager. * <li>and finally the GoDefaultRenderer will get used (if it can process * the object) * </ul> * * @param _o - the object which shall be rendered * @param _ctx - the context in which the rendering should happen * @return a renderer object (usually an IGoRenderer) */ public Object rendererForObjectInContext(Object _o, WOContext _ctx) { if (_o == null) return null; /* special support for authentication infrastructure */ // TBD: this is somewhat mixed up, works in association with handleException if (_o instanceof GoSecurityException) { IGoAuthenticator authenticator = ((GoSecurityException) _o).authenticator(); if (authenticator instanceof IGoObjectRendererFactory) { Object renderer = ((IGoObjectRendererFactory) authenticator).rendererForObjectInContext(_o, _ctx); if (renderer != null) return renderer; } } /* look in traversal path */ if (_ctx != null) { final Object renderer = IGoObjectRendererFactory.Utility.rendererForObjectInContext(_o, _ctx); if (renderer != null) return renderer; } /* check the products for a renderer */ if (this.goProductManager != null) { final Object renderer = this.goProductManager.rendererForObjectInContext(_o, _ctx); if (renderer != null) return renderer; } /* use default renderer (if he accepts ;-) */ if (GoDefaultRenderer.sharedRenderer.canRenderObjectInContext(_o, _ctx)) return GoDefaultRenderer.sharedRenderer; return null; } /** * Renders the given object in the given context. It does so by looking up * a 'renderer' object (usually an IGoObjectRenderer) using * rendererForObjectInContext() and then calling renderObjectInContext() * on it. * <p> * In the default configuration this will usually use the GoDefaultRenderer * which can deal with quite a few setups. * * @param _result - the object to be rendered * @param _ctx - the context in which the rendering should happen * @return a WOResponse containing the rendered results */ public WOResponse renderObjectInContext(Object _result, WOContext _ctx) { if (false) { // this is non-sense, we might have already called a method and the result // is a plain null. // A 404 must be explicitly triggered by *lookup*. if (_result == null) { // TODO: add some customizable way to deal with this (to return some // custom error page) final WOResponse r = _ctx.response(); r.setStatus(WOMessage.HTTP_STATUS_NOT_FOUND); r.appendContentHTMLString("did not find requested path"); return r; } } /* lookup renderer (by walking the traversal path) */ Object renderer = this.rendererForObjectInContext(_result, _ctx); /* check renderer */ if (renderer == null) { log.error("did not find a renderer for result: " + _result); final WOResponse r = _ctx.response(); r.setStatus(WOMessage.HTTP_STATUS_INTERNAL_ERROR); r.appendContentHTMLString("did not find renderer for result of class: " + (_result != null ? _result.getClass() : "NULL")); return r; } /* render */ Object renderError; if (renderer instanceof IGoObjectRenderer) { IGoObjectRenderer typedRenderer = ((IGoObjectRenderer) renderer); renderError = typedRenderer.renderObjectInContext(_result, _ctx); } else { try { renderError = selRendererForObjectInContext.invoke(renderer, new Object[] { _result, _ctx }); } catch (IllegalArgumentException e) { renderError = e; } catch (NoSuchMethodException e) { renderError = e; } catch (IllegalAccessException e) { renderError = e; } catch (InvocationTargetException e) { renderError = e; } } if (renderError != null) { /* some error occurred */ /* render the error ... */ // TODO: need to avoid unlimited recursion? return this.renderObjectInContext(renderError, _ctx); } /* everything was great */ return _ctx.response(); } /** * This method is called by the GoDefaultRenderer if its asked to render a * WOApplication object. This usually means that the root-URL of the * application was accessed. * The default implementation will return a redirect to the "wa/Main/default" * GoPath. * * @param _ctx - the WOContext the request happened in * @return a WOResponse to be used for the application object */ public WOResponse redirectToApplicationEntry(WOContext _ctx) { // TBD: Add a behaviour which makes sense for Go based applications, // eg redirect to default method. // This is called by renderObjectInContext() final WORequestHandler drh = this.defaultRequestHandler(); final WOResourceManager rm = this.resourceManager(); String url = null; /* Note: in both cases we use the DA request handler for entry */ // TBD: it would be better to perform a GoTraversal to check whether // wa/DirectAction/default or wa/Main/default // can be processed. if ((drh instanceof WODirectActionRequestHandler) && rm != null) { if (rm != null && rm.lookupDirectActionClass("DirectAction") != null) url = "DirectAction/default"; } if (url == null && rm != null && rm.lookupComponentClass("Main") != null) url = "Main/default"; if (url == null) { log.warn("Did not find DirectAction or Main class for initial request!"); return null; } final Map<String, Object> qd = new HashMap<String, Object>(1); final Map<String, Object[]> fv = _ctx.request().formValues(); if (fv != null) qd.putAll(fv); if (_ctx.hasSession()) qd.put(WORequest.SessionIDKey, _ctx.session().sessionID()); else qd.remove(WORequest.SessionIDKey); // could be in the form values url = _ctx.directActionURLForActionNamed(url, qd); // TODO: some devices, eg mobile ones, might have issues here final WOResponse r = _ctx.response(); r.setStatus(WOMessage.HTTP_STATUS_FOUND /* Redirect */); r.setHeaderForKey(url, "location"); return r; } /* request logging */ /** * This method is called when 'info' is enabled in the profile logger. * * @param _rq - the WORequest * @param _rqId - the numeric ID of the request (counter) */ protected void logRequestStart(final WORequest _rq, final int _rqId) { final StringBuilder sb = new StringBuilder(512); sb.append("WOApp["); sb.append(_rqId); sb.append("] "); if (_rq != null) { sb.append(_rq.method()); sb.append(" "); sb.append(_rq.uri()); final String[] qks = _rq.formValueKeys(); if (qks != null && qks.length > 0) { sb.append(" F["); for (String qk : qks) { sb.append(" "); sb.append(qk); // do not log passwords if (qk.startsWith("pass") || qk.startsWith("pwd")) { sb.append("=HIDE"); continue; } final Object[] vs = _rq.formValuesForKey(qk); if (vs != null && vs.length > 0) { sb.append('='); boolean isFirst = true; for (Object v : vs) { if (isFirst) isFirst = false; else sb.append(","); if (v == null) { sb.append(",[null]"); continue; } String s = v.toString(); if (s.length() > 0) { if (s.length() > 16) s = s.substring(0, 14) + ".."; sb.append(s); } } } } sb.append(" ]"); } final Collection<WOCookie> cookies = _rq.cookies(); if (cookies != null && cookies.size() > 0) { sb.append(" C["); WOCookie.addCookieInfo(cookies, sb); sb.append("]"); } } else sb.append("no request"); profile.info(sb.toString()); } /** * This method is called when 'info' is enabled in the profile logger. * * @param _rq - the WORequest * @param _rqId - the numeric ID of the request (counter) * @param _r - the generated WOResponse */ protected void logRequestEnd(WORequest _rq, int _rqId, WOResponse _r) { final StringBuilder sb = new StringBuilder(512); sb.append("WOApp["); sb.append(_rqId); sb.append("] "); if (_r != null) { String s; final int status = _r.status(); sb.append(status); if ((s = _r.headerForKey("content-length")) != null) { sb.append(" "); sb.append(s); } else if (!_r.isStreaming()) { int len = _r.content().length; sb.append(" len="); sb.append(len); } if ((s = _r.headerForKey("content-type")) != null) { sb.append(' '); sb.append(s); } final Collection<WOCookie> cookies = _r.cookies(); if (cookies != null && cookies.size() > 0) { sb.append(" C["); WOCookie.addCookieInfo(cookies, sb); sb.append("]"); } if (status == 302 && (s = _r.headerForKey("location")) != null) { sb.append(" 302["); sb.append(s); sb.append(']'); } } else sb.append("no response"); if (_rq != null) { final double duration = _rq.requestDurationSinceStart(); if (duration > 0.0) { // TBD: are there more efficient ways to do this? (apparently there is // no way to cache the parsed format?) Formatter formatter = new Formatter(sb, Locale.US); formatter.format(" (%.3fs)", duration); formatter.close(); // didn't know that ;-) } } profile.info(sb.toString()); } /* request handler */ /** * Returns the WORequestHandler which is responsible for the given request. * This retrieves the request handler key from the request. If there is none, * or if the key maps to nothing the <code>defaultRequestHandler()</code> is * used. * Otherwise the WORequestHandler stored for the key will be returned. * * @param _rq - the WORequest to be handled * @return a WORequestHandler object responsible for processing the request */ public WORequestHandler requestHandlerForRequest(final WORequest _rq) { WORequestHandler rh; String k; if ("/favicon.ico".equals(_rq.uri())) { log.debug("detected favicon.ico request, use resource handler."); rh = this.requestHandlerRegistry.get(this.resourceRequestHandlerKey()); if (rh != null) return rh; } if ((k = _rq.requestHandlerKey()) == null) { log.debug("no request handler key in request, using default:" + _rq.uri()); return this.defaultRequestHandler(); } if ((rh = this.requestHandlerRegistry.get(k)) != null) return rh; log.debug("did not find request handler key, using default: " + k + " / " + _rq.uri()); return this.defaultRequestHandler(); } /** * Maps the given request handler to the given request handler key. * * @param _rh - the request handler object to be mapped * @param _key - the request handler key which will trigger the handler */ public void registerRequestHandler(WORequestHandler _rh, final String _key) { this.requestHandlerRegistry.put(_key, _rh); } public String[] registeredRequestHandlerKeys() { return (String[]) (this.requestHandlerRegistry.keySet().toArray()); } public void setDefaultRequestHandler(final WORequestHandler _rh) { // THREAD: may not be called at runtime this.defaultRequestHandler = _rh; } public WORequestHandler defaultRequestHandler() { return this.defaultRequestHandler; } public String directActionRequestHandlerKey() { return this.properties.getProperty("WODirectActionRequestHandlerKey", "wa"); } public String componentRequestHandlerKey() { return this.properties.getProperty("WOComponentRequestHandlerKey", "wo"); } public String resourceRequestHandlerKey() { return this.properties.getProperty("WOResourceRequestHandlerKey", "wr"); } /** * This method explicitly sets the name of the application. If no name is set, * we will usually use the short name of the application class. * * @param _name - the application name to be used */ public void _setName(final String _name) { this.name = _name; } /** * Returns the name of the application. This is either the name set using * _setName(), or the simple (short) name of the WOApplication subclass * (eg HelloWorld). * * @return the name */ public String name() { return this.name != null ? this.name : this.getClass().getSimpleName(); } /** * Sets volatile properties, i.e. properties provided via the command * line or debugger. * @param _properties - non-permanent properties used during this run */ public void _setVolatileProperties(final Properties _properties) { this.volatileProperties = _properties; } /** * This is just called once, during initialization. Subclasses can override * the method to return the name of an own WOContext class. Per default this * return 'null' which triggers the default behaviour of looking for a class * named "Context" which lives beside the WOApplication subclass. * * @return a fully qualified name of a WOContext subclass, or null */ public String contextClassName() { return null; /* use Context class of application package */ } public WOContext createContextForRequest(final WORequest _rq) { if (this.contextClass == null) return new WOContext(this, _rq); return (WOContext) NSJavaRuntime.NSAllocateObject(this.contextClass, new Class[] { WOApplication.class, WORequest.class }, new Object[] { this, _rq }); } /* page handling */ /** * Primary method for user code to generate new WOComponent objects. This is * also called by WOComponent.pageWithName(). * <p> * The method first locates a WOResourceManager by asking the active * component, and if this has none, it uses the WOResourceManager set in the * application.<br> * It then asks the WOResourceManager to instantiate the page. Afterwards it * awakes the component in the given WOContext. * <p> * Again: do not trigger the WOResourceManager directly, always use this * method (or WOComponent.pageWithName()) to acquire WOComponents. * * @param _pageName - the name of the WOComponent to instantiate * @param _ctx - the context for the component * @return the WOComponent or null if the WOResourceManager found none */ public WOComponent pageWithName(String _pageName, final WOContext _ctx) { pageLog.debug("pageWithName:" + _pageName); WOResourceManager rm = null; final WOComponent cursor = _ctx != null ? _ctx.component() : null; if (cursor != null) rm = cursor.resourceManager(); if (rm == null) rm = this.resourceManager(); if (rm == null) { pageLog.error("did not find a resource manager to instantiate: " + _pageName); return null; } final WOComponent page = rm.pageWithName(_pageName, _ctx); if (page == null) { pageLog.error("could not instantiate page " + _pageName + " using: " + rm); return null; } page.ensureAwakeInContext(_ctx); return page; } /* sessions */ /** * Sets the session store of the application. * <em>Important!</em> only call this method in properly locked sections, the * sessionStore ivar is not protected. * Usually you should only call this in the applications init() method or * constructor. * * @param _wss - the session store to be used with this application. */ public void setSessionStore(final WOSessionStore _wss) { // NOTE: only call in threadsafe sections! this.sessionStore = _wss; } /** * Returns the active session store. * * @return the WOSessionStore used for preserving WOSession objects */ public WOSessionStore sessionStore() { return this.sessionStore; } /** * Uses the configured WOSessionStore to unarchive a WOSession for the current * request(/context). * All code should use this method instead of directly dealing with the * session store. * <p> * Note: this method also checks out the session from the store to avoid * concurrent modifications! * * @param _sid - the ID of the session, eg retrieved from the WORequest wosid * @param _ctx - the WOContext in which the session should be restored * @return the restored and awake session or null if it could not be restored */ public WOSession restoreSessionWithID(final String _sid, WOContext _ctx) { final WORequest rq = _ctx != null ? _ctx.request() : null; //System.err.println("RESTORE: " + _sid + ": " + rq.cookies()); if (_sid == null) { log.info("attempt to restore session w/o session-id"); return null; } final WOSessionStore st = this.sessionStore(); if (st == null) { log.info("cannot restore session, no store is available: " + _sid); return null; } WOSession sn = st.checkOutSessionForID(_sid, rq); if (sn == null && rq != null) { /* check all cookies */ Collection<String> vals = rq.cookieValuesForKey(WORequest.SessionIDKey); if (vals != null) { if (vals.size() > 1 && log.isWarnEnabled()) log.warn("multiple sid-cookies in request: " + rq.cookies()); for (String sid : vals) { if (sid == null) continue; if (sid.equals(_sid)) continue; // already checked that sn = st.checkOutSessionForID(_sid, rq); if (sn != null) break; } } } if (sn != null) { if (log.isDebugEnabled()) log.debug("checked out session: " + sn.sessionID()); _ctx.setSession(sn); sn._awakeWithContext(_ctx); } else if (log.isInfoEnabled()) log.info("could not checkout session: " + _sid); return sn; } /** * Save the session to a store and check it in. * * @param _ctx - the context which contains the session to be stored * @return true if the session could be stored, false on error */ public boolean saveSessionForContext(final WOContext _ctx) { // TBD: check whether we always properly check in session! (whether we always // call this method in case we unarchived a session) if (_ctx == null || !_ctx.hasSession()) return false; final WOSessionStore st = this.sessionStore(); if (st == null) { log.error("cannot save session, missing a session store!"); return false; } /* first put session to sleep */ final WOSession sn = _ctx.session(); if (sn != null) { sn._sleepWithContext(_ctx); if (log.isInfoEnabled()) log.info("WOApp: checking in session: " + sn.sessionID()); } st.checkInSessionForContext(_ctx); return true; } /** * Can be overridden by subclasses to configure whether an application should * refuse to accept new session (eg when its in shutdown mode). * The method always returns false in the default implementation. * * @return true when new sessions are forbidden, false if not. */ public boolean refusesNewSessions() { return false; } /** * This is called by WORequest or our handleRequest() in case a session needs * to be created. It calls createSessionForRequest() to instantiate the clean * session object. It then registers the session in the context and performs * wake up (calls awakeWithContext()). * * @param _ctx the context in which the session shall be active initially. * @return a fresh session */ public WOSession initializeSession(final WOContext _ctx) { if (_ctx == null) { log.info("got no context in initializeSession!"); return null; } final WOSession sn = this.createSessionForRequest(_ctx.request()); if (sn == null) { log.debug("createSessionForRequest returned null ..."); return null; } final Object tov = this.defaults().valueForKey("WOSessionTimeOut"); if (tov != null) { int to = UObject.intValue(tov); if (to < 1) log.error("unexpected WOSessionTimeOut value (must be >0s):" + tov); else sn.setTimeOut(to); } _ctx.setNewSession(sn); sn._awakeWithContext(_ctx); // TODO: post WOSessionDidCreateNotification return sn; } /** * Called by initializeSession to create a new session for the given request. * <p> * This method is a hook for subclasses which want to change the class of * the WOSession object based on the request. If they just want to change the * static class, they can change the 'sessionClass' ivar. * * @param _rq the request which is associated with the new session. * @return a new, not-yet-awake session */ public WOSession createSessionForRequest(final WORequest _rq) { if (this.sessionClass == null) return new WOSession(); return (WOSession) NSJavaRuntime.NSAllocateObject(this.sessionClass); } // TBD: I think this configures how 'expires' is set public boolean isPageRefreshOnBacktrackEnabled() { return true; } /** * This method gets called by WOContext if its asked to restore a query * session. If you want to store complex objects in your session, you might * want to override this. */ public WOQuerySession restoreQuerySessionInContext(final WOContext _ctx) { if (_ctx == null) { log.error("attempt to restore query session w/o context!"); return null; } if (this.querySessionClass == null) return new WOQuerySession(_ctx); return (WOQuerySession) NSJavaRuntime.NSAllocateObject(this.querySessionClass, _ctx); } /* resource manager */ // TODO: consider threading issues protected WOResourceManager resourceManager; /* Note: set by linker */ /** * Sets the resource manager. Be careful with this, its not thread safe, hence * you may only replace the RM when no requests are happening. * * @param _rm - the new WOResourceManager */ public void setResourceManager(final WOResourceManager _rm) { this.resourceManager = _rm; } public WOResourceManager resourceManager() { return this.resourceManager; } /* error handling */ public WOActionResults handleException(Throwable _e, WOContext _ctx) { /* support for security infrastructure */ if (_e instanceof GoSecurityException) { IGoAuthenticator authenticator = ((GoSecurityException) _e).authenticator(); if (authenticator instanceof IGoObjectRendererFactory) return this.renderObjectInContext(_e, _ctx); if (log.isDebugEnabled()) { if (authenticator == null) log.debug("security exception provided no authenticator: " + _e); else { log.debug( "authenticator provided by exception is not a renderer " + "factory: " + authenticator); } } } else if (_e instanceof SecurityException) { WOResponse r = _ctx.response(); if (r == null) r = new WOResponse(_ctx != null ? _ctx.request() : null); r.setStatus(403); ; log.warn("Core Java security exception", _e); return r; } if (true) { // new behavior for testing final WOResponse r = this.renderObjectInContext(_e, _ctx); return r; } // TODO: improve exception page, eg include stacktrace _ctx.response().appendContentHTMLString("fail: " + _e.toString()); _e.printStackTrace(); return _ctx.response(); } public WOActionResults handleSessionRestorationError(final WOContext _ctx) { // TODO: improve exception page _ctx.response().appendContentHTMLString("sn fail: " + _ctx.toString()); return _ctx.response(); } public WOActionResults handleMissingAction(String _action, WOContext _ctx) { /* this is called if a direct action could not be found */ // TODO: improve exception page _ctx.response().appendContentHTMLString("missing action: " + _action); return _ctx.response(); } /* licensing */ public static final boolean licensingAllowsMultipleInstances() { return true; } public static final boolean licensingAllowsMultipleThreads() { return true; } public static final int licensedRequestLimit() { return 100000 /* number of requests (not more than that per window) */; } public static final long licensedRequestWindow() { return 1 /* ms */; } /* statistics */ public WOStatisticsStore statisticsStore() { return this.statisticsStore; } /* responder */ /** * This starts the takeValues phase of the request processing. In this phase * the relevant objects fill themselves with the state of the request before * the action is invoked. * <p> * The default method calls the takeValuesFromRequest() of the WOSession, if * one is active. Otherwise it enters the contexts' page and calls * takeValuesFromRequest() on it. */ public void takeValuesFromRequest(final WORequest _rq, final WOContext _ctx) { if (_ctx == null) return; if (_ctx.hasSession()) _ctx.session().takeValuesFromRequest(_rq, _ctx); else { final WOComponent page = _ctx.page(); if (page != null) { _ctx.enterComponent(page, null /* component content */); try { page.takeValuesFromRequest(_rq, _ctx); } finally { _ctx.leaveComponent(page); } } } } /** * This triggers the invokeAction phase of the request processing. In this * phase the relevant objects got their form values pushed in and the action * is ready to be performed. * <p> * The default method calls the invokeAction() of the WOSession, if * one is active. Otherwise it enters the contexts' page and calls * invokeAction() on it. */ public Object invokeAction(final WORequest _rq, final WOContext _ctx) { final Object result; if (_ctx.hasSession()) { result = _ctx.session().invokeAction(_rq, _ctx); } else { final WOComponent page = _ctx.page(); if (page != null) { _ctx.enterComponent(page, null /* component content */); try { result = page.invokeAction(_rq, _ctx); } finally { _ctx.leaveComponent(page); } } else result = null; } return result; } /** * Render the page stored in the WOContext. This works by calling * appendToResponse() on the WOSession, if there is one. If there is none, * the page set in the context will get invoked directly. * * @param _response - the response * @param _ctx - the context */ public void appendToResponse(WOResponse _response, final WOContext _ctx) { if (_ctx.hasSession()) _ctx.session().appendToResponse(_response, _ctx); else { final WOComponent page = _ctx.page(); if (page != null) { _ctx.enterComponent(page, null /* component content */); try { page.appendToResponse(_response, _ctx); } finally { _ctx.leaveComponent(page); } } else if (log.isInfoEnabled()) log.info("called WOApp.appendToResponse w/o a page"); } } /* properties */ /** * Loads the configuration for the application. This is called very early in * the configuration process because settings like WOCachingEnabled affect * subsequent initialization. * <p> * Properties will be loaded in order from: * <ul> * <li>Defaults.properties in the Go package * <li>Defaults.properties in your application package * <li>ApplicationName.properties in the current directory (user.dir) * </ul> */ protected void loadProperties() { InputStream in; final Properties sysProps = System.getProperties(); this.properties = new Properties(); /* First load the internal properties of WOApplication */ in = WOApplication.class.getResourceAsStream("Defaults.properties"); if (!this.loadProperties(in)) log.error("failed to load Defaults.properties of WOApplication"); /* Try to load the Defaults.properties resource which is located * right beside the WOApplication subclass. */ in = this.getClass().getResourceAsStream("Defaults.properties"); if (!this.loadProperties(in)) log.error("failed to load Defaults.properties of application"); /* Load configuration from the current directory. We might want * to change the lookup strategy ... */ File f = this.userDomainPropertiesFile(); if (f != null && f.exists()) { try { in = new FileInputStream(f); if (!this.loadProperties(in)) log.error("failed to load user domain properties: " + f); else log.info("did load user domain properties: " + f); } catch (FileNotFoundException e) { log.error("did not find user domains file: " + f); } } for (Object key : sysProps.keySet()) { /* we add all keys starting with uppercase, weird, eh?! ;-) * this passes through WOxyz stuff and avoids the standard Java props */ if (!(key instanceof String)) continue; if (Character.isUpperCase(((String) key).charAt(0))) this.properties.put(key, sysProps.get(key)); } /* Finally, add volatile properties set by the adapter */ if (this.volatileProperties != null) { for (Object key : this.volatileProperties.keySet()) { if (!(key instanceof String)) continue; this.properties.put(key, this.volatileProperties.get(key)); } } } /** * Loads a properties configuration file from the current directory. For * example if your WOApplication subclass is named 'HelloWorld', this will * attempt to locate a file called 'HelloWorld.properties'. * * @return a File object representing the properties file */ protected File userDomainPropertiesFile() { // Note: I don't think 'user.dir' actually returns something. The system // properties are probably reset by Jetty String fn = this.getClass().getSimpleName() + ".properties"; return new File(System.getProperty("user.dir", "."), fn); } /** * Load a given properties stream into the application properties object. * * @param _in - a stream containing properties * @return true if the loading was successful, false on error */ protected boolean loadProperties(final InputStream _in) { if (_in == null) return true; /* yes, true, resource was not found, no load error */ try { this.properties.load(_in); return true; } catch (IOException ioe) { return false; } } /* a trampoline to make the properties accessible via KVC */ protected NSObject defaults = new NSObject() { public void takeValueForKey(final Object _value, final String _key) { // do nothing, we do not mutate properties } public Object valueForKey(final String _key) { return WOApplication.this.properties.getProperty(_key); } }; public NSObject defaults() { return this.defaults; } public boolean isCachingEnabled() { return UObject.boolValue(this.properties.get("WOCachingEnabled")); } /* During development, you might want to load all resources from a specific * directory (where all sources reside) */ public String projectDirectory() { return this.properties != null ? UObject.stringValue(this.properties.get("WOProjectDirectory")) : null; } /* page cache */ public WOResponse handlePageRestorationErrorInContext(final WOContext _ctx) { log.warn("could not restore page from context: " + _ctx); final WOResponse r = _ctx.response(); r.setStatus(500 /* server error */); // TBD?! r.appendContentString("<h1>You have backtracked too far</h1>"); return r; } public void setPermanentPageCacheSize(int _size) { this.permanentPageCacheSize = _size; } public int permanentPageCacheSize() { return this.permanentPageCacheSize; } public void setPageCacheSize(int _size) { this.pageCacheSize = _size; } public int pageCacheSize() { return this.pageCacheSize; } /* KVC */ @Override public Object handleQueryWithUnboundKey(final String _key) { return this.objectForKey(_key); } @Override public void handleTakeValueForUnboundKey(Object _value, final String _key) { this.setObjectForKey(_value, _key); } /* GoClass */ public GoClass goClassInContext(final IGoContext _ctx) { return _ctx.goClassRegistry().goClassForJavaObject(this, _ctx); } /* GoObject */ public Object lookupName(String _name, IGoContext _ctx, boolean _acquire) { if (_name == null) return null; /* a few hardcoded object pathes */ // TODO: why hardcode? move it to a GoClass! if ("s".equals(_name)) return this.sessionStore(); if ("stats".equals(_name)) return this.statisticsStore(); if ("favicon.ico".equals(_name)) { log.debug("detected favicon.ico name, returning resource handler."); return this.requestHandlerRegistry.get(this.resourceRequestHandlerKey()); } /* check class */ final GoClass goClass = this.goClassInContext(_ctx); if (goClass != null) { final Object o = goClass.lookupName(this, _name, _ctx); if (o != null) return o; } /* request handlers */ return this.requestHandlerRegistry.get(_name); } public GoClassRegistry goClassRegistry() { return this.goClassRegistry; } public GoProductManager goProductManager() { return this.goProductManager; } /* extra attributes */ public void setObjectForKey(final Object _value, final String _key) { if (_value == null) { this.removeObjectForKey(_key); return; } this.extraAttributes.put(_key, _value); } public void removeObjectForKey(final String _key) { if (this.extraAttributes == null) return; this.extraAttributes.remove(_key); } public Object objectForKey(final String _key) { if (_key == null || this.extraAttributes == null) return null; return this.extraAttributes.get(_key); } public Map<String, Object> variableDictionary() { return this.extraAttributes; } /* description */ public Log log() { return log; } @Override public void appendAttributesToDescription(final StringBuilder _d) { super.appendAttributesToDescription(_d); if (this.name != null) { _d.append(" name="); _d.append(this.name); } _d.append(" #reqs="); _d.append(this.requestCounter.get()); _d.append("/"); _d.append(this.activeDispatchCount.get()); if (this.extraAttributes != null) this.appendExtraAttributesToDescription(_d); } public void appendExtraAttributesToDescription(final StringBuilder _d) { if (this.extraAttributes == null || this.extraAttributes.size() == 0) return; _d.append(" vars="); boolean isFirst = true; for (String ekey : this.extraAttributes.keySet()) { if (isFirst) isFirst = false; else _d.append(","); _d.append(ekey); final Object v = this.extraAttributes.get(ekey); if (v == null) _d.append("=null"); else if (v instanceof Number) { _d.append("="); _d.append(v); } else if (v instanceof String) { String s = (String) v; _d.append("=\""); if (s.length() > 16) s = s.substring(0, 14) + ".."; _d.append(s); _d.append('\"'); } } } } /* Local Variables: c-basic-offset: 2 tab-width: 8 End: */