/*
 * Helma License Notice
 *
 * The contents of this file are subject to the Helma License
 * Version 2.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://adele.helma.org/download/helma/license.txt
 *
 * Copyright 1998-2003 Helma Software. All Rights Reserved.
 *
 * $RCSfile$
 * $Author$
 * $Revision$
 * $Date$
 */

package helma.framework.core;

import helma.extensions.ConfigurationException;
import helma.extensions.HelmaExtension;
import helma.framework.*;
import helma.framework.repository.*;
import helma.main.Server;
import helma.objectmodel.*;
import helma.objectmodel.db.*;
import helma.util.*;
import helma.scripting.ScriptingEngine;
import helma.scripting.ScriptingException;

import java.io.*;
import java.lang.reflect.*;
import java.rmi.*;
import java.util.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.ArrayList;


/**
 * The central class of a Helma application. This class keeps a pool of
 * request evaluators (threads with JavaScript interpreters), waits for
 * requests from the Web server or XML-RPC port and dispatches them to
 * the evaluators.
 */
public final class Application implements Runnable {
    // the name of this application
    private String name;

    // application sources
    ArrayList repositories;

    // properties and db-properties
    ResourceProperties props;

    // properties and db-properties
    ResourceProperties dbProps;

    // This application's main directory
    File appDir;

    // Helma server hopHome directory
    File hopHome;

    // embedded db directory
    File dbDir;

    // false if hopobjects are case sensitive (default)
    public boolean caseInsensitive;

    // this application's node manager
    protected NodeManager nmgr;

    // the root of the website, if a custom root object is defined.
    // otherwise this is managed by the NodeManager and not cached here.
    Object rootObject = null;
    String rootObjectClass;
    // if defined this will cause us to get the root object straight
    // from the scripting engine, circumventing all hopobject db fluff
    String rootObjectPropertyName;
    String rootObjectFunctionName;

    // The session manager
    SessionManager sessionMgr;

     /**
     *  The type manager checks if anything in the application's prototype definitions
     * has been updated prior to each evaluation.
     */
    public TypeManager typemgr;

    /**
     * The skin manager for this application
     */
    protected SkinManager skinmgr;

    /**
     * Collections for evaluator thread pooling
     */
    protected Stack freeThreads;
    protected Vector allThreads;
    boolean running = false;
    boolean debug;
    long starttime;
    Hashtable dbSources;

    // map of app modules reflected at app.modules
    Map modules;

    // internal worker thread for scheduler, session cleanup etc.
    Thread worker;
    // request timeout defaults to 60 seconds
    long requestTimeout = 60000;
    ThreadGroup threadgroup;

    // threadlocal variable for the current RequestEvaluator
    ThreadLocal currentEvaluator = new ThreadLocal();

    // Map of requesttrans -> active requestevaluators
    Hashtable activeRequests;

    String logDir;

    // Two logs for each application
    Log eventLog;
    Log accessLog;

    // Symbolic names for each log
    String eventLogName;
    String accessLogName;

    // A transient node that is shared among all evaluators
    protected INode cachenode;

    // some fields for statistics
    protected volatile long requestCount = 0;
    protected volatile long xmlrpcCount = 0;
    protected volatile long errorCount = 0;

    // the URL-prefix to use for links into this application
    private String baseURI;
    // the name of the root prototype as far as href() is concerned
    private String hrefRootPrototype;

    // the id of the object to use as root object
    String rootId = "0";

    // Db mappings for some standard prototypes
    private DbMapping rootMapping;
    private DbMapping userRootMapping;
    private DbMapping userMapping;

    // name of response encoding
    String charset;

    // password file to use for authenticate() function
    private CryptResource pwfile;

    // Map of java class names to object prototypes
    ResourceProperties classMapping;

    // Map of extensions allowed for public skins
    Properties skinExtensions;

    // time we last read the properties file
    private long lastPropertyRead = -1L;

    // the set of prototype/function pairs which are allowed to be called via XML-RPC
    private HashSet xmlrpcAccess;

    // the name under which this app serves XML-RPC requests. Defaults to the app name
    private String xmlrpcHandlerName;

    // the list of currently active cron jobs
    Hashtable activeCronJobs = null;
    // the list of custom cron jobs
    Hashtable customCronJobs = null;

    private ResourceComparator resourceComparator;
    private Resource currentCodeResource;

    // Field to cache unmapped java classes
    private final static String CLASS_NOT_MAPPED = "(unmapped)";

    /**
     * Namespace search path for global macros
     */
    String[] globalMacroPath = null;

    /**
     *  Simple constructor for dead application instances.
     */
    public Application(String name) {
        this.name = name;
    }

    /**
     * Build an application with the given name with the given sources. No
     * Server-wide properties are created or used.
     */
    public Application(String name, Repository[] repositories, File dbDir)
                throws RemoteException, IllegalArgumentException {
        this(name, null, repositories, null, dbDir);
    }

    /**
     * Build an application with the given name and server instance. The
     * app directories will be created if they don't exist already.
     */
    public Application(String name, Server server)
                throws RemoteException, IllegalArgumentException {
        this(name, server, new Repository[0], null, null);
    }

    /**
     * Build an application with the given name, server instance, sources and
     * db directory.
     */
    public Application(String name, Server server, Repository[] repositories,
                       File customAppDir, File customDbDir)
                throws RemoteException, IllegalArgumentException {
        if ((name == null) || (name.trim().length() == 0)) {
            throw new IllegalArgumentException("Invalid application name: " + name);
        }

        if (repositories.length == 0) {
            throw new java.lang.IllegalArgumentException("No sources defined for application: " + name);
        }

        this.name = name;

        this.caseInsensitive = "true".equalsIgnoreCase(server.getAppsProperties(name).getProperty("caseInsensitive"));

        this.repositories = new ArrayList();
        this.repositories.addAll(Arrays.asList(repositories));
        resourceComparator = new ResourceComparator(this);

        appDir = customAppDir;
        dbDir = customDbDir;

        // system-wide properties, default to null
        ResourceProperties sysProps;

        // system-wide properties, default to null
        ResourceProperties sysDbProps;

        sysProps = sysDbProps = null;
        hopHome = null;

        if (server != null) {
            hopHome = server.getHopHome();

            if (dbDir == null) {
                dbDir = new File(server.getDbHome(), name);
            }

            // get system-wide properties
            sysProps = server.getProperties();
            sysDbProps = server.getDbProperties();
        }

        if (!dbDir.exists()) {
            dbDir.mkdirs();
        }

        if (appDir == null) {
            for (int i=repositories.length-1; i>=0; i--) {
                if (repositories[i] instanceof FileRepository) {
                    appDir = new File(repositories[i].getName());
                    break;
                }
            }
        }

        // give the Helma Thread group a name so the threads can be recognized
        threadgroup = new ThreadGroup("TX-" + name);

        // create app-level properties
        props = new ResourceProperties(this, "app.properties", sysProps);

        // get log names
        accessLogName = props.getProperty("accessLog",
                new StringBuffer("helma.").append(name).append(".access").toString());
        eventLogName = props.getProperty("eventLog",
                new StringBuffer("helma.").append(name).append(".event").toString());

        // create app-level db sources
        dbProps = new ResourceProperties(this, "db.properties", sysDbProps, false);

        // the passwd file, to be used with the authenticate() function
        CryptResource parentpwfile = null;

        if (hopHome != null) {
            parentpwfile = new CryptResource(new FileResource(new File(hopHome, "passwd")), null);
        }

        pwfile = new CryptResource(repositories[0].getResource("passwd"), parentpwfile);

        // the properties that map java class names to prototype names
        classMapping = new ResourceProperties(this, "class.properties");
        classMapping.setIgnoreCase(false);

        // get class name of root object if defined. Otherwise native Helma objectmodel will be used.
        rootObjectClass = classMapping.getProperty("root");

        updateProperties();

        dbSources = new Hashtable();
        modules = new SystemMap();
    }

