/*
  Copyright (C) 2006 Helge Hess

  This file is part of JOPE.

  JOPE 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.

  JOPE 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 JOPE; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

package org.opengroupware.jope.appserver.core;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
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.opengroupware.jope.appserver.publisher.JoCallable;
import org.opengroupware.jope.appserver.publisher.JoClassRegistry;
import org.opengroupware.jope.appserver.publisher.JoContext;
import org.opengroupware.jope.appserver.publisher.JoObject;
import org.opengroupware.jope.appserver.publisher.JoSecurityManager;
import org.opengroupware.jope.appserver.publisher.JoTraversalPath;
import org.opengroupware.jope.foundation.NSJavaRuntime;
import org.opengroupware.jope.foundation.NSKeyValueCoding;
import org.opengroupware.jope.foundation.NSKeyValueCodingAdditions;
import org.opengroupware.jope.foundation.NSObject;

/*
 * WOApplication
 * 
 * TODO: document me
 * TODO: document how it works in a servlet environment
 * TODO: document how properties are located and loaded
 */
public class WOApplication extends NSObject
  implements JoObject
{
  private static AtomicInteger requestCounter = new AtomicInteger(0); 

  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 WORequestHandler  defaultRequestHandler;
  protected Map<String,WORequestHandler> requestHandlerRegistry;
  
  protected Properties        properties;
  protected WOSessionStore    sessionStore;
  protected WOStatisticsStore statisticsStore;
  protected JoSecurityManager joSecurityManager;
  protected JoClassRegistry   joClassRegistry;
  protected Class             contextClass;
  protected Class             sessionClass;
  
  public WOApplication() {
    this.init();
  }
  
  public void init() {
    /* at the very beginning, load configuration */
    this.loadProperties();
    
    this.linkFrameworksAndSetupResourceManager();
    this.registerInitialRequestHandlers();
    this.setupDefaultClasses();
    
    this.sessionStore      = WOSessionStore.serverSessionStore();
    this.statisticsStore   = new WOStatisticsStore();
    this.joSecurityManager = new JoSecurityManager(this);
    this.joClassRegistry   = new JoClassRegistry(this);
  }
  
  protected void linkFrameworksAndSetupResourceManager() {
    WOPackageLinker linker = new WOPackageLinker(this.isCachingEnabled());
    
    linker.linkClass(this.getClass());
    linker.linkWithSpecification(this.getClass().getResource("jopelink.txt"));
    
    /* link system frameworks */
    String sysbase = WOApplication.class.getPackage().getName();
    linker.linkFramework("org.opengroupware.jope.appserver.elements");
    linker.linkFramework(sysbase);
    
    /* retrieve the resulting resource manager */
    this.resourceManager = linker.resourceManager();
  }
  
  protected void registerInitialRequestHandlers() {
    WORequestHandler rh;
    
    this.requestHandlerRegistry = 
      new ConcurrentHashMap<String, WORequestHandler>(4);
    
    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());
  }
  
  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);
    
    this.contextClass = NSJavaRuntime.NSClassFromString(pkgname + "Context");
    this.sessionClass = NSJavaRuntime.NSClassFromString(pkgname + "Session");
  }
  
  /* notifications */
  
  public void awake() {
  }
  
  public void sleep() {
  }
  
  /* request handling */
  
  private static final String[] emptyStringArray = {};
  
  public String[] traversalPathForRequest(WORequest _rq, WOContext _ctx) {
    // TODO: maybe we should move this to WORequest or somewhere else so that
    //       WOApplication isn't cluttered that much
    // TODO: should we scan form parameters for a ":action" form values, I think
    //       so
    String uri = _rq != null ? _rq.uri() : null;
    if (uri == null)
      return null;

    /* cut off adaptor prefix */
    
    String p = _rq.adaptorPrefix();
    if (p != null && p.length() > 0) {
      if (uri.startsWith(p))
        uri = uri.substring(p.length());
    }
    
    /* clean up URI */
    
    if (uri.startsWith("/")) uri = uri.substring(1);
    if (uri.endsWith("/"))   uri = uri.substring(0, uri.length() - 1);
    if (uri.length() == 0)   return emptyStringArray;
    
    /* split URL */
    
    /* "".split returns [], "/".split returns [],
     * "/ab".split returns [, ab]
     * "ab/".split returns [ab]
     */
    String[] urlParts  = uri.split("/");
    if (urlParts.length == 0)
      return emptyStringArray;
    if (urlParts.length == 1 && urlParts[0].equals(""))
      return emptyStringArray;
    
    /* perform URL decoding (must be done after split so that '/' can be
     * encoded in the URL */
    
    String charset = WOMessage.defaultURLEncoding();
    for (int i = 0; i < urlParts.length; i++) {
      try {
        urlParts[i] = URLDecoder.decode(urlParts[i], charset);
      }
      catch (UnsupportedEncodingException e) {
        /* Note: in this case we leave the part as-is */
        log.error("could not decode part of URL: " + urlParts[i]);
      }
    }
    
    /* Now it gets interesting, do we want to include the name of the app or
     * not? For now we consume a match.
     * Eg /Calendar/abc.ics vs /MyApp/wr/doIt
     */
    if (urlParts[0].equals(this.getClass().getSimpleName())) {
      if (urlParts.length == 1) return emptyStringArray;
      String[] nURLParts = new String[urlParts.length - 1];
      System.arraycopy(urlParts, 1, nURLParts, 0, nURLParts.length);
      urlParts = nURLParts;
    }
    
    /* thats it? */
    return urlParts;
  }
  
  public Object rootObjectInContext(WOContext _ctx) {
    /* The root object, we start at the application object. We might want to
     * change that, eg rendering an application object isn't that useful. A
     * root folder object might be better in such a scenario.
     */
    return this;
  }
  
  public WOResponse handleRequest(WORequest _rq) {
    /*
     * Note: this is different to WO, we always use JoObjects to handle requests
     *       and the a WORequestHandler object is just a special kind of
     *       JoObject.
     */
    WOContext  ctx;
    WOResponse r       = null;
    WOSession  session = null;
    String     sessionId;
    boolean    debugOn = log.isDebugEnabled();

    if (debugOn) log.debug("handleRequest: " + _rq);
    
    ctx = this.createContextForRequest(_rq);
    if (debugOn) log.debug("  created context: " + ctx);
    
    /* prepare session */
    sessionId = _rq != null 
      ? _rq.stringFormValueForKey(WORequest.SessionIDKey) : null;
    if (sessionId != null) {
      if (sessionId.length() == 0)
        sessionId = null;
      else if (sessionId.equals("nil"))
        sessionId = null;
    }
    // TODO: we might also want to check cookies
    if (debugOn) log.debug("  session-id: " + sessionId);
    
    try {
      /* Note: the awake method is rather useless, since we run multithreaded */
      this.awake();
      
      /* restore session */
      
      if (true /* restoreSessionsUsingIDs */) {
        if (sessionId != null) {
          if (debugOn) log.debug("  restore session: " + sessionId);
        
          session = this.restoreSessionWithID(sessionId, ctx);
          if (session == null) {
            r = this.handleSessionRestorationError(ctx);
            sessionId = null;
          }
        }
      
        if (r == null /* no error */ && session == null) {
          /* no error, but session is not here */
          if (false /* autocreateSession */) {
            if (debugOn) log.debug("  autocreate session: " + sessionId);
            
            if (!this.refusesNewSessions()) {
              session = this.initializeSession(ctx);
              if (session == null)
                r = this.handleSessionRestorationError(ctx);
            }
            else {
              // TODO: this already failed once? will it return null again?
              r = this.handleSessionRestorationError(ctx);
            }
          }
        }
        
        if (debugOn) {
          if (session != null)
            log.debug("  restored session: " + session);
          else if (sessionId != null)
            log.debug("  did not restore session with id: " + sessionId);
        }
      }
      
      /* now perform the JoObject path traversal */
      
      JoTraversalPath tpath = null;
      if (r == null) {
        tpath = new JoTraversalPath
          (this.traversalPathForRequest(_rq, ctx),
           this.rootObjectInContext(ctx), 
           ctx);
        
        ctx._setJoTraversalPath(tpath);
        tpath.traverse();
        
        if (tpath.lastException() != null)
          r = this.handleException(tpath.lastException(), ctx);
      }
      
      // TODO: We might want to insert takeValuesFromRequest here, not sure.
      //       For now we consider the request a parameter to the JoMethod, eg
      //       a WOComponent JoMethod would do the takeValues and then issue
      //       the performActionNamed or invokeActionForRequest.
      
      /* call the JoObject */
      
      Object result = null;
      if (tpath != null && r == null) {
        result = tpath.resultObject();
        if (result != null &&
            result instanceof JoCallable &&
            ((JoCallable)result).isCallableInContext(ctx))
        {
          JoCallable method = ((JoCallable)result);
          if (debugOn)
            log.debug("call: " + method + ", with: " + tpath.clientObject());
          result = method.callInContext(tpath.clientObject(), ctx);
          if (debugOn) log.debug("  call result: " + result);
        }
        else if (debugOn)
          log.debug("lookup result is not a method, using it: " + result);
      }
      
      /* now render the result */
      
      if (r == null) {
        /* setup fragment rendering mode */
        if (_rq.isFragmentIDInRequest()) {
          /* for fragments, we initially disable rendering of elements */
          ctx.disableRendering();
        }
        
        r = this.renderResult(result, ctx);
        if (debugOn) log.debug("rendered object: " + result + ", as: " + r);
      }
      
      /* save session */
      
      // TODO: store session cookies
      
      if (ctx.hasSession()) {
        // TODO: ensure that session gets a sleep?
        this.saveSessionForContext(ctx);
      }
      else
        log.debug("no session to store ...");
    }
    catch (Exception e) {
      // TODO: call some handler method
      // TODO: ensure that session gets a sleep?
      if (debugOn) log.debug("  handler catched exception", e);
      r = this.handleException(e, ctx);
    }
    
    /* deal with missing responses */
    
    if (r == null) {
      log.warn("request handler produced no result.");
      r = ctx.response();
    }
    if (r == null)
      r = new WOResponse(_rq);
    
    /* tear down context */
    // TODO: send sleep() or something to tear it down?
    // TODO: possibly sleep the context?
    ctx = null;
    
    return r;
  }
  
  public boolean useHandlerRequestDispatch() {
    return false;
  }
  
  public WOResponse dispatchRequest(WORequest _rq) {
    WOResponse r = null;
    int rqId = requestCounter.incrementAndGet();
    
    if (profile.isInfoEnabled())
      this.logRequestStart(_rq, rqId);
    
    if (this.useHandlerRequestDispatch()) {
      WORequestHandler rh = this.requestHandlerForRequest(_rq);
      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 catched exception", e);
          r = null;
        }
      }
    }
    else {
      try {
        r = this.handleRequest(_rq);
      }
      catch (Exception e) {
        log.error("WOApplication dispatcher catched exception", e);
        r = null;
      }
    }
    
    if (profile.isInfoEnabled())
      this.logRequestEnd(_rq, rqId, r);
    return r;
  }
  
  /* rendering results */
  
  public WOResponse renderResult(Object _result, WOContext _ctx) {
    if (_result == null) {
      WOResponse r = _ctx.response();
      r.setStatus(404 /* Not Found */);
      r.appendContentHTMLString("did not find requested path");
      return r;
    }
    
    // TODO: lookup a renderer by traversing the path
    
    if (_result instanceof WOComponent)
      return this.renderComponent((WOComponent)_result, _ctx);
    
    if (_result instanceof WOResponse)
      return (WOResponse)_result;
    
    if (_result instanceof WOActionResults)
      return ((WOActionResults)_result).generateResponse();
    
    if (_result instanceof WOElement)
      return this.renderElement((WOElement)_result, _ctx);

    if (_result instanceof WOApplication) {
      /* This is if someone enters the root URL, per default we either redirect
       * to the DirectAction or to the Main page.
       */
      return this.redirectToApplicationEntry(_ctx);
    }
    
    // TODO: render exceptions?
    
    log.error("cannot render result: " + _result);
    return null;
  }
  
  public WOResponse redirectToApplicationEntry(WOContext _ctx) {
    String url;
    
    /* Note: in both cases we use the DA request handler for entry */
    url = this.defaultRequestHandler() instanceof WODirectActionRequestHandler
      ? "DirectAction/default"
      : "Main/default";
    
    Map<String, Object>   qd = new HashMap<String, Object>(1);
    Map<String, Object[]> fv = _ctx.request().formValues();
    if (fv != null) qd.putAll(fv);
    if (_ctx.hasSession())
      qd.put(WORequest.SessionIDKey, _ctx.session().sessionID());
    url = _ctx.directActionURLForActionNamed(url, qd);
    
    // TODO: some devices, eg mobile ones, might have issues here
    WOResponse r = _ctx.response();
    r.setStatus(302 /* Redirect */);
    r.setHeaderForKey(url, "location");
    return r;
  }

  public WOResponse renderComponent(WOComponent _page, WOContext _ctx) {
    /* reuse context response for WOComponent */
    WOResponse r = _ctx.response();
    if (_page == null) return null;
    log.debug("delivering page: " + _page);
    
    _ctx.setPage(_page);
    _page.ensureAwakeInContext(_ctx);
    _ctx.enterComponent(_page, null /* component-content */);
    _page.appendToResponse(r, _ctx);
    _ctx.leaveComponent(_page);
    
    return r;
  }
  
  public WOResponse renderElement(WOElement _e, WOContext _ctx) {
    WOResponse r = _ctx.response();
    _e.appendToResponse(r, _ctx);
    return r;
  }
  
  /* request logging */
  
  protected void logRequestStart(WORequest _rq, int rqId) {
    StringBuffer sb = new StringBuffer(256);
    sb.append("WOApp: [" + rqId + "] start dispatch: ");
    if (_rq != null) {
      sb.append(_rq.method());
      sb.append(" ");
      sb.append(_rq.uri());

      String[] qks = _rq.formValueKeys();
      if (qks != null && qks.length > 0) {
        sb.append(" F[");
        for (String qk: qks) {
          sb.append(" ");
          sb.append(qk);
          String v = _rq.stringFormValueForKey(qk);
          if (v != null && v.length() > 0) {
            if (v.length() > 16) v = v.substring(0, 14) + "..";
            sb.append("=");
            sb.append(v);
          }
        }
        sb.append(" ]");
      }
    }
    else
      sb.append("no request");
    
    profile.info(sb.toString());
  }
  protected void logRequestEnd(WORequest _rq, int rqId, WOResponse r) {
    StringBuffer sb = new StringBuffer(128);
    sb.append("WOApp: [");
    sb.append(rqId);
    sb.append("] finished dispatch: ");
    if (r != null){
      sb.append(r.status());
      if (r.headerForKey("content-length") != null)
        sb.append(" " + r.headerForKey("content-length"));
      
      // System.err.println("r: " + r.contentString());
    }
    else
      sb.append("no response");
    profile.info(sb.toString());
  }
  
  /* request handler */
  
  public WORequestHandler requestHandlerForRequest(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();
  }
  public void registerRequestHandler(WORequestHandler _rh, String _key) {
    this.requestHandlerRegistry.put(_key, _rh);
  }
  
  public String[] registeredRequestHandlerKeys() {
    return (String[])(this.requestHandlerRegistry.keySet().toArray());
  }
  
  public void setDefaultRequestHandler(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");
  }
  
  public WOContext createContextForRequest(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 */
  
  public WOComponent pageWithName(String _pageName, WOContext _ctx) {
    // TODO: implement
    pageLog.debug("pageWithName:" + _pageName);
    
    WOResourceManager rm = null;
    WOComponent cursor = _ctx.component();
    if (cursor != null)
      rm = cursor.resourceManager();
    if (rm == null)
      rm = this.resourceManager();
    
    if (rm == null) {
      pageLog.error("did not find a resource manager!");
      return null;
    }
    
    WOComponent page = rm.pageWithName(_pageName, _ctx);
    if (page == null) {
      pageLog.error("could not create page: " + _pageName);
      return null;      
    }
    page.ensureAwakeInContext(_ctx);
    return page;
  }
  
  /* sessions */
  
  public void setSessionStore(WOSessionStore _wss) {
    // NOTE: only call in threadsafe sections!
    this.sessionStore = _wss;
  }
  public WOSessionStore sessionStore() {
    return this.sessionStore;
  }
  
  public WOSession restoreSessionWithID(String _sid, WOContext _ctx) {
    if (_sid == null) {
      log.info("attempt to restore session w/o session-id");
      return null;
    }
    
    WOSessionStore st = this.sessionStore();
    if (st == null) {
      log.info("can't restore session, no store is available: " + _sid);
      return null;
    }
    
    if (log.isDebugEnabled())
      log.debug("checkout session: " + _sid);
    
    WOSession sn = st.checkOutSessionForID(_sid, _ctx.request());
    if (sn != null) {
      _ctx.setSession(sn);
      sn._awakeWithContext(_ctx);
    }
    return sn;
  }
  
  public boolean saveSessionForContext(WOContext _ctx) {
    if (_ctx == null)
      return false;
    
    if (!_ctx.hasSession())
      return false;
    
    WOSessionStore st = this.sessionStore();
    if (st == null) {
      log.error("cannot save session, missing a session store!");
      return false;
    }
    
    log.info("checking in session ...");
    st.checkInSessionForContext(_ctx);
    return true;
  }
  
  public boolean refusesNewSessions() {
    return false;
  }
  
  public WOSession initializeSession(WOContext _ctx) {
    if (_ctx == null) {
      log.info("got no context in initializeSession!");
      return null;
    }
    
    WOSession sn = this.createSessionForRequest(_ctx.request());
    if (sn == null) {
      log.debug("createSessionForRequest returned null ...");
      return null;
    }
    
    _ctx.setNewSession(sn);
    sn._awakeWithContext(_ctx);
    // TODO: post WOSessionDidCreateNotification
    return sn;
  }
  
  public WOSession createSessionForRequest(WORequest _rq) {
    if (this.sessionClass == null)
      return new WOSession();
    
    return (WOSession)NSJavaRuntime.NSAllocateObject(this.sessionClass);
  }
  
  public boolean isPageRefreshOnBacktrackEnabled() {
    return true;
  }
  
  /* resource manager */
  
  // TODO: consider threading issues
  protected WOResourceManager resourceManager; /* Note: set by linker */
  
  public void setResourceManager(WOResourceManager _rm) {
    this.resourceManager = _rm;
  }
  public WOResourceManager resourceManager() {
    return this.resourceManager;
  }
  
  /* error handling */
  
  public WOResponse handleException(Throwable _e, WOContext _ctx) {
    // TODO: improve exception page, eg include stacktrace
    _ctx.response().appendContentHTMLString("fail: " + _e.toString());
    _e.printStackTrace();
    return _ctx.response();
  }
  
  public WOResponse handleSessionRestorationError(WOContext _ctx) {
    // TODO: improve exception page
    _ctx.response().appendContentHTMLString("sn fail: " + _ctx.toString());
    return _ctx.response();
  }
  
  public WOResponse 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 */
  
  public void takeValuesFromRequest(WORequest _rq, WOContext _ctx) {
    if (_ctx.hasSession())
      _ctx.session().takeValuesFromRequest(_rq, _ctx);
    else {
      WOComponent page = _ctx.page();
      
      if (page != null) {
        _ctx.enterComponent(page, null /* component content */);
        page.takeValuesFromRequest(_rq, _ctx);
        _ctx.leaveComponent(page);
      }
    }
  }

  public Object invokeAction(WORequest _rq, WOContext _ctx) {
    Object result;
    
    if (_ctx.hasSession())
      result = _ctx.session().invokeAction(_rq, _ctx);
    else {
      WOComponent page = _ctx.page();
      
      if (page != null) {
        _ctx.enterComponent(page, null /* component content */);
        result = page.invokeAction(_rq, _ctx);
        _ctx.leaveComponent(page);
      }
      else
        result = null;
    }
    return result;
  }

  public void appendToResponse(WOResponse _r, WOContext _ctx) {
    if (_ctx.hasSession())
      _ctx.session().appendToResponse(_r, _ctx);
    else {
      WOComponent page = _ctx.page();
      
      if (page != null) {
        _ctx.enterComponent(page, null /* component content */);
        page.appendToResponse(_r, _ctx);
        _ctx.leaveComponent(page);
      }
    }
  }
  
  /* properties */
  
  protected void loadProperties() {
    InputStream in;
    
    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");
    
    /* Finally 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);
      }
    }
  }
  
  protected File userDomainPropertiesFile() {
    String fn = this.getClass().getSimpleName() + ".properties";
    return new File(System.getProperty("user.dir", "."), fn); 
  }
  
  protected boolean loadProperties(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(Object _value, String _key) {
      // do nothing, we do not mutate properties
    }
    public Object valueForKey(String _key) {
      return WOApplication.this.properties.getProperty(_key);
    }
  };
  public NSObject defaults() {
    return this.defaults;
  }
  
  public boolean isCachingEnabled() {
    return NSJavaRuntime
      .boolValueForObject(this.properties.get("WOCachingEnabled"));
  }
  
  /* KVC */
  
  public void takeValueForKey(Object _value, String _key) {
    NSKeyValueCoding.DefaultImplementation.takeValueForKey(this, _value, _key);
  }
  public Object valueForKey(String _key) {
    return NSKeyValueCoding.DefaultImplementation.valueForKey(this, _key);
  }
  
  public void takeValueForKeyPath(Object _value, String _keyPath) {
    NSKeyValueCodingAdditions.DefaultImplementation.
      takeValueForKeyPath(this, _value, _keyPath);
  }
  public Object valueForKeyPath(String _keyPath) {
    return NSKeyValueCodingAdditions.DefaultImplementation.
             valueForKeyPath(this, _keyPath);    
  }
  
  /* JoObject */
  
  public Object lookupName(String _name, JoContext _ctx, boolean _aquire) {
    if (_name == null)
      return null;
    
    /* a few hardcoded object pathes */
    // TODO: why hardcode? move it to a JoClass!
    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());
    }
    
    /* request handlers */
    return this.requestHandlerRegistry.get(_name);
  }
  
  public JoSecurityManager joSecurityManager() {
    return this.joSecurityManager;
  }
  public JoClassRegistry joClassRegistry() {
    return this.joClassRegistry;
  }

  /* description */
  
  public void appendAttributesToDescription(StringBuffer d) {
    // TODO: add some info
  }
}
