axiom.framework.core.Application.java Source code

Java tutorial

Introduction

Here is the source code for axiom.framework.core.Application.java

Source

/*
 * 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
 * http://adele.helma.org/download/helma/license.txt
 *
 * Copyright 1998-2003 Helma Software. All Rights Reserved.
 *
 * $RCSfile: Application.java,v $
 * $Author: hannes $
 * $Revision: 1.182 $
 * $Date: 2006/05/24 12:29:09 $
 */

/* 
 * Modified by:
 * 
 * Axiom Software Inc., 11480 Commerce Park Drive, Third Floor, Reston, VA 20191 USA
 * email: info@axiomsoftwareinc.com
 */

package axiom.framework.core;

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 org.mortbay.jetty.Handler;
import org.mortbay.jetty.handler.ContextHandler;
import org.mortbay.jetty.handler.HandlerCollection;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.Undefined;
import org.mozilla.javascript.Wrapper;

import axiom.cluster.ClusterCommunicator;
import axiom.extensions.ConfigurationException;
import axiom.extensions.AxiomExtension;
import axiom.framework.*;
import axiom.framework.repository.FileRepository;
import axiom.framework.repository.FileResource;
import axiom.framework.repository.Repository;
import axiom.framework.repository.Resource;
import axiom.framework.repository.ResourceComparator;
import axiom.framework.repository.ZipRepository;
import axiom.main.Server;
import axiom.objectmodel.*;
import axiom.objectmodel.db.*;
import axiom.scripting.rhino.AxiomObject;
import axiom.scripting.rhino.QueryBean;
import axiom.scripting.rhino.RhinoCore;
import axiom.scripting.rhino.RhinoEngine;
import axiom.scripting.rhino.debug.AxiomDebugger;
import axiom.util.*;

import java.util.ArrayList;