    /**
     * Get the application ready to run, initializing the evaluators and type manager.
     */
    public void init()
            throws DatabaseException, IllegalAccessException, InstantiationException,
                   ClassNotFoundException, InterruptedException {
        init(null);
    }

    /**
     * Get the application ready to run, initializing the evaluators and type manager.
     *
     * @param ignoreDirs comma separated list of directory names to ignore
     */
    public void init(final String ignoreDirs)
            throws DatabaseException, IllegalAccessException, InstantiationException,
                   ClassNotFoundException, InterruptedException {

        Initializer i = new Initializer(ignoreDirs);
        i.start();
        i.join();
        if (i.exception != null) {
            if (i.exception instanceof DatabaseException)
                throw (DatabaseException) i.exception;
            if (i.exception instanceof IllegalAccessException)
                throw (IllegalAccessException) i.exception;
            if (i.exception instanceof InstantiationException)
                throw (InstantiationException) i.exception;
            if (i.exception instanceof ClassNotFoundException)
                throw (ClassNotFoundException) i.exception;
            throw new RuntimeException(i.exception);
        }
    }

    // We need to call initialize in a fresh thread because the calling thread could
    // already be associated with a rhino context, for example when starting from the
    // manage application.
    class Initializer extends Thread {
        Exception exception = null;
        String ignoreDirs;

        Initializer(String dirs) {
            super(name + "-init");
            ignoreDirs = dirs;
        }

        public void run() {
            try {
                synchronized (Application.this) {
                    initInternal();
                }
            } catch (Exception x) {
                exception = x;
            }
        }

        private void initInternal()
                throws DatabaseException, IllegalAccessException,
                       InstantiationException, ClassNotFoundException {
            running = true;
            // create and init type mananger
            typemgr = new TypeManager(Application.this, ignoreDirs);
            // set the context classloader. Note that this must be done before
            // using the logging framework so that a new LogFactory gets created
            // for this app.
            Thread.currentThread().setContextClassLoader(typemgr.getClassLoader());
            try {
                typemgr.createPrototypes();
            } catch (Exception x) {
                logError("Error creating prototypes", x);
            }

            if (Server.getServer() != null) {
                Vector extensions = Server.getServer().getExtensions();

                for (int i = 0; i < extensions.size(); i++) {
                    HelmaExtension ext = (HelmaExtension) extensions.get(i);

                    try {
                        ext.applicationStarted(Application.this);
                    } catch (ConfigurationException e) {
                        logEvent("couldn't init extension " + ext.getName() + ": " +
                                 e.toString());
                    }
                }
            }

            // create and init evaluator/thread lists
            freeThreads = new Stack();
            allThreads = new Vector();

            activeRequests = new Hashtable();
            activeCronJobs = new Hashtable();
            customCronJobs = new Hashtable();

            // create the skin manager
            skinmgr = new SkinManager(Application.this);

            // read in root id, root prototype, user prototype
            rootId = props.getProperty("rootid", "0");
            String rootPrototype = props.getProperty("rootprototype", "root");
            String userPrototype = props.getProperty("userprototype", "user");

            rootMapping = getDbMapping(rootPrototype);
            if (rootMapping == null)
                throw new RuntimeException("rootPrototype does not exist: " + rootPrototype);
            userMapping = getDbMapping(userPrototype);
            if (userMapping == null)
                throw new RuntimeException("userPrototype does not exist: " + userPrototype);

            // The whole user/userroot handling is basically old
            // ugly obsolete crap. Don't bother.
            ResourceProperties p = new ResourceProperties();
            String usernameField = (userMapping != null) ? userMapping.getNameField() : null;

            if (usernameField == null) {
                usernameField = "name";
            }

            p.put("_children", "collection(" + userPrototype + ")");
            p.put("_children.accessname", usernameField);
            userRootMapping = new DbMapping(Application.this, "__userroot__", p);
            userRootMapping.update();

            // create the node manager
            nmgr = new NodeManager(Application.this);
            nmgr.init(dbDir.getAbsoluteFile(), props);

            // create the app cache node exposed as app.data
            cachenode = new TransientNode(Application.this, "app");

            // create and init session manager
            String sessionMgrImpl = props.getProperty("sessionManagerImpl",
                                                      "helma.framework.core.SessionManager");
            sessionMgr = (SessionManager) Class.forName(sessionMgrImpl).newInstance();
            logEvent("Using session manager class " + sessionMgrImpl);
            sessionMgr.init(Application.this);

            // read the sessions if wanted
            if ("true".equalsIgnoreCase(getProperty("persistentSessions"))) {
                RequestEvaluator ev = getEvaluator();
                try {
                    ev.initScriptingEngine();
                    sessionMgr.loadSessionData(null, ev.scriptingEngine);
                } finally {
                    releaseEvaluator(ev);
                }
            }

            // preallocate minThreads request evaluators
            int minThreads = 0;

            try {
                minThreads = Integer.parseInt(props.getProperty("minThreads"));
            } catch (Exception ignore) {
                // not parsable as number, keep 0
            }

            if (minThreads > 0) {
                logEvent("Starting "+minThreads+" evaluator(s) for " + name);
            }

            for (int i = 0; i < minThreads; i++) {
                RequestEvaluator ev = new RequestEvaluator(Application.this);

                if (i == 0) {
                    ev.initScriptingEngine();
                }
                freeThreads.push(ev);
                allThreads.addElement(ev);
            }
        }
    }


    /**
     *  Create and start scheduler and cleanup thread
     */
    public synchronized void start() {
        starttime = System.currentTimeMillis();

        // as first thing, invoke global onStart() function
        RequestEvaluator eval = null;
        try {
            eval = getEvaluator();
            eval.invokeInternal(null, "onStart", RequestEvaluator.EMPTY_ARGS);
        } catch (Exception xcept) {
            logError("Error in " + name + ".onStart()", xcept);
        } finally {
            releaseEvaluator(eval);
        }

        worker = new Thread(this, name + "-worker");
        worker.setPriority(Thread.NORM_PRIORITY + 1);
        worker.start();
    }

    /**
     * This is called to shut down a running application.
     */
    public synchronized void stop() {
        // invoke global onStop() function
        RequestEvaluator eval = null;
        try {
            eval = getEvaluator();
            eval.invokeInternal(null, "onStop", RequestEvaluator.EMPTY_ARGS);
        } catch (Exception x) {
            logError("Error in " + name + ".onStop()", x);
        }

        // mark app as stopped
        running = false;

        // stop all threads, this app is going down
        if (worker != null) {
            worker.interrupt();
        }

        worker = null;

        // stop evaluators
        if (allThreads != null) {
            for (Enumeration e = allThreads.elements(); e.hasMoreElements();) {
                RequestEvaluator ev = (RequestEvaluator) e.nextElement();
                ev.stopTransactor();
                ev.shutdown();
            }
        }

        // remove evaluators
        allThreads.removeAllElements();
        freeThreads.clear();

        // shut down node manager and embedded db
        try {
            nmgr.shutdown();
        } catch (DatabaseException dbx) {
            System.err.println("Error shutting down embedded db: " + dbx);
        }

        // tell the extensions that we're stopped.
        if (Server.getServer() != null) {
            Vector extensions = Server.getServer().getExtensions();

            for (int i = 0; i < extensions.size(); i++) {
                HelmaExtension ext = (HelmaExtension) extensions.get(i);

                ext.applicationStopped(this);
            }
        }

        // store the sessions if wanted
        if ("true".equalsIgnoreCase(getProperty("persistentSessions"))) {
            // sessionMgr.storeSessionData(null);
            sessionMgr.storeSessionData(null, eval.scriptingEngine);
        }
        sessionMgr.shutdown();
    }

    /**
     * Returns true if this app is currently running
     *
     * @return true if the app is running
     */
    public boolean isRunning() {
        return running;
    }

    /**
     * Get the application directory.
     *
     * @return the application directory, or first file based repository
     */
    public File getAppDir() {
        return appDir;
    }

    /**
     * Get a comparator for comparing Resources according to the order of
     * repositories they're contained in.
     *
     * @return a comparator that sorts resources according to their repositories
     */
    public ResourceComparator getResourceComparator() {
        return resourceComparator;
    }

    /**
     * Returns a free evaluator to handle a request.
     */
    public RequestEvaluator getEvaluator() {
        if (!running) {
            throw new ApplicationStoppedException();
        }

        // first try
        try {
            return (RequestEvaluator) freeThreads.pop();
        } catch (EmptyStackException nothreads) {
            int maxThreads = 50;

            String maxThreadsProp = props.getProperty("maxThreads");
            if (maxThreadsProp != null) {
                try {
                    maxThreads = Integer.parseInt(maxThreadsProp);
                } catch (Exception ignore) {
                    logEvent("Couldn't parse maxThreads property: " + maxThreadsProp);
                }
            }

            synchronized (this) {
                // allocate a new evaluator
                if (allThreads.size() < maxThreads) {
                    logEvent("Starting engine " + (allThreads.size() + 1) +
                             " for " + name);

                    RequestEvaluator ev = new RequestEvaluator(this);

                    allThreads.addElement(ev);

                    return (ev);
                }
            }
        }

        // we can't create a new evaluator, so we wait if one becomes available.
        // give it 3 more tries, waiting 3 seconds each time.
        for (int i = 0; i < 4; i++) {
            try {
                Thread.sleep(3000);

                return (RequestEvaluator) freeThreads.pop();
            } catch (EmptyStackException nothreads) {
                // do nothing
            } catch (InterruptedException inter) {
                throw new RuntimeException("Thread interrupted.");
            }
        }

        // no luck, give up.
        throw new RuntimeException("Maximum Thread count reached.");
    }

    /**
     * Returns an evaluator back to the pool when the work is done.
     */
    public void releaseEvaluator(RequestEvaluator ev) {
        if (ev != null) {
            ev.recycle();
            freeThreads.push(ev);
        }
    }

    /**
     * This can be used to set the maximum number of evaluators which will be allocated.
     * If evaluators are required beyound this number, an error will be thrown.
     */
    public boolean setNumberOfEvaluators(int n) {
        if ((n < 2) || (n > 511)) {
            return false;
        }

        int current = allThreads.size();

        synchronized (allThreads) {
            if (n > current) {
                int toBeCreated = n - current;

                for (int i = 0; i < toBeCreated; i++) {
                    RequestEvaluator ev = new RequestEvaluator(this);

                    freeThreads.push(ev);
                    allThreads.addElement(ev);
                }
            } else if (n < current) {
                int toBeDestroyed = current - n;

                for (int i = 0; i < toBeDestroyed; i++) {
                    try {
                        RequestEvaluator re = (RequestEvaluator) freeThreads.pop();
                        allThreads.removeElement(re);
                        re.stopTransactor();
                    } catch (EmptyStackException empty) {
                        return false;
                    }
                }
            }
        }

        return true;
    }

    /**
     *  Return the number of currently active threads
     */
    public int getActiveThreads() {
        return 0;
    }

    /**
     *  Execute a request coming in from a web client.
     */
    public ResponseTrans execute(RequestTrans req) {
        requestCount += 1;

        // get user for this request's session
        Session session = createSession(req.getSession());
        session.touch();

        ResponseTrans res = null;
        RequestEvaluator ev = null;

        // are we responsible for releasing the evaluator and closing the result?
        boolean primaryRequest = false;

        try {
            // first look if a request with same user/path/data is already being executed.
            // if so, attach the request to its output instead of starting a new evaluation
            // this helps to cleanly solve "doubleclick" kind of users
            ev = (RequestEvaluator) activeRequests.get(req);

            if (ev != null) {
                res = ev.attachHttpRequest(req);
                if (res != null) {
                    // we can only use the existing response object if the response
                    // wasn't written to the HttpServletResponse directly.
                    res.waitForClose();
                    if (res.getContent() == null) {
                        res = null;
                    }
                }
            }

            if (res == null) {
                primaryRequest = true;

                // if attachHttpRequest returns null this means we came too late
                // and the other request was finished in the meantime
                // check if the properties file has been updated
                updateProperties();

                // get evaluator and invoke
                ev = getEvaluator();
                res = ev.invokeHttp(req, session);
            }
        } catch (ApplicationStoppedException stopped) {
            // let the servlet know that this application has gone to heaven
            throw stopped;
        } catch (Exception x) {
            errorCount += 1;
            res = new ResponseTrans(this, req);
            res.reportError(x);
        } finally {
            if (primaryRequest) {
                activeRequests.remove(req);
                releaseEvaluator(ev);

                // response needs to be closed/encoded before sending it back
                try {
                    res.close(charset);
                } catch (UnsupportedEncodingException uee) {
                    logError("Unsupported response encoding", uee);
                }
            }
        }

        return res;
    }

    /**
     *  Called to execute a method via XML-RPC, usally by helma.main.ApplicationManager
     *  which acts as default handler/request dispatcher.
     */
    public Object executeXmlRpc(String method, Vector args)
                         throws Exception {
        xmlrpcCount += 1;

        Object retval = null;
        RequestEvaluator ev = null;

        try {
            // check if the properties file has been updated
            updateProperties();

            // get evaluator and invoke
            ev = getEvaluator();
            retval = ev.invokeXmlRpc(method, args.toArray());
        } finally {
            releaseEvaluator(ev);
        }

        return retval;
    }