/**
 * The central class of a Axiom application. This class keeps a pool of so-called
 * 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 IPathElement, Runnable {

    private boolean omitXmlDecl = true;

    // the name of this application
    private String name;

    // application sources
    ArrayList<Repository> repositories;

    // properties and db-properties
    ResourceProperties props;

    // properties and db-prkoperties
    ResourceProperties dbProps;

    // search profiles
    ResourceProperties searchProps;

    // rewrite rules
    String[][] rewriteRules;

    // This application's main directory
    File appDir;

    // Axiom server axiomHome directory
    File axiomHome;

    // embedded db directory
    File dbDir;

    // file/image storage directory
    String blobDir;

    String cookieDomain;

    // check for resource updates on a periodic basis?
    boolean autoUpdate = true;

    // 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;

    // 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;

    /**
     * Collections for evaluator thread pooling
     */
    protected Stack<RequestEvaluator> freeThreads;
    protected Vector<RequestEvaluator> allThreads;
    boolean running = false;
    boolean debug;
    long starttime;
    Hashtable<String, DbSource> dbSources;

    Set<String> onstartFunctions;

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

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

    // Map of requesttrans -> active requestevaluators
    Hashtable<RequestTrans, RequestEvaluator> activeRequests;

    String logDir;

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

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

    // 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";

    // Added the id for the sessions object that stores all sessions and their
    // data, if persistent sessions are used
    String sessionsId = "2";

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

    // name of response encoding
    String charset;

    // 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;

    // time we last read the module properties
    private long lastModuleRead = -1L;

    // the list of currently active cron jobs
    Hashtable<String, CronRunner> activeCronJobs = null;
    // the list of custom cron jobs
    Hashtable<String, CronJob> customCronJobs = null;

    private ResourceComparator resourceComparator;

    // for static mountpoint for file/image api
    private String staticMountpoint;

    // for specifying the transaction manager
    protected TransSource tsource;

    // the object that manages the path indexing for this application
    protected PathIndexer pathIndexer;

    // the interface to query objects in this application
    protected QueryBean qbean;

    protected ExecutionCache executionCache;
    protected ExecutionCache talCache;

    // Axiom Cluster communication interface if this app is part of a clustered environment
    // will be null if there is no cluster
    private ClusterCommunicator clusterComm = null;
    private String clusterHost = null;

    private HashMap<String, Integer> draftHosts = new HashMap<String, Integer>();
    private int highestPreviewLayer = 1;

    private ArrayList<String> extDbTypes = new ArrayList<String>();

    private Server server = null;

    // Stores the context paths of the application
    private ArrayList<String> contextPaths = new ArrayList<String>();

    /**
     *  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, Exception {
        this(name, null, repositories, null);
    }

    /**
     * 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, Exception {
        this(name, server, 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)
            throws RemoteException, IllegalArgumentException, Exception {
        if ((name == null) || (name.trim().length() == 0)) {
            throw new IllegalArgumentException("Invalid application name: " + name);
        }

        this.name = name;

        this.server = server;

        appDir = customAppDir;

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

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

        sysProps = sysDbProps = null;
        axiomHome = null;

        if (server != null) {
            axiomHome = server.getAxiomHome();

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

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

        this.repositories = new ArrayList<Repository>();
        try {
            // assume that the appdir is, in fact, a directory...
            Repository newRepository = new FileRepository(appDir);
            this.repositories.add(newRepository);
        } catch (Exception ex) {
            System.out.println("Adding application directory " + appDir + " failed. "
                    + "Will not use that repository. Check your initArgs!");
        }

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

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

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

        if (appDir == null) {
            if (repositories[0] instanceof FileRepository) {
                appDir = new File(repositories[0].getName());
                SampleApp sa = new SampleApp();
                sa.setupSampleApp(appDir);
            }
        }

        String dbdir = props.getProperty("dbdir");
        if (dbdir != null) {
            dbDir = new File(dbdir);
            if (!dbDir.isAbsolute()) {
                dbDir = new File(server.getAxiomHome(), dbdir);
            }
        } else {
            dbDir = new File(server.getDbHome(), name);
        }
        if (!dbDir.exists()) {
            dbDir.mkdirs();
        }

        updateDbLocation(name);

        this.cookieDomain = props.getProperty("cookieDomain", "");
        this.staticMountpoint = props.getProperty("staticMountpoint",
                props.getProperty("mountpoint", "/" + this.name) + "/static");

        this.rewriteRules = setupRewriteRules();

        // get log names
        accessLogName = props.getProperty("accessLog",
                new StringBuffer("axiom.").append(name).append(".access").toString());
        eventLogName = props.getProperty("eventLog", new StringBuffer("axiom.").append(name).toString());
        errorLogName = props.getProperty("errorLog");
        requestLogName = props.getProperty("requestLog",
                new StringBuffer("axiom.").append(name).append(".request.log").toString());
        if (!requestLogName.endsWith(".log")) {
            requestLogName += ".log";
        }

        // insert xml declarations into rendered tal?
        omitXmlDecl = props.containsKey("omitxmldeclaration")
                ? (new Boolean((String) props.get("omitXmlDeclaration"))).booleanValue()
                : true;

        ResourceProperties dhprops = props.getSubProperties("draftHost.");
        int count = 1;
        final int dhpropsSize = dhprops.size();
        for (; count <= dhpropsSize; count++) {
            String dhvalue = dhprops.getProperty("" + count);
            if (dhvalue != null) {
                String[] dhvalues = dhvalue.split(",");
                for (int j = 0; j < dhvalues.length; j++) {
                    this.draftHosts.put(dhvalues[j].trim(), new Integer(count));
                }
            }
        }
        this.highestPreviewLayer = dhpropsSize == 0 ? 0 : count;

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

        setupDefaultDb(dbProps);

        searchProps = new ResourceProperties(this, "search.properties", null, false);

        // reads in and creates a transaction manager properties file for this app 
        try {
            this.tsource = new TransSource(this, dbProps.getSubProperties("_default."));
        } catch (Exception ex) {
            throw new IllegalArgumentException(
                    "Could not create the transaction database source: " + ex.getMessage());
        }

        // 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 Axiom objectmodel will be used.
        rootObjectClass = classMapping.getProperty("root");

        onstartFunctions = new LinkedHashSet<String>();
        updateProperties();

        dbSources = new Hashtable<String, DbSource>();

        cachenode = new TransientNode("app");

        ArrayList<String> names = this.getDbNames();
        for (int i = 0; i < names.size(); i++) {
            String dbname = names.get(i).toString();
            DbSource dbsource = this.getDbSource(dbname);
            String initClass = dbsource.getProperty("initClass", null);
            if (initClass != null) {
                Class[] parameters = { Application.class };
                IDBSourceInitializer dbsi = (IDBSourceInitializer) Class.forName(initClass)
                        .getConstructor(parameters).newInstance(new Object[] { this });
                dbsi.init();
            }
        }
    }

    private Repository[] initRepositories() {
        Repository[] repositories;
        ResourceProperties conf = this.props;
        // parse main application directory   
        ArrayList<Repository> repositoryList = new ArrayList<Repository>();

        // read and configure additional app repositories
        Class[] parameters = { String.class };
        String modulesStr = conf.getProperty("modules");
        String[] modules;
        if (modulesStr != null) {
            modules = modulesStr.split(",");
        } else {
            modules = new String[0];
        }

        String pathPrefix = this.axiomHome.getPath();
        if (!pathPrefix.endsWith(File.separator)) {
            pathPrefix += File.separator;
        }
        pathPrefix += "modules" + File.separator;
        for (int i = 0; i < modules.length; i++) {
            String repositoryArgs = pathPrefix + modules[i].trim();

            String repositoryImpl;
            // implementation not set manually, have to guess it
            if (repositoryArgs.endsWith(".zip")) {
                repositoryImpl = "axiom.framework.repository.ZipRepository";
            } else if (repositoryArgs.endsWith(".js")) {
                repositoryImpl = "axiom.framework.repository.SingleFileRepository";
            } else {
                repositoryImpl = "axiom.framework.repository.FileRepository";
            }

            try {
                Repository newRepository = (Repository) Class.forName(repositoryImpl).getConstructor(parameters)
                        .newInstance(new Object[] { repositoryArgs });
                repositoryList.add(newRepository);
            } catch (Exception ex) {
                System.out.println("Adding repository " + repositoryArgs + " failed. "
                        + "Will not use that repository. Check your initArgs!");
            }
        }

        // Load any zip files in the lib directory as repositories for the application

        File libDir = new File(Server.getServer().getAxiomHome(), "lib");
        File[] libFiles = libDir.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                String n = name.toLowerCase();

                return n.endsWith(".zip");
            }
        });

        for (int i = 0; i < libFiles.length; i++) {
            try {
                Repository newRepository = new ZipRepository(libFiles[i]);
                repositoryList.add(newRepository);
            } catch (Exception ex) {
                System.out.println("Adding repository " + libFiles[i].getAbsolutePath()
                        + " failed. Will not use that repository. Check your initArgs!");
            }
        }

        repositories = new Repository[repositoryList.size()];
        repositoryList.toArray(repositories);

        return repositories;
    }

    /**
     * Get the application ready to run, initializing the evaluators and type manager.
     */
    public synchronized void init()
            throws DatabaseException, IllegalAccessException, InstantiationException, ClassNotFoundException {
        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 synchronized void init(String ignoreDirs)
            throws DatabaseException, IllegalAccessException, InstantiationException, ClassNotFoundException {

        running = true;

        // create and init type mananger
        typemgr = new TypeManager(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<AxiomExtension> extensions = Server.getServer().getExtensions();

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

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

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

        // preallocate minThreads request evaluators
        int minThreads = 0;

        try {
            minThreads = Integer.parseInt(props.getProperty("minThreads"));
        } catch (Exception ignore) {
            try {
                minThreads = Integer.parseInt(props.getProperty("maxThreads"));
                minThreads /= 4;
            } catch (Exception ignoreagain) {
                minThreads = 0;
            }
        }

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

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

            ev.initScriptingEngine();

            freeThreads.push(ev);
            allThreads.addElement(ev);
        }

        activeRequests = new Hashtable<RequestTrans, RequestEvaluator>();
        activeCronJobs = new Hashtable<String, CronRunner>();
        customCronJobs = new Hashtable<String, CronJob>();

        // 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", "");
        p.put("_children.type", "collection(" + userPrototype + ")");
        p.put("_children.accessname", usernameField);
        p.put("roles", "");
        p.put("roles.type", "String");
        p.put("roles.multivalue", "true");
        userRootMapping = new DbMapping(this, "__userroot__", p);
        userRootMapping.update();

        // add the session mappings for persisting sessions in the object database
        String sessionPrototype = props.getProperty("sessionprototype", "session");
        sessionMapping = getDbMapping(sessionPrototype);
        if (sessionMapping == null)
            throw new RuntimeException("sessionPrototype does not exist: " + sessionPrototype);
        p = new ResourceProperties();
        String nameField = (sessionMapping != null) ? sessionMapping.getNameField() : null;
        if (nameField == null) {
            nameField = "name";
        }
        p.put("_children", "");
        p.put("_children.type", "collection(" + sessionPrototype + ")");
        p.put("_children.accessname", nameField);
        sessionRootMapping = new DbMapping(this, "__sessionroot__", p);
        sessionRootMapping.update();

        // create/setup the path indexer for this application
        try {
            pathIndexer = new PathIndexer(this);
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new RuntimeException("Could not create the path indexer for the application " + this.name);
        }

        String cluster = this.getProperty("cluster", "false");
        if ("true".equalsIgnoreCase(cluster)) {
            try {
                this.clusterComm = new ClusterCommunicator(this);
            } catch (Exception ex) {
                ex.printStackTrace();
                throw new InstantiationException("Could not initiate the jgroups cluster for " + this.name);
            }

            this.clusterHost = this.getProperty("cluster.host");
            if (this.clusterHost == null) {
                throw new InstantiationException("ERROR: cluster.host not specified in app.properties");
            }
        }

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

        this.executionCache = new ExecutionCache(this, "rhino");

        this.talCache = new ExecutionCache(this, "tal");

        // create and init session manager
        String sessionMgrImpl = props.getProperty("sessionManagerImpl", "axiom.framework.core.SessionManager");
        sessionMgr = (SessionManager) Class.forName(sessionMgrImpl).newInstance();
        logEvent("Using session manager class " + sessionMgrImpl);
        sessionMgr.init(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);
            }
        }

        // reset the classloader to the parent/system/server classloader.
        Thread.currentThread().setContextClassLoader(typemgr.getClassLoader().getParent());

        try {
            this.qbean = new QueryBean(this, "query-filter-" + getName());
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new InstantiationException("Could not instantiate the QueryBean for " + this.name);
        }

        Enumeration e = this.dbProps.keys();
        while (e.hasMoreElements()) {
            String key = (String) e.nextElement();
            if (key.indexOf(".type") != -1) {
                String value = this.dbProps.getProperty(key);
                if (!extDbTypes.contains(value) && !value.equalsIgnoreCase("relational")) {
                    extDbTypes.add(value);
                }
            }
        }
        try {
            updateResources();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    private void setupDefaultDb(ResourceProperties dbprops) {
        if (dbprops.get("_default.driver") == null || dbprops.get("_default.url") == null) {
            dbprops.put("_default.driver", TransSource.DEFAULT_DRIVER);
            if (Server.getServer().isDbServerTcp()) {
                String port = Server.getServer().getTcpServerPort();
                String host = Server.getServer().getTcpServerHost();
                if (host == null) {
                    host = "localhost";
                }
                StringBuffer defurl = new StringBuffer();
                defurl.append("jdbc:h2:tcp://").append(host).append(":");
                defurl.append(port).append("/").append(TransSource.TRANSACTIONS_DB_NAME).append("_" + name);
                dbprops.put("_default.url", defurl.toString());
            } else {
                File db = dbDir;

                if (!db.exists() || !db.isDirectory()) {
                    db.mkdir();
                }
                String path = db.getPath();
                //              String path = db.getAbsolutePath();
                if (!path.endsWith(File.separator)) {
                    path += File.separator;
                }
                path += TransSource.TRANSACTIONS_DB_DIR;

                db = new File(path);
                if (!path.endsWith(File.separator)) {
                    path += File.separator;
                }

                path += TransSource.TRANSACTIONS_DB_NAME;
                dbprops.put("_default.url", TransSource.DEFAULT_URL + path);
            }
        }
    }

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

        this.onStart();

        synchronized (this) {
            worker = new Thread(this, "Worker-" + name);
            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();
            }
        }

        // 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);
        } catch (Exception e) {
            e.printStackTrace();
        }

        this.executionCache.shutdown();
        this.talCache.shutdown();

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

            for (int i = 0; i < extensions.size(); i++) {
                AxiomExtension ext = (AxiomExtension) 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();

        if (this.pathIndexer != null) {
            try {
                this.pathIndexer.shutdown();
            } catch (Exception ex) {
                ex.printStackTrace();
                this.logError("Failed to shutdown the PathIndexer", ex);
            }
        }

        if (this.qbean != null) {
            this.qbean.shutdown();
        }

        AxiomDebugger.removeDebugger(this);

        releaseEvaluator(eval);

        Handler[] handlers = this.server.getContexts().getHandlers();
        for (int i = 0; i < handlers.length; i++) {
            if (handlers[i] instanceof ContextHandler) {
                if (handlers[i] != null) {
                    ContextHandler context = (ContextHandler) handlers[i];
                    if (this.contextPaths.contains(context.getContextPath())) {
                        if (!context.isStopped()) {
                            try {
                                context.stop();
                                context.destroy();
                            } catch (Exception e) {
                            }
                        }
                    }
                }
            }
        }
        contextPaths.clear();
    }

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

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

    public String getCookieDomain() {
        return this.cookieDomain;
    }

    /**
     * Get the application's database directory.
     * 
     * Useful function overall to have, but in particular, used in the 
     * QueryBean class to get the Lucene index location.
     */
    public File getDbDir() {
        return this.dbDir;
    }

    /**
     * Get the applications File/Image (blob) storage location.
     */
    public String getBlobDir() {
        return this.blobDir;
    }

    /**
     * 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 {
            RequestEvaluator ev = freeThreads.pop();
            //this.logEvent("Getting evaluator " + ev + ", total evaluators = " + allThreads.size() + ", free evaluators = " + freeThreads.size());
            return ev;
        } catch (EmptyStackException nothreads) {
            int maxThreads = 50;

            try {
                maxThreads = Integer.parseInt(props.getProperty("maxThreads"));
            } catch (Exception ignore) {
                // property not set, use default value
            }

            synchronized (this) {
                // allocate a new evaluator
                if (allThreads.size() < maxThreads) {
                    RequestEvaluator ev = new RequestEvaluator(this);

                    allThreads.addElement(ev);
                    this.logEvent("Getting evaluator " + ev + ", total evaluators = " + allThreads.size()
                            + ", free evaluators = " + freeThreads.size());

                    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);

                RequestEvaluator ev = freeThreads.pop();
                this.logEvent("Getting evaluator " + ev + ", total evaluators = " + allThreads.size()
                        + ", free evaluators = " + freeThreads.size());
                return ev;
            } catch (EmptyStackException nothreads) {
                logEvent("Exception in Application::getEvaluator:" + nothreads);
            } 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.logEvent("Releasing evaluator " + ev + ", total evaluators = " + allThreads.size() + ", free evaluators = " + freeThreads.size());
        }
    }

    /**
     * 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) {
        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 = freeThreads.pop();

                        allThreads.removeElement(re);

                        // typemgr.unregisterRequestEvaluator (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());

        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 = activeRequests.get(req);

            if (ev != null) {
                res = ev.attachHttpRequest(req);
            }

            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(name, x.getMessage());
        } 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);
                }
            } else {
                res.waitForClose();
            }
        }

        return res;
    }

    /**
     *  Called to execute a method via XML-RPC, usally by axiom.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<String> 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 retrieved from
     * the database.
     */
    public void clearCache() {
        nmgr.clearCache();
    }

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

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

    /**
     * This method returns the root object of this application's object tree.
     */
    public Object getDataRoot() {
        // check if we have a custom root object class
        if (rootObjectClass != null) {
            // create custom root element.
            if (rootObject == null) {
                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;
        }
        // no custom root object is defined - use standard Axiom objectmodel
        else {
            return nmgr.safe.getRootNode();
        }
    }

    /**
     *  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 axiom.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 axiom.objectmodel packages, which perform the mapping of objects to
     * relational database tables or the embedded database.
     */
    public WrappedNodeManager getWrappedNodeManager() {
        return nmgr.safe;
    }

    /**
     *  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 axiomobject prototype.
     */
    public Prototype getPrototype(Object obj) {
        String protoname = getPrototypeName(obj);

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

        Prototype p = typemgr.getPrototype(protoname);

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

        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 java.util.Collection<Prototype> getPrototypes() {
        return typemgr.getPrototypes();
    }

    public String[] getPrototypeNames() {
        java.util.Collection<Prototype> protos = typemgr.getPrototypes();
        String[] protoNames = new String[protos.size()];
        int count = 0;
        for (Prototype prototype : protos) {
            protoNames[count++] = prototype.getName();
        }
        return protoNames;
    }

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

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

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

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

    /**
     *  Return the whole session map.
     */
    public Map<String, Session> getSessions() {
        return sessionMgr.getSessions();
    }

    /**
     * Returns the number of currently 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();
    }

    /**
     *  Return the href to the root of this application.
     */
    public String getRootHref() throws UnsupportedEncodingException {
        return getNodeHref(getDataRoot(), null);
    }

    /**
     * Return a path to be used in a URL pointing to the given element  and action
     */

    public String getNodeHref(Object elem, String actionName) throws UnsupportedEncodingException {
        return this.getNodeHref(elem, actionName, false);
    }

    public String getNodeHref(Object elem, String actionName, boolean apply_rewrite)
            throws UnsupportedEncodingException {
        StringBuffer b = new StringBuffer();
        composeHref(elem, b, 0);
        if (actionName != null) {
            b.append(UrlEncoded.encode(actionName, charset));
        }

        String uri = baseURI + (apply_rewrite ? applyUrlRewrite(b.toString()) : b.toString());
        int len;
        if ((len = uri.length()) > 1 && uri.endsWith("/")) {
            uri = uri.substring(0, len - 1);
        }

        return uri;
    }

    public String applyUrlRewrite(String href) {
        String[][] rules = this.rewriteRules;
        final int length = rules.length;
        boolean addedSlash = false;
        if (!href.startsWith("/")) {
            href = "/" + href;
            addedSlash = true;
        }
        for (int i = 0; i < length; i++) {
            if (href.startsWith(rules[i][1])) {
                href = href.replaceFirst(rules[i][1], rules[i][0]);
                break;
            }
        }
        if (addedSlash) {
            href = href.substring(1);
        }
        return href;
    }

    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(parent, 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;
    }

    /**
     *  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;
    }

    /**
     *  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 Axiom'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[1];

        arg[0] = 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 Axiom scripting and rendering framework.
     */
    public String getPrototypeName(Object obj) {
        if (obj == null) {
            return "global";
        }

        // check if e implements the IPathElement interface
        if (obj instanceof IPathElement) {
            // e implements the getPrototype() method
            return ((IPathElement) obj).getPrototype();
        } else {
            // look up prototype name by java class name
            Class clazz = obj.getClass();
            String protoname = classMapping.getProperty(clazz.getName());
            if (protoname != null) {
                return protoname;
            }
            // walk down superclass path
            while ((clazz = clazz.getSuperclass()) != null) {
                protoname = classMapping.getProperty(clazz.getName());
                if (protoname != null) {
                    // cache the class name for the object so we run faster next time
                    classMapping.setProperty(obj.getClass().getName(), protoname);
                    return protoname;
                }
            }
            // check interfaces, too
            Class[] classes = obj.getClass().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(obj.getClass().getName(), protoname);
                    return protoname;
                }
            }
            // nada
            return null;
        }
    }

    //////////////////////////////////////////////////////////////////////////////////////////////////////////
    ///   The following methods are the IPathElement interface for this application.
    ///   this is useful for scripting and url-building in the base-app.
    //////////////////////////////////////////////////////////////////////////////////////////////////////////
    public String getElementName() {
        return name;
    }

    /**
     *
     *
     * @param name ...
     *
     * @return ...
     */
    public IPathElement getChildElement(String name) {
        return null;
    }

    /**
     *
     *
     * @return ...
     */
    public IPathElement getParentElement() {
        return axiom.main.Server.getServer();
    }

    /**
     *
     *
     * @return ...
     */
    public String getPrototype() {
        return "application";
    }

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

    /**
     * Log an application error
     */
    public void logError(String msg, Throwable error) {
        getErrorLog().error(msg, error);
    }

    /**
     * Log an application error
     */
    public void logError(String msg) {
        getErrorLog().error(msg);
    }

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

    /**
     * Log an exception's message and stack trace
     */
    public void logEvent(Exception ex) {
        StringBuffer msg = new StringBuffer();
        msg.append(ex.getLocalizedMessage());
        msg.append("\n");

        StackTraceElement[] trace = ex.getStackTrace();
        int len = trace.length;
        for (int i = 0; i < len; i++) {
            msg.append("\tat " + trace[i].toString());
            msg.append("\n");
        }

        getEventLog().info(msg);
    }

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

    /**
     * get the app's event log.
     */
    Log getEventLog() {
        if (eventLog == null) {
            eventLog = getLogger(eventLogName);
            // set log level for event log in case it is a axiom.util.Logger
            if (eventLog instanceof Logger) {
                ((Logger) eventLog).setLogLevel(debug ? Logger.DEBUG : Logger.INFO);
            }

        }
        return eventLog;
    }

    Log getErrorLog() {
        if (errorLog == null && errorLogName != null) {
            errorLog = getLogger(errorLogName);
            // set log level for event log in case it is a axiom.util.Logger
            if (errorLog instanceof Logger) {
                ((Logger) errorLog).setLogLevel(debug ? Logger.DEBUG : Logger.INFO);
            }
            return errorLog;
        }

        return (errorLog != null ? errorLog : getEventLog());
    }

    /**
     * get the app's access log.
     */
    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);
        }
    }

    public void onStart() {
        for (Object f : onstartFunctions) {
            String func = f.toString();
            RequestEvaluator eval = null;
            try {
                eval = getEvaluator();
                // initialize scripting engine
                eval.initScriptingEngine();
                this.setCurrentRequestEvaluator(eval);
                // update scripting prototypes
                eval.scriptingEngine.updatePrototypes();
                String strt = this.getProperty("onStartTimeout");
                long timeout = -1;
                if (strt != null) {
                    try {
                        timeout = Long.parseLong(strt) * 1000L;
                    } catch (Exception ex) {
                        timeout = -1;
                    }
                }
                if (timeout == -1) {
                    eval.invokeInternal(null, func, RequestEvaluator.EMPTY_ARGS);
                } else {
                    eval.invokeInternal(null, func, RequestEvaluator.EMPTY_ARGS, timeout);
                }
            } catch (Exception xcept) {
                logError("Error in " + name + " " + func, xcept);
            } finally {
                releaseEvaluator(eval);
                this.setCurrentRequestEvaluator(null);
            }
        }
    }

    /**
     * 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();

        // logEvent ("Starting scheduler for "+name);

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

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

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

            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) {
                logEvent("Error in Application::run (error parsing sleepInterval)" + ignore.getMessage());
            }

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

        // when interrupted, shutdown running cron jobs
        synchronized (activeCronJobs) {
            for (CronRunner cron : activeCronJobs.values()) {
                cron.interrupt();
                activeCronJobs.remove(cron);
            }
        }

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

    /**
     * Get the session timeout.
     * 
     * @return the session timeout in milliseconds.
     */
    public long getSessionTimeout() {
        return this.sessionTimeout;
    }

    /**
     * Purge sessions that have not been used for a certain amount of time.
     * This is called by run().
     *
     * @param lastSessionCleanup the last time sessions were purged
     * @return the updated lastSessionCleanup value
     */
    private long cleanupSessions(long lastSessionCleanup) {
        long now = System.currentTimeMillis();
        long sessionCleanupInterval = 60000;

        // check if we should clean up user sessions
        if ((now - lastSessionCleanup) > sessionCleanupInterval) {
            RequestEvaluator thisEvaluator = null;

            try {

                thisEvaluator = getEvaluator();

                Map<String, Session> sessions = sessionMgr.getSessions();
                for (Session session : sessions.values()) {
                    if ((now - session.lastTouched()) > (sessionTimeout)) {
                        NodeHandle userhandle = session.userHandle;

                        if (userhandle != null) {
                            try {
                                Object[] param = { session.getSessionId() };

                                thisEvaluator.invokeInternal(userhandle, "onLogout", param);
                            } catch (Exception x) {
                                // errors should already be logged by requestevaluator, but you never know
                                logError("Error in onLogout", x);
                            }
                        }

                        sessionMgr.discardSession(session);
                    }
                }
            } catch (Exception cx) {
                logEvent("Error cleaning up sessions: " + cx);
            } finally {
                if (thisEvaluator != null) {
                    releaseEvaluator(thisEvaluator);
                }
            }
            return now;
        } else {
            return lastSessionCleanup;
        }
    }

    /**
     * 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<CronJob> jobs = CronJob.parse(props);
        Date date = new Date();

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

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

        for (CronJob job : jobs) {
            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) {
        for (Enumeration en = classMapping.elements(); en.hasMoreElements();) {
            String value = (String) en.nextElement();

            if (typename.equals(value)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Return the java class that a given prototype wraps, or null.
     */
    public String getJavaClassForPrototype(String typename) {
        for (Map.Entry entry : classMapping.entrySet()) {

            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 = dbSources.get(dbSrcName);

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

        try {
            if (name.equalsIgnoreCase("_default")) {
                ResourceProperties tsource_props = tsource.getProperties();
                ResourceProperties def_props = new ResourceProperties();
                for (Object key_object : tsource_props.keySet()) {
                    String key = (String) key_object;
                    def_props.setProperty("_default." + key, tsource_props.getProperty(key));
                }
                dbs = new DbSource(name, def_props);
            } else {
                dbs = new DbSource(name, dbProps);
            }
        } catch (Exception problem) {
            logEvent("Error creating DbSource " + name + ": ");
            logEvent(problem.toString());
        }

        dbSources.put(dbSrcName, dbs);
        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
     * @return if the repository was not yet contained
     */
    public boolean addRepository(Repository rep) {
        if (rep != null && !repositories.contains(rep)) {
            // Add the new repository before its parent repository.
            // This establishes the order of compilation between FileRepositories
            // and embedded ZipRepositories.
            Repository parent = rep.getParentRepository();
            if (parent != null) {
                int idx = repositories.indexOf(parent);
                if (idx > -1) {
                    repositories.add(idx, 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<Repository> getRepositories() {
        return Collections.unmodifiableList(repositories);
    }

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

    /**
     * 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", "ISO-8859-1");

            // 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;
            }

            // default to 1hour
            String defaultSessionTimeout = "3600";
            String sessionTimeout = props.getProperty("sessiontimeout", defaultSessionTimeout);
            try {
                this.sessionTimeout = Long.parseLong(sessionTimeout) * 1000L;
            } catch (Exception ignore) {
                // go with default value
                requestTimeout = 3600000L;
            }

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

            if (base != null) {
                setBaseURI(base);
            } else if (baseURI == null) {
                baseURI = "/";
            }
            hrefRootPrototype = props.getProperty("hrefrootprototype");

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

            // update extensions
            if (Server.getServer() != null) {
                for (AxiomExtension ext : Server.getServer().getExtensions()) {
                    try {
                        ext.applicationUpdated(this);
                    } catch (ConfigurationException e) {
                        logEvent("Error updating extension " + ext + ": " + e);
                    }
                }
            }
            logDir = props.getProperty("logdir");
            if (logDir != null) {
                File dir = new File(logDir);
                System.setProperty("axiom.logdir", dir.getAbsolutePath());
            } else {
                logDir = "log";
            }

            String repos = props.getProperty("db.blob.dir");
            if (repos == null) {
                File dir = new File(this.dbDir, "blob");
                if (!dir.exists() || !dir.isDirectory()) {
                    if (!dir.mkdir()) {
                        throw new IllegalArgumentException("Could not create the blob dir for " + this.name);
                    }
                }
                repos = dir.getPath();
            } else {
                File dir = new File(repos);
                if (!dir.isAbsolute()) {
                    ResourceProperties appProps = new ResourceProperties(this, "app.properties");
                    boolean definedInApp = appProps.getProperty("db.blob.dir") != null ? true : false;
                    if (definedInApp) {
                        dir = new File(appDir, repos);
                    } else {
                        dir = new File(axiomHome, repos);
                    }
                }
                if (!dir.exists() || !dir.isDirectory()) {
                    if (!dir.mkdirs()) {
                        throw new IllegalArgumentException("Could not create the blob dir for " + this.name);
                    }
                }
                repos = dir.getPath();
            }
            this.blobDir = repos;

            this.autoUpdate = new Boolean(props.getProperty("automaticResourceUpdate", "true")).booleanValue();

            // set log level for event log in case it is a axiom.util.Logger
            if (eventLog instanceof Logger) {
                ((Logger) eventLog).setLogLevel(debug ? Logger.DEBUG : Logger.INFO);
            }

            String onStart = props.getProperty("onStart");
            if (onStart != null) {
                for (String funcs : onStart.split(",")) {
                    this.onstartFunctions.add(funcs.trim());
                }
            }

            // 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;
    }

    /**
     * Get the applications Search Profiles
     * 
     * @deprecated As of 3.2.9, replaced by {@link #getSearchProfiles()}
     */
    public ResourceProperties getSearchProperties() {
        return this.searchProps;
    }

    /**
     * Get the applications Search Profiles
     * 
     * @return A ResourceProperties object containing the profiles
     */
    public ResourceProperties getSearchProfiles() {
        return this.searchProps;
    }

    /**
     * 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");
    }

    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 + "]";
        }
    }

    /**
     * Method to get the static mountpoint for this application, for use in the href() methods
     * of the file/image api
     */

    public String getStaticMountpoint() {
        return this.staticMountpoint;
    }

    public String getStaticMountpoint(Object action) {
        String act = "";
        if (action != null && !(action instanceof Undefined)) {
            if (action instanceof Wrapper) {
                act = ((Wrapper) action).unwrap().toString();
            } else {
                act = action.toString();
            }

            if (act != null && !act.startsWith("/")) {
                act = "/" + act;
            }
        }

        String mountPoint = this.getStaticMountpoint();
        if (mountPoint.endsWith("/")) {
            mountPoint = mountPoint.substring(0, mountPoint.length() - 1);
        }

        return mountPoint + act;
    }

    /**
     * Method to get the Transaction Db Source, for implementing transactionality of commits
     */

    public TransSource getTransSource() {
        return this.tsource;
    }

    /**
     * Method to get the Path Indexer, for storing id to path mappings on all nodes in an 
     * appliation.
     */
    public PathIndexer getPathIndexer() {
        return this.pathIndexer;
    }

    /**
     * Update the application's prototype resources.
     */
    public void updateResources() throws IOException {
        RequestEvaluator reqEval = this.getCurrentRequestEvaluator();
        if (reqEval != null && reqEval.scriptingEngine instanceof RhinoEngine) {
            RhinoCore core = ((RhinoEngine) reqEval.scriptingEngine).getCore();
            if (core != null) {
                core.updatePrototypes(true);
            }
        }
    }

    /**
     * Method to return the Sessions object that is the root of all stored session data
     * in the object database
     */
    public INode getSessionsRoot() {
        INode sessions = nmgr.safe.getNode(this.sessionsId, sessionRootMapping);

        sessions.setDbMapping(sessionRootMapping);

        return sessions;
    }

    public String getSessionsRootId() {
        return this.sessionsId;
    }

    public ArrayList<String> getSearchablePrototypes() {
        ArrayList<String> names = new ArrayList<String>();
        for (Prototype n : this.getPrototypes()) {
            if (n != null) {
                String name = ((Prototype) n).getName();
                if ((name != null) && (!name.equals("AxiomObject")) && (!name.equals("Global"))) {
                    names.add(name);
                }
            }
        }
        return names;
    }

    public static boolean resourceExists(Prototype prototype, String resourceName) {
        while (prototype != null) {
            Resource[] resources = prototype.getResources();
            for (int i = resources.length - 1; i > -1; i--) {
                Resource resource = resources[i];
                if (resource.exists() && resource.getShortName().equals(resourceName))
                    return true;
            }
            prototype = prototype.getParentPrototype();
        }

        return false;
    }

    public String getTmpDir() {
        String tmpdir = this.getProperty("tmpdir");
        if (tmpdir == null) {
            tmpdir = System.getProperty("java.io.tmpdir");
        }
        if (tmpdir != null && !tmpdir.endsWith(File.separator)) {
            tmpdir += File.separator;
        }
        return tmpdir;
    }

    public void isAllowed(Object obj, String action, RhinoEngine rhinoEng, RequestEvaluator reqeval) {
        if (!(obj instanceof AxiomObject)) {
            obj = Context.toObject(obj, rhinoEng.getCore().getScope());
        }

        reqeval.getRequest().setAction(action);

        boolean allowed = ActionSecurityManager.isAllowed(this, obj, action, rhinoEng, reqeval);

        if (!allowed) {
            logError("Unauthorized for action " + action + ".");
            reqeval.getResponse().setStatus(401);
            throw new RuntimeException("Unauthorized for action " + action + ".");
        }
    }

    public boolean getOmitXmlDecl() {
        return omitXmlDecl;
    }

    public QueryBean getQueryBean() {
        QueryBean qb = this.qbean;
        RequestEvaluator reqev = this.getCurrentRequestEvaluator();
        if (reqev != null) {
            RhinoEngine re = (RhinoEngine) reqev.scriptingEngine;
            qb.setRhinoCore(re.getCore());
        } else {
            qb.setRhinoCore(null);
        }
        return qb;
    }

    public String[][] getRewriteRules() {
        return this.rewriteRules;
    }

    public String resolveUrlToPath(String url) {
        if (url.startsWith(this.getStaticMountpoint())) {
            return url;
        }
        if (!url.startsWith("/")) {
            url = "/" + url;
        }
        boolean addedSlash = false;
        if (!url.endsWith("/")) {
            url += "/";
            addedSlash = true;
        }
        String[][] rules = this.getRewriteRules();
        final int length = rules.length;
        for (int i = 0; i < length; i++) {
            if (url.startsWith(rules[i][0])) {
                url = url.replaceFirst(rules[i][0], rules[i][1]);
                break;
            }
        }
        if (addedSlash) {
            url = url.substring(0, url.length() - 1);
        }
        url = url.substring(1);
        return url;
    }

    /**
     * Adds a rewrite rule to the head of the array.
     * 
     * @param from
     * @param to
     */
    public void addRewriteRule(String from, String to) {
        // need to synchronize because the length and items within can change
        synchronized (this.rewriteRules) {
            final int rrlen = this.rewriteRules.length;
            String[][] rules = new String[rrlen + 1][2];
            rules[0][0] = from;
            rules[0][1] = to;

            for (int i = 0; i < rrlen; i++) {
                rules[i + 1] = this.rewriteRules[i];
            }

            this.rewriteRules = rules;
        }
    }

    protected String[][] setupRewriteRules() {
        String[][] rules = null;
        for (Repository repository : this.getRepositories()) {
            BufferedReader br = null;
            boolean found_resource = false;
            try {
                //properties aren't ordered, must read lines
                Resource res = repository.getResource("rewrite.properties");
                if (res != null && res.exists()) {
                    br = new BufferedReader(new InputStreamReader(res.getInputStream()));
                    String line;
                    ArrayList<String> lines = new ArrayList<String>();
                    while ((line = br.readLine()) != null) {
                        lines.add(line);
                    }

                    final int size = lines.size();
                    rules = new String[size][2];
                    for (int i = 0; i < size; i++) {
                        String[] split = lines.get(i).toString().split("=");
                        if (split.length > 1) {
                            split[0] = split[0].trim();
                            split[1] = split[1].trim();
                            if (!split[0].equals("/") && !split[0].endsWith("/")) {
                                split[0] += "/";
                            }
                            if (!split[1].equals("/") && !split[1].endsWith("/")) {
                                split[1] += "/";
                            }
                            rules[i][0] = split[0].trim();
                            rules[i][1] = split[1].trim();
                        } else {
                            rules[i][0] = "";
                            rules[i][1] = "";
                        }
                    }

                    lines.clear();
                    lines = null;

                    found_resource = true;
                }
            } catch (Exception ignore) {
                logEvent("Error applying rewrite rules: " + ignore.getMessage());
            } finally {
                if (br != null) {
                    try {
                        br.close();
                    } catch (Exception ignoreagain) {
                        logEvent("Error applying rewrite rules: " + ignoreagain.getMessage());
                    }
                    br = null;
                }
            }

            if (found_resource) {
                break;
            }
        }

        if (rules == null) {
            rules = new String[0][0];
        }

        return rules;
    }

    public boolean autoUpdate() {
        return this.autoUpdate;
    }

    public ClusterCommunicator getClusterCommunicator() {
        return this.clusterComm;
    }

    public String getClusterHost() {
        return this.clusterHost;
    }

    public ExecutionCache getExecutionCache() {
        return this.executionCache;
    }

    public ExecutionCache getTALCache() {
        return this.talCache;
    }

    public boolean isFunctionResponseCachable(Scriptable obj, Object result, String func) {
        if (result != null && obj instanceof AxiomObject) {
            INode n = ((AxiomObject) obj).getNode();
            String prototype = n.getPrototype();
            Prototype p = this.getPrototypeByName(prototype);
            if (p.isFunctionResponseCachable(func)) {
                return true;
            }
        }
        return false;
    }

    public boolean isFunctionResultCachable(Scriptable obj, Object result, String func) {

        if (result != null && obj instanceof AxiomObject) {
            INode n = ((AxiomObject) obj).getNode();
            if (n == null) {
                return false;
            }
            String prototype = n.getPrototype();
            Prototype p = this.getPrototypeByName(prototype);
            if (p.isFunctionReturnCachable(func)) {
                return true;
            }
        }
        return false;
    }

    public boolean isPropertyFilesIgnoreCase() {
        String s = (String) this.props.get("propertyFilesIgnoreCase");
        if (s != null && "true".equalsIgnoreCase(s)) {
            return true;
        }
        return false;
    }

    /**
     * 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 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);
    }

    public int getLayer(String host) {
        int mode = DbKey.LIVE_LAYER;
        if (host != null) {
            Object pos = (Object) this.draftHosts.get(host);
            if (pos != null) {
                mode = ((Integer) pos).intValue();
            }
        }
        return mode;
    }

    public int getHighestPreviewLayer() {
        return this.highestPreviewLayer;
    }

    public Object[] getDomainsForLayer(int layer) {
        if (layer <= DbKey.LIVE_LAYER) {
            return new Object[0];
        }

        String value = this.props.getProperty("draftHost." + layer);
        if (value != null) {
            String[] split = value.split(",");
            Object[] ret = new Object[split.length];
            for (int i = 0; i < split.length; i++) {
                ret[i] = split[i].trim();
            }
            return ret;
        }

        return new Object[0];
    }

    public boolean dbTypeExists(String dbType) {
        if (dbType == null) {
            return false;
        }

        dbType = dbType.toLowerCase();
        Enumeration e = this.dbProps.keys();
        while (e.hasMoreElements()) {
            String key = (String) e.nextElement();
            if (key.endsWith(".type")) {
                String value = this.dbProps.getProperty(key);
                if (value != null && dbType.equals(value.toLowerCase())) {
                    return true;
                }
            }
        }

        return false;
    }

    public ArrayList<String> getDbNames() {
        ArrayList<String> ret = new ArrayList<String>();
        Enumeration e = this.dbProps.keys();
        while (e.hasMoreElements()) {
            String key = (String) e.nextElement();
            if (key.indexOf(".") == -1) {
                ret.add(key);
            }
        }
        return ret;
    }

    public ArrayList<String> getExtDBTypes() {
        return extDbTypes;
    }

    private void updateDbLocation(String appName) {
        try {
            File oldDbDir = new File(dbDir, TransSource.TRANSACTIONS_DB_DIR + "_" + appName);
            if (oldDbDir.exists()) {
                File appDbDir = new File(dbDir, appName);
                if (appDbDir.exists()) {
                    File appTransDbDir = new File(appDbDir, TransSource.TRANSACTIONS_DB_DIR);
                    if (!appTransDbDir.exists()) {
                        appTransDbDir.mkdir();
                    }
                    File[] appFiles = oldDbDir.listFiles();
                    System.out.println("Updating database location on file system...");
                    for (int i = 0; i < appFiles.length; i++) {
                        appFiles[i].renameTo(new File(appTransDbDir, appFiles[i].getName()));
                    }
                }
            }
            oldDbDir.delete();
        } catch (Exception e) {
            System.out.println("Error in updateDb: " + e);
        }
    }

    public String getLogDir() {
        return logDir;
    }

    public String getRequestLogName() {
        return requestLogName;
    }

    public void addContextPath(String path) {
        contextPaths.add(path);
    }

    public Server getServer() {
        return server;
    }
}