    public Object executeExternal(String method, Vector args)
                        throws Exception {
        Object retval = null;
        RequestEvaluator ev = null;
        try {
            // check if the properties file has been updated
            updateProperties();
            // get evaluator and invoke
            ev = getEvaluator();
            retval = ev.invokeExternal(method, args.toArray());
        } finally {
            releaseEvaluator(ev);
        }
        return retval;
    }

    /**
     * Reset the application's object cache, causing all objects to be refetched from
     * the database.
     */
    public void clearCache() {
        nmgr.clearCache();
    }

    /**
     * Returns the number of elements in the NodeManager's cache
     */
    public int getCacheUsage() {
        return nmgr.countCacheEntries();
    }

    /**
     * Returns a map of cache statistics
     */
    public Map getCacheStatistics() {
        return nmgr.getCacheStatistics();
    }

    /**
     *  Set the application's root element to an arbitrary object. After this is called
     *  with a non-null object, the helma node manager will be bypassed. This function
     * can be used to script and publish any Java object structure with Helma.
     */
    public void setDataRoot(Object root) {
        this.rootObject = root;
    }

    /**
     * This method returns the root object of this application's object tree.
     */
    public Object getDataRoot() throws Exception {
        return getDataRoot(null);
    }

    /**
     * This method returns the root object of this application's object tree.
     */
    protected Object getDataRoot(ScriptingEngine engine) throws Exception {
        if (rootObject != null) {
            return rootObject;
        }
        // check if we have a custom root object class
        if (rootObjectClass != null) {
            // create custom root element.
            try {
                if (classMapping.containsKey("root.factory.class") &&
                        classMapping.containsKey("root.factory.method")) {
                    String rootFactory = classMapping.getProperty("root.factory.class");
                    Class c = typemgr.getClassLoader().loadClass(rootFactory);
                    Method m = c.getMethod(
                            classMapping.getProperty("root.factory.method"),
                            (Class[]) null);
                    rootObject = m.invoke(c, (Object[]) null);
                } else {
                    String rootClass = classMapping.getProperty("root");
                    Class c = typemgr.getClassLoader().loadClass(rootClass);
                    rootObject = c.newInstance();
                }
            } catch (Exception e) {
                throw new RuntimeException("Error creating root object: " +
                        e.toString());
            }
            return rootObject;
        } else if (rootObjectPropertyName != null || rootObjectFunctionName != null) {
            // get root object from a global scripting engine property or function
            if (engine == null) {
                RequestEvaluator reval = getEvaluator();
                try {
                    return getDataRootFromEngine(reval.getScriptingEngine());
                } finally {
                    releaseEvaluator(reval);
                }
            } else {
                return getDataRootFromEngine(engine);
            }
        } else {
            // no custom root object is defined - use standard helma objectmodel
            return nmgr.getRootNode();
        }
    }

    private Object getDataRootFromEngine(ScriptingEngine engine)
            throws ScriptingException {
        return rootObjectPropertyName != null ?
                engine.getGlobalProperty(rootObjectPropertyName) :
                engine.invoke(null, rootObjectFunctionName,
                        RequestEvaluator.EMPTY_ARGS,
                        ScriptingEngine.ARGS_WRAP_DEFAULT, true);
    }

    /**
     *  Return the prototype of the object to be used as this application's root object
     */
    public DbMapping getRootMapping() {
        return rootMapping;
    }

    /**
     *  Return the id of the object to be used as this application's root object
     */
    public String getRootId() {
        return rootId;
    }

    /**
     * Returns the Object which contains registered users of this application.
     */
    public INode getUserRoot() {
        INode users = nmgr.safe.getNode("1", userRootMapping);

        users.setDbMapping(userRootMapping);

        return users;
    }

    /**
     * Returns the node manager for this application. The node manager is
     * the gateway to the helma.objectmodel packages, which perform the mapping
     * of objects to relational database tables or the embedded database.
     */
    public NodeManager getNodeManager() {
        return nmgr;
    }

    /**
     * Returns a wrapper containing the node manager for this application. The node manager is
     * the gateway to the helma.objectmodel packages, which perform the mapping of objects to
     * relational database tables or the embedded database.
     */
    public WrappedNodeManager getWrappedNodeManager() {
        return nmgr.safe;
    }

    /**
     * Return the application's session manager
     * @return the SessionManager instance used by this app
     */
    public SessionManager getSessionManager() {
        return sessionMgr;
    }

    /**
     *  Return a transient node that is shared by all evaluators of this application ("app node")
     */
    public INode getCacheNode() {
        return cachenode;
    }

    /**
     * Returns a Node representing a registered user of this application by his or her user name.
     */
    public INode getUserNode(String uid) {
        try {
            INode users = getUserRoot();

            return (INode) users.getChildElement(uid);
        } catch (Exception x) {
            return null;
        }
    }

    /**
     * Return a prototype for a given node. If the node doesn't specify a prototype,
     * return the generic hopobject prototype.
     */
    public Prototype getPrototype(Object obj) {
        String protoname = getPrototypeName(obj);

        if (protoname == null) {
            return typemgr.getPrototype("hopobject");
        }

        Prototype p = typemgr.getPrototype(protoname);

        if (p == null) {
            p = typemgr.getPrototype("hopobject");
        }

        return p;
    }

    /**
     * Return the prototype with the given name, if it exists
     */
    public Prototype getPrototypeByName(String name) {
        return typemgr.getPrototype(name);
    }

    /**
     * Return a collection containing all prototypes defined for this application
     */
    public Collection getPrototypes() {
        return typemgr.getPrototypes();
    }

    /**
     * Programmatically define a new prototype. If a prototype with this name already exists return
     * the existing prototype.
     * @param name the prototype name
     * @param typeProps custom type properties map
     * @return the new prototype
     */
    public Prototype definePrototype(String name, Map typeProps) {
        Prototype proto = typemgr.getPrototype(name);
        if (proto == null) {
            proto = typemgr.createPrototype(name, null, typeProps);
        } else {
            proto.setTypeProperties(typeProps);
        }
        return proto;
    }

    /**
     *  Return a skin for a given object. The skin is found by determining the prototype
     *  to use for the object, then looking up the skin for the prototype.
     */
    public Skin getSkin(String protoname, String skinname, Object[] skinpath) throws IOException {
        Prototype proto = getPrototypeByName(protoname);

        if (proto == null) {
            return null;
        }

        return skinmgr.getSkin(proto, skinname, skinpath);
    }

    /**
     * Return the session currently associated with a given Hop session ID.
     * Create a new session if necessary.
     */
    public Session createSession(String sessionId) {
        return sessionMgr.createSession(sessionId);
    }

    /**
     * Return a list of Helma nodes (HopObjects -  the database object representing the user,
     *  not the session object) representing currently logged in users.
     */
    public List getActiveUsers() {
        return sessionMgr.getActiveUsers();
    }

    /**
     * Return a list of Helma nodes (HopObjects -  the database object representing the user,
     *  not the session object) representing registered users of this application.
     */
    public List getRegisteredUsers() {
        ArrayList list = new ArrayList();
        INode users = getUserRoot();

        // add all child nodes to the list
        for (Enumeration e = users.getSubnodes(); e.hasMoreElements();) {
            list.add(e.nextElement());
        }

        // if none, try to get them from properties (internal db)
        if (list.size() == 0) {
            for (Enumeration e = users.properties(); e.hasMoreElements();) {
                list.add(users.getNode((String) e.nextElement()));
            }
        }

        return list;
    }

    /**
     * Return an array of <code>SessionBean</code> objects currently associated
     * with a given Helma user.
     */
    public List getSessionsForUsername(String username) {
        return sessionMgr.getSessionsForUsername(username);
    }

    /**
     * Return the session currently associated with a given Hop session ID.
     */
    public Session getSession(String sessionId) {
        return sessionMgr.getSession(sessionId);
    }

    /**
     *  Return the whole session map.
     */
    public Map getSessions() {
        return sessionMgr.getSessions();
    }

    /**
     * Returns the number of currenty active sessions.
     */
    public int countSessions() {
        return sessionMgr.countSessions();
    }

    /**
     * Register a user with the given user name and password.
     */
    public INode registerUser(String uname, String password) {
        if (uname == null) {
            return null;
        }

        uname = uname.toLowerCase().trim();

        if ("".equals(uname)) {
            return null;
        }

        INode unode;

        try {
            INode users = getUserRoot();

            // check if a user with this name is already registered
            unode = (INode) users.getChildElement(uname);
            if (unode != null) {
                return null;
            }

            unode = new Node(uname, "user", nmgr.safe);

            String usernameField = (userMapping != null) ? userMapping.getNameField() : null;
            String usernameProp = null;

            if (usernameField != null) {
                usernameProp = userMapping.columnNameToProperty(usernameField);
            }

            if (usernameProp == null) {
                usernameProp = "name";
            }

            unode.setName(uname);
            unode.setString(usernameProp, uname);
            unode.setString("password", password);

            return users.addNode(unode);

        } catch (Exception x) {
            logEvent("Error registering User: " + x);

            return null;
        }
    }

    /**
     * Log in a user given his or her user name and password.
     */
    public boolean loginSession(String uname, String password, Session session) {
        // Check the name/password of a user and log it in to the current session
        if (uname == null) {
            return false;
        }

        uname = uname.toLowerCase().trim();

        if ("".equals(uname)) {
            return false;
        }

        try {
            INode users = getUserRoot();
            Node unode = (Node) users.getChildElement(uname);
            if (unode == null)
                return false;

            String pw = unode.getString("password");

            if ((pw != null) && pw.equals(password)) {
                // let the old user-object forget about this session
                session.logout();
                session.login(unode);

                return true;
            }
        } catch (Exception x) {
            return false;
        }

        return false;
    }

    /**
     * Log out a session from this application.
     */
    public void logoutSession(Session session) {
        session.logout();
    }

    /**
     * In contrast to login, this works outside the Hop user object framework. Instead, the user is
     * authenticated against a passwd file in the application directory. This is to have some sort of
     * authentication available *before* the application is up and running, i.e. for application setup tasks.
     */
    public boolean authenticate(String uname, String password) {
        if ((uname == null) || (password == null)) {
            return false;
        }

        return pwfile.authenticate(uname, password);
    }

    /**
     *  Return the href to the root of this application.
     * @return the root element's URL
     * @throws UnsupportedEncodingException if the application's charset property
     *         is not a valid encoding name
     */
    public String getRootHref() throws UnsupportedEncodingException {
        return getNodeHref(null, null, null);
    }

    /**
     * Return a path to be used in a URL pointing to the given element  and action
     * @param elem the object to get the URL for
     * @param actionName an optional action name
     * @param queryParams optional map of query parameters
     * @return the element's URL
     * @throws UnsupportedEncodingException if the application's charset property
     *         is not a valid encoding name
     */
    public String getNodeHref(Object elem, String actionName, Map queryParams)
            throws UnsupportedEncodingException {
        StringBuffer buffer = new StringBuffer(baseURI);

        composeHref(elem, buffer, 0);

        if (actionName != null) {
            buffer.append(UrlEncoded.encode(actionName, charset));
        }
        if (queryParams != null) {
            appendQueryParams(buffer, queryParams, null, 0);
        }

        return buffer.toString();
    }

    private int appendQueryParams(StringBuffer buffer, Map params,
                                  String prefix, int count)
            throws UnsupportedEncodingException {
        for (Iterator it = params.entrySet().iterator(); it.hasNext();) {
            Map.Entry entry = (Map.Entry) it.next();
            Object value = entry.getValue();
            if (value == null) {
                continue;
            }
            String key = UrlEncoded.encode(entry.getKey().toString(), charset);
            if (prefix != null) key = prefix + '[' + key + ']';
            if (value instanceof Map) {
                count = appendQueryParams(buffer, (Map) value, key, count);
            } else {
                buffer.append(count++ == 0 ? '?' : '&');
                buffer.append(key);
                buffer.append('=');
                buffer.append(UrlEncoded.encode(value.toString(), charset));
            }
        }
        return count;
    }

    private void composeHref(Object elem, StringBuffer b, int pathCount)
            throws UnsupportedEncodingException {
        if ((elem == null) || (pathCount > 50)) {
            return;
        }

        if ((hrefRootPrototype != null) &&
             hrefRootPrototype.equals(getPrototypeName(elem))) {
            return;
        }

        Object parent = getParentElement(elem);

        if (parent == null) {
            return;
        }

        // recurse to parent element
        composeHref(getParentElement(elem), b, ++pathCount);

        // append ourselves
        String ename = getElementName(elem);
        if (ename != null) {
            b.append(UrlEncoded.encode(ename, charset));
            b.append("/");
        }
    }

    /**
     *  Returns the baseURI for Hrefs in this application.
     */
    public String getBaseURI() {
        return baseURI;
    }


    /**
     *  This method sets the base URL of this application which will be prepended to
     *  the actual object path.
     */
    public void setBaseURI(String uri) {
        if (uri == null) {
            this.baseURI = "/";
        } else if (!uri.endsWith("/")) {
            this.baseURI = uri + "/";
        } else {
            this.baseURI = uri;
        }
    }

    /**
     *  Return true if the baseURI property is defined in the application
     *  properties, false otherwise.
     */
    public boolean hasExplicitBaseURI() {
        return props.containsKey("baseuri");
    }

    /**
     * Returns the prototype name that Hrefs in this application should
     * start with.
     */
    public String getHrefRootPrototype() {
        return hrefRootPrototype;
    }

    /**
     * Tell other classes whether they should output logging information for
     * this application.
     */
    public boolean debug() {
        return debug;
    }

    /**
     * Get the current RequestEvaluator, or null if the calling thread
     * is not evaluating a request.
     *
     * @return the RequestEvaluator belonging to the current thread
     */
    public RequestEvaluator getCurrentRequestEvaluator() {
        return (RequestEvaluator) currentEvaluator.get();
    }

    /**
     * Set the current RequestEvaluator for the calling thread.
     * @param eval the RequestEvaluator belonging to the current thread
     */
    protected void setCurrentRequestEvaluator(RequestEvaluator eval) {
        currentEvaluator.set(eval);
    }

    /**
     *  Utility function invoker for the methods below. This *must* be called
     *  by an active RequestEvaluator thread.
     */
    private Object invokeFunction(Object obj, String func, Object[] args) {
        RequestEvaluator reval = getCurrentRequestEvaluator();
        if (reval != null) {
            if (args == null) {
                args = RequestEvaluator.EMPTY_ARGS;
            }
            try {
                return reval.invokeDirectFunction(obj, func, args);
            } catch (Exception x) {
                if (debug) {
                    System.err.println("Error in Application.invokeFunction (" +
                                        func + "): " + x);
                }
            }
        }
        return null;
    }

    /**
     * Returns the correct property name which is either case sensitive or case insensitive
     * @param propName the raw property name
     * @return the correct property name
     */
    public String correctPropertyName(String propName) {
        if (propName == null)
            return null;
        if (caseInsensitive)
            return propName.toLowerCase();
        return propName;
    }

    /**
     *  Return the application's classloader
     */
    public ClassLoader getClassLoader() {
        return typemgr.getClassLoader();
    }

    //////////////////////////////////////////////////////////////////////////////////////////////////////////
    ///   The following methods mimic the IPathElement interface. This allows us
    ///   to script any Java object: If the object implements IPathElement (as does
    ///   the Node class in Helma's internal objectmodel) then the corresponding
    ///   method is called in the object itself. Otherwise, a corresponding script function
    ///   is called on the object.
    //////////////////////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Return the name to be used to get this element from its parent
     */
    public String getElementName(Object obj) {
        if (obj instanceof IPathElement) {
            return ((IPathElement) obj).getElementName();
        }

        Object retval = invokeFunction(obj, "getElementName", RequestEvaluator.EMPTY_ARGS);

        if (retval != null) {
            return retval.toString();
        }

        return null;
    }

    /**
     * Retrieve a child element of this object by name.
     */
    public Object getChildElement(Object obj, String name) {
        if (obj instanceof IPathElement) {
            return ((IPathElement) obj).getChildElement(name);
        }

        Object[] arg = new Object[] { name };
        return invokeFunction(obj, "getChildElement", arg);
    }

    /**
     * Return the parent element of this object.
     */
    public Object getParentElement(Object obj) {
        if (obj instanceof IPathElement) {
            return ((IPathElement) obj).getParentElement();
        }

        return invokeFunction(obj, "getParentElement", RequestEvaluator.EMPTY_ARGS);
    }

    /**
     * Get the name of the prototype to be used for this object. This will
     * determine which scripts, actions and skins can be called on it
     * within the Helma scripting and rendering framework.
     */
    public String getPrototypeName(Object obj) {
        if (obj == null) {
            return "global";
        } else if (obj instanceof IPathElement) {
            // check if e implements the IPathElement interface
            return ((IPathElement) obj).getPrototype();
        }

        // How class name to prototype name lookup works:
        // If an object is not found by its direct class name, a cache entry is added
        // for the class name. For negative result, the string "(unmapped)" is used
        // as cache value.
        //
        // Caching is done directly in classProperties, as ResourceProperties have
        // the nice effect of being purged when the underlying resource is updated,
        // so cache invalidation happens implicitely.

        Class clazz = obj.getClass();
        String className = clazz.getName();
        String protoName = classMapping.getProperty(className);
        // fast path: direct hit, either positive or negative
        if (protoName != null) {
            return protoName == CLASS_NOT_MAPPED ? null : protoName;
        }

        // walk down superclass path. We already checked the actual class,
        // and we know that java.lang.Object does not implement any interfaces,
        // and the code is streamlined a bit to take advantage of this.
        while (clazz != Object.class) {
            // check interfaces
            Class[] classes = clazz.getInterfaces();
            for (int i = 0; i < classes.length; i++) {
                protoName = classMapping.getProperty(classes[i].getName());
                if (protoName != null) {
                    // cache the class name for the object so we run faster next time
                    classMapping.setProperty(className, protoName);
                    return protoName;
                }
            }
            clazz = clazz.getSuperclass();
            protoName = classMapping.getProperty(clazz.getName());
            if (protoName != null) {
                // cache the class name for the object so we run faster next time
                classMapping.setProperty(className, protoName);
                return protoName == CLASS_NOT_MAPPED ? null : protoName;
            }
        }
        // not mapped - cache negative result
        classMapping.setProperty(className, CLASS_NOT_MAPPED);
        return null;
    }

    ////////////////////////////////////////////////////////////////////////

    /**
     * Log an application error
     */
    public void logError(String msg, Throwable error) {
        if (eventLog == null) {
            eventLog = getLogger(eventLogName);
        }
        eventLog.error(msg, error);
    }

    /**
     * Log an application error
     */
    public void logError(String msg) {
        if (eventLog == null) {
            eventLog = getLogger(eventLogName);
        }
        eventLog.error(msg);
    }

    /**
     * Log a generic application event
     */
    public void logEvent(String msg) {
        getEventLog().info(msg);
    }

    /**
     * Log a generic application debug message
     */
    public void logDebug(String msg) {
        getEventLog().debug(msg);
    }

    /**
     * Log an application access
     */
    public void logAccess(String msg) {
        getAccessLog().info(msg);
    }

    /**
     * get the app's event log.
     */
    public Log getEventLog() {
        if (eventLog == null) {
            eventLog = getLogger(eventLogName);
            setEventLogLevel();
        }
        return eventLog;
    }

    /**
     * get the app's access log.
     */
    public Log getAccessLog() {
        if (accessLog == null) {
            accessLog = getLogger(accessLogName);
        }
        return accessLog;
    }

    /**
     *  Get a logger object to log events for this application.
     */
    public Log getLogger(String logname) {
        if ("console".equals(logDir) || "console".equals(logname)) {
            return Logging.getConsoleLog();
        } else {
            return LogFactory.getLog(logname);
        }
    }

    private void setEventLogLevel() {
        // set log level for event log in case it is a helma.util.Logger
        if (eventLog instanceof Logger) {
            if (debug) {
                if (!eventLog.isDebugEnabled()) {
                    ((Logger) eventLog).setLogLevel(Logger.DEBUG);
                }
            } else if (eventLog.isDebugEnabled()) {
                ((Logger) eventLog).setLogLevel(Logger.INFO);
            }
        }
    }

    /**
     * The run method performs periodic tasks like executing the scheduler method and
     * kicking out expired user sessions.
     */
    public void run() {

        // interval between session cleanups
        long lastSessionCleanup = System.currentTimeMillis();

        while (Thread.currentThread() == worker) {

            try {
                // interval between scheduler runs
                long sleepInterval = 60000;

                try {
                    String sleepProp = props.getProperty("schedulerInterval");
                    if (sleepProp != null) {
                        sleepInterval = Math.max(1000, Integer.parseInt(sleepProp) * 1000);
                    } else {
                        sleepInterval = CronJob.millisToNextFullMinute();
                    }
                } catch (Exception ignore) {
                    // we'll use the default interval
                }

                // sleep until the next full minute
                try {
                    Thread.sleep(sleepInterval);
                } catch (InterruptedException x) {
                    worker = null;
                    break;
                }

                // purge sessions
                try {
                    lastSessionCleanup = sessionMgr.cleanupSessions(lastSessionCleanup);
                } catch (Exception x) {
                    logError("Error in session cleanup: " + x, x);
                } catch (LinkageError x) {
                    logError("Error in session cleanup: " + x, x);
                }

                // execute cron jobs
                try {
                    executeCronJobs();
                } catch (Exception x) {
                    logError("Error in cron job execution: " + x, x);
                } catch (LinkageError x) {
                    logError("Error in cron job execution: " + x, x);
                }

            } catch (VirtualMachineError error) {
                logError("Error in scheduler loop: " + error, error);
            }
        }

        // when interrupted, shutdown running cron jobs
        synchronized (activeCronJobs) {
            for (Iterator i = activeCronJobs.values().iterator(); i.hasNext();) {
                ((CronRunner) i.next()).interrupt();
                i.remove();
            }
        }

        logEvent("Scheduler for " + name + " exiting");
    }

    /**
     * Executes cron jobs for the application, which are either
     * defined in app.properties or via app.addCronJob().
     * This method is called by run().
     */
    private void executeCronJobs() {
        // loop-local cron job data
        List jobs = CronJob.parse(props.getSubProperties("cron."));
        Date date = new Date();

        jobs.addAll(customCronJobs.values());
        CronJob.sort(jobs);

        if (debug) {
            logEvent("Running cron jobs: " + jobs);
        }
        if (!activeCronJobs.isEmpty()) {
            logEvent("Cron jobs still running from last minute: " + activeCronJobs);
        }

        for (Iterator i = jobs.iterator(); i.hasNext();) {
            CronJob job = (CronJob) i.next();

            if (job.appliesToDate(date)) {
                // check if the job is already active ...
                if (activeCronJobs.containsKey(job.getName())) {
                    logEvent(job + " is still active, skipped in this minute");

                    continue;
                }

                RequestEvaluator evaluator;

                try {
                    evaluator = getEvaluator();
                } catch (RuntimeException rt) {
                    if (running) {
                        logEvent("couldn't execute " + job +
                                ", maximum thread count reached");

                        continue;
                    } else {
                        break;
                    }
                }

                // if the job has a long timeout or we're already late during this minute
                // the job is run from an extra thread
                if ((job.getTimeout() > 20000) ||
                        (CronJob.millisToNextFullMinute() < 30000)) {
                    CronRunner runner = new CronRunner(evaluator, job);

                    activeCronJobs.put(job.getName(), runner);
                    runner.start();
                } else {
                    try {
                        evaluator.invokeInternal(null, job.getFunction(),
                                RequestEvaluator.EMPTY_ARGS, job.getTimeout());
                    } catch (Exception ex) {
                        logEvent("error running " + job + ": " + ex);
                    } finally {
                        releaseEvaluator(evaluator);
                    }
                }
            }
        }
    }

    /**
     * Check whether a prototype is for scripting a java class, i.e. if there's an entry
     * for it in the class.properties file.
     */
    public boolean isJavaPrototype(String typename) {
        return classMapping.contains(typename);
    }

    /**
     * Return the java class that a given prototype wraps, or null.
     */
    public String getJavaClassForPrototype(String typename) {

        for (Iterator it = classMapping.entrySet().iterator(); it.hasNext();) {
            Map.Entry entry = (Map.Entry) it.next();

            if (typename.equals(entry.getValue())) {
                return (String) entry.getKey();
            }
        }

        return null;
    }


    /**
     * Return a DbSource object for a given name. A DbSource is a relational database defined
     * in a db.properties file.
     */
    public DbSource getDbSource(String name) {
        String dbSrcName = name.toLowerCase();
        DbSource dbs = (DbSource) dbSources.get(dbSrcName);

        if (dbs != null) {
            return dbs;
        }

        try {
            dbs = new DbSource(name, dbProps);
            dbSources.put(dbSrcName, dbs);
        } catch (Exception problem) {
            logEvent("Error creating DbSource " + name +": ");
            logEvent(problem.toString());
        }

        return dbs;
    }

    /**
     * Return the name of this application
     */
    public String getName() {
        return name;
    }

    /**
     * Add a repository to this app's repository list. This is used for
     * ZipRepositories contained in top-level file repositories, for instance.
     *
     * @param rep the repository to add
     * @param current the current/parent repository
     * @return if the repository was not yet contained
     */
    public boolean addRepository(Repository rep, Repository current) {
        if (rep != null && !repositories.contains(rep)) {
            // Add the new repository before its parent/current repository.
            // This establishes the order of compilation between FileRepositories
            // and embedded ZipRepositories, or repositories added
            // via app.addRepository()
            if (current != null) {
                int pos = repositories.indexOf(current);
                if (pos > -1) {
                    repositories.add(pos, rep);
                    return true;
                }
            }
            // no parent or parent not in app's repositories, add at end of list.
            repositories.add(rep);
            return true;
        }
        return false;
    }

    /**
     * Searches for the index of the given repository for this app.
     * The arguement must be a root argument, or -1 will be returned.
     *
     * @param   rep one of this app's root repositories.
     * @return  the index of the first occurrence of the argument in this
     *          list; returns <tt>-1</tt> if the object is not found.
     */
    public int getRepositoryIndex(Repository rep) {
        return repositories.indexOf(rep);
    }

    /**
     * Returns the repositories of this application
     * @return iterator through application repositories
     */
    public List getRepositories() {
        return Collections.unmodifiableList(repositories);
    }

    /**
     * Set the code resource currently being evaluated/compiled. This is used
     * to set the proper parent repository when a new repository is added
     * via app.addRepository().
     *
     * @param resource the resource being currently evaluated/compiled
     */
    public void setCurrentCodeResource(Resource resource) {
        currentCodeResource = resource;
    }

    /**
     * Set the code resource currently being evaluated/compiled. This is used
     * to set the proper parent repository when a new repository is added
     * via app.addRepository().

     * @return the resource being currently evaluated/compiled
     */
    public Resource getCurrentCodeResource() {
        return currentCodeResource;
    }

    /**
     * Return the directory of the Helma server
     */
    public File getServerDir() {
        return hopHome;
    }

    /**
     * Get the DbMapping associated with a prototype name in this application
     */
    public DbMapping getDbMapping(String typename) {
        Prototype proto = typemgr.getPrototype(typename);

        if (proto == null) {
            return null;
        }

        return proto.getDbMapping();
    }

    /**
     * Return the current upload status.
     * @param req the upload RequestTrans
     * @return the current upload status.
     */
    public UploadStatus getUploadStatus(RequestTrans req) {
        String uploadId = (String) req.get("upload_id");
        if (uploadId == null)
            return null;

        String sessionId = req.getSession();
        Session session = getSession(sessionId);
        if (session == null)
            return null;
        return session.createUpload(uploadId);
    }



    private synchronized void updateProperties() {
        // if so property file has been updated, re-read props.
        if (props.lastModified() > lastPropertyRead) {
            // force property update
            props.update();

            // character encoding to be used for responses
            charset = props.getProperty("charset", "UTF-8");

            // debug flag
            debug = "true".equalsIgnoreCase(props.getProperty("debug"));

            // if rhino debugger is enabled use higher (10 min) default request timeout
            String defaultReqTimeout =
                    "true".equalsIgnoreCase(props.getProperty("rhino.debug")) ?
                        "600" : "60";
            String reqTimeout = props.getProperty("requesttimeout", defaultReqTimeout);
            try {
                requestTimeout = Long.parseLong(reqTimeout) * 1000L;
            } catch (Exception ignore) {
                // go with default value
                requestTimeout = 60000L;
            }

            // set base URI
            String base = props.getProperty("baseuri");

            if (base != null) {
                setBaseURI(base);
            } else if (baseURI == null) {
                baseURI = "/";
            }

            hrefRootPrototype = props.getProperty("hrefrootprototype");
            rootObjectPropertyName = props.getProperty("rootobjectpropertyname");
            rootObjectFunctionName = props.getProperty("rootobjectfunctionname");

            // update the XML-RPC access list, containting prototype.method
            // entries of functions that may be called via XML-RPC
            String xmlrpcAccessProp = props.getProperty("xmlrpcaccess");
            HashSet xra = new HashSet();

            if (xmlrpcAccessProp != null) {
                StringTokenizer st = new StringTokenizer(xmlrpcAccessProp, ",; ");

                while (st.hasMoreTokens()) {
                    String token = st.nextToken().trim();

                    xra.add(token.toLowerCase());
                }
            }

            xmlrpcAccess = xra;

            // if node manager exists, update it
            if (nmgr != null) {
                nmgr.updateProperties(props);
            }

            // update extensions
            if (Server.getServer() != null) {
                Vector extensions = Server.getServer().getExtensions();

                for (int i = 0; i < extensions.size(); i++) {
                    HelmaExtension ext = (HelmaExtension) extensions.get(i);

                    try {
                        ext.applicationUpdated(this);
                    } catch (ConfigurationException e) {
                        logEvent("Error updating extension "+ext+": "+e);
                    }
                }
            }

            String loggerFactory = props.getProperty("loggerFactory", "helma.util.Logging");
            if ("helma.util.Logging".equals(loggerFactory)) {
                logDir = props.getProperty("logdir", "log");
                if (System.getProperty("helma.logdir") == null) {
                    // set up helma.logdir system property in case we're using it
                    // FIXME: this sets a global System property, should be per-app
                    File dir = new File(logDir);
                    System.setProperty("helma.logdir", dir.getAbsolutePath());
                }
            } else {
                logDir = null;
            }

            // set log level for event log in case debug flag has changed
            setEventLogLevel();

            // set prop read timestamp
            lastPropertyRead = props.lastModified();
        }
    }

    /**
     *  Get a checksum that mirrors the state of this application in the sense
     *  that if anything in the applciation changes, the checksum hopefully will
     *  change, too.
     */
    public long getChecksum() {
        return starttime +
               typemgr.getLastCodeUpdate() +
               props.getChecksum();
    }

    /**
     * Proxy method to get a property from the applications properties.
     */
    public String getProperty(String propname) {
        return props.getProperty(propname);
    }

    /**
     * Proxy method to get a property from the applications properties.
     */
    public String getProperty(String propname, String defvalue) {
        return props.getProperty(propname, defvalue);
    }

    /**
     * Get the application's app properties
     *
     * @return the properties reflecting the app.properties
     */
    public ResourceProperties getProperties() {
        return props;
    }

    /**
     * Get the application's db properties
     *
     * @return the properties reflecting the db.properties
     */
    public ResourceProperties getDbProperties() {
        return dbProps;
    }


    /**
     * Return the XML-RPC handler name for this app. The contract is to
     * always return the same string, even if it has been changed in the properties file
     * during runtime, so the app gets unregistered correctly.
     */
    public String getXmlRpcHandlerName() {
        if (xmlrpcHandlerName == null) {
            xmlrpcHandlerName = props.getProperty("xmlrpcHandlerName", this.name);
        }

        return xmlrpcHandlerName;
    }

    /**
     * Return a string representation for this app.
     */
    public String toString() {
        return "[Application "+name+"]";
    }

    /**
     *
     */
    public int countThreads() {
        return threadgroup.activeCount();
    }

    /**
     *
     */
    public int countEvaluators() {
        return allThreads.size();
    }

    /**
     *
     */
    public int countFreeEvaluators() {
        return freeThreads.size();
    }

    /**
     *
     */
    public int countActiveEvaluators() {
        return allThreads.size() - freeThreads.size();
    }

    /**
     *
     */
    public int countMaxActiveEvaluators() {
        // return typemgr.countRegisteredRequestEvaluators () -1;
        // not available due to framework refactoring
        return -1;
    }

    /**
     *
     */
    public long getRequestCount() {
        return requestCount;
    }

    /**
     *
     */
    public long getXmlrpcCount() {
        return xmlrpcCount;
    }

    /**
     *
     */
    public long getErrorCount() {
        return errorCount;
    }

    /**
     *
     *
     * @return ...
     */
    public long getStarttime() {
        return starttime;
    }

    /**
     * Return the name of the character encoding used by this application
     *
     * @return the character encoding
     */
    public String getCharset() {
        return charset;
    }

    /**
     * Periodically called to log thread stats for this application
     */
    public void printThreadStats() {
        logEvent("Thread Stats for " + name + ": " + threadgroup.activeCount() +
                 " active");

        Runtime rt = Runtime.getRuntime();
        long free = rt.freeMemory();
        long total = rt.totalMemory();

        logEvent("Free memory: " + (free / 1024) + " kB");
        logEvent("Total memory: " + (total / 1024) + " kB");
    }

    /**
     * Check if a method may be invoked via XML-RPC on a prototype.
     */
    protected void checkXmlRpcAccess(String proto, String method)
                              throws Exception {
        String key = proto + "." + method;

        // XML-RPC access items are case insensitive and stored in lower case
        if (!xmlrpcAccess.contains(key.toLowerCase())) {
            throw new Exception("Method " + key + " is not callable via XML-RPC");
        }
    }

    class CronRunner extends Thread {
        RequestEvaluator thisEvaluator;
        CronJob job;

        public CronRunner(RequestEvaluator thisEvaluator, CronJob job) {
            this.thisEvaluator = thisEvaluator;
            this.job = job;
        }

        public void run() {
            try {
                thisEvaluator.invokeInternal(null, job.getFunction(),
                                             RequestEvaluator.EMPTY_ARGS, job.getTimeout());
            } catch (Exception ex) {
                logEvent("error running " + job + ": " + ex);
            } finally {
                releaseEvaluator(thisEvaluator);
                thisEvaluator = null;
                activeCronJobs.remove(job.getName());
            }
        }

        public String toString() {
            return "CronRunner[" + job + "]";
        }
    }
}
