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

package helma.framework.core;

import helma.framework.*;
import helma.objectmodel.*;
import helma.objectmodel.db.*;
import helma.scripting.*;
import java.lang.reflect.*;
import java.util.*;

import org.apache.xmlrpc.XmlRpcRequestProcessor;
import org.apache.xmlrpc.XmlRpcServerRequest;
import org.apache.commons.logging.Log;

/**
 * This class does the work for incoming requests. It holds a transactor thread
 * and an EcmaScript evaluator to get the work done. Incoming threads are
 * blocked until the request has been serviced by the evaluator, or the timeout
 * specified by the application has passed. In the latter case, the evaluator thread
 * is killed and an error message is returned.
 */
public final class RequestEvaluator implements Runnable {
    static final int NONE = 0; // no request
    static final int HTTP = 1; // via HTTP gateway
    static final int XMLRPC = 2; // via XML-RPC
    static final int INTERNAL = 3; // generic function call, e.g. by scheduler
    static final int EXTERNAL = 4; // function from script etc

    public static final Object[] EMPTY_ARGS = new Object[0];

    public final Application app;

    protected ScriptingEngine scriptingEngine;

    // skin depth counter, used to avoid recursive skin rendering
    protected int skinDepth;

    private volatile RequestTrans req;
    private volatile ResponseTrans res;

    // the one and only transactor thread
    private volatile Thread thread;

    private volatile Transactor transactor;

    // the type of request to be serviced,
    // used to coordinate worker and waiter threads
    private volatile int reqtype;

    // the object on which to invoke a function, if specified
    private volatile Object thisObject;

    // the method to be executed
    private volatile Object function;

    // the session object associated with the current request
    private volatile Session session;

    // arguments passed to the function
    private volatile Object[] args;

    // the result of the operation
    private volatile Object result;

    // the exception thrown by the evaluator, if any.
    private volatile Exception exception;

    // For numbering threads.
    private int threadId;


    /**
     *  Create a new RequestEvaluator for this application.
     *  @param app the application
     */
    public RequestEvaluator(Application app) {
        this.app = app;
    }

    protected synchronized void initScriptingEngine() {
        if (scriptingEngine == null) {
            String engineClassName = app.getProperty("scriptingEngine",
                                                     "helma.scripting.rhino.RhinoEngine");
            try {
                app.setCurrentRequestEvaluator(this);
                Class clazz = app.getClassLoader().loadClass(engineClassName);

                scriptingEngine = (ScriptingEngine) clazz.newInstance();
                scriptingEngine.init(app, this);
            } catch (Exception x) {
                Throwable t = x;

                if (x instanceof InvocationTargetException) {
                    t = ((InvocationTargetException) x).getTargetException();
                }

                app.logEvent("******************************************");
                app.logEvent("*** Error creating scripting engine: ");
                app.logEvent("*** " + t.toString());
                app.logEvent("******************************************");
                app.logError("Error creating scripting engine", t);

                // null out invalid scriptingEngine
                scriptingEngine = null;
                // rethrow exception
                if (t instanceof RuntimeException) {
                    throw((RuntimeException) t);
                } else {
                    throw new RuntimeException(t.toString(), t);
                }
            } finally {
                app.setCurrentRequestEvaluator(null);
            }
        }
    }

    protected synchronized void shutdown() {
        if (scriptingEngine != null) {
            scriptingEngine.shutdown();
        }
    }

    /**
     *
     */
    public void run() {
        // first, set a local variable to the current transactor thread so we know
        // when it's time to quit because another thread took over.
        Thread localThread = Thread.currentThread();

        // spans whole execution loop - close connections in finally clause
        try {

            // while this thread is serving requests
            while (localThread == thread) {

                // object reference to ressolve request path
                Object currentElement;

                // Get req and res into local variables to avoid memory caching problems
                // in unsynchronized method.
                RequestTrans req = getRequest();
                ResponseTrans res = getResponse();

                // request path object
                RequestPath requestPath = new RequestPath(app);

                String txname = req.getMethod() + ":" + req.getPath();
                Log eventLog = app.getEventLog();
                if (eventLog.isDebugEnabled()) {
                    eventLog.debug(txname + " starting");
                }

                int tries = 0;
                boolean done = false;
                Throwable error = null;
                String functionName = function instanceof String ?
                        (String) function : null;

                while (!done && localThread == thread) {
                    // catch errors in path resolution and script execution
                    try {

                        // initialize scripting engine
                        initScriptingEngine();
                        app.setCurrentRequestEvaluator(this);
                        // update scripting prototypes
                        scriptingEngine.enterContext();


                        // avoid going into transaction if called function doesn't exist.
                        // this only works for the (common) case that method is a plain
                        // method name, not an obj.method path
                        if (reqtype == INTERNAL) {
                            // if object is an instance of NodeHandle, get the node object itself.
                            if (thisObject instanceof NodeHandle) {
                                thisObject = ((NodeHandle) thisObject).getNode(app.nmgr.safe);
                                // If no valid node object return immediately
                                if (thisObject == null) {
                                    done = true;
                                    reqtype = NONE;
                                    break;
                                }
                            }
                            // If function doesn't exist, return immediately
                            if (functionName != null && !scriptingEngine.hasFunction(thisObject, functionName, true)) {
                                app.logEvent(missingFunctionMessage(thisObject, functionName));
                                done = true;
                                reqtype = NONE;
                                break;
                            }
                        } else if (function != null && functionName == null) {
                            // only internal requests may pass a function instead of a function name
                            throw new IllegalStateException("No function name in non-internal request ");
                        }

                        // Update transaction name in case we're processing an error
                        if (error != null) {
                            txname = "error:" + txname;
                        }

                        // begin transaction
                        transactor = Transactor.getInstance(app.nmgr);
                        transactor.begin(txname);

                        Object root = app.getDataRoot(scriptingEngine);
                        initGlobals(root, requestPath);

                        String action = null;

                        if (error != null) {
                            res.setError(error);
                        }

                        switch (reqtype) {
                            case HTTP:

                                // bring over the message from a redirect
                                session.recoverResponseMessages(res);

                                // catch redirect in path resolution or script execution
                                try {
                                    // catch object not found in path resolution
                                    try {
                                        if (error != null) {
                                            // there was an error in the previous loop, call error handler
                                            currentElement = root;
                                            res.setStatus(500);

                                            // do not reset the requestPath so error handler can use the original one
                                            // get error handler action
                                            String errorAction = app.props.getProperty("error",
                                                    "error");

                                            action = getAction(currentElement, errorAction, req);

                                            if (action == null) {
                                                throw new RuntimeException(error);
                                            }
                                        } else if ((req.getPath() == null) ||
                                                "".equals(req.getPath().trim())) {
                                            currentElement = root;
                                            requestPath.add(null, currentElement);

                                            action = getAction(currentElement, null, req);

                                            if (action == null) {
                                                throw new NotFoundException("Action not found");
                                            }
                                        } else {
                                            // march down request path...
                                            StringTokenizer st = new StringTokenizer(req.getPath(),
                                                    "/");
                                            int ntokens = st.countTokens();

                                            // limit path to < 50 tokens
                                            if (ntokens > 50) {
                                                throw new RuntimeException("Path too long");
                                            }

                                            String[] pathItems = new String[ntokens];

                                            for (int i = 0; i < ntokens; i++)
                                                pathItems[i] = st.nextToken();

                                            currentElement = root;
                                            requestPath.add(null, currentElement);

                                            for (int i = 0; i < ntokens; i++) {
                                                if (currentElement == null) {
                                                    throw new NotFoundException("Object not found.");
                                                }

                                                if (pathItems[i].length() == 0) {
                                                    continue;
                                                }

                                                // if we're at the last element of the path,
                                                // try to interpret it as action name.
                                                if (i == (ntokens - 1) && !req.getPath().endsWith("/")) {
                                                    action = getAction(currentElement, pathItems[i], req);
                                                }

                                                if (action == null) {
                                                    currentElement = getChildElement(currentElement,
                                                            pathItems[i]);

                                                    // add object to request path if suitable
                                                    if (currentElement != null) {
                                                        // add to requestPath array
                                                        requestPath.add(pathItems[i], currentElement);
                                                    }
                                                }
                                            }

                                            if (currentElement == null) {
                                                throw new NotFoundException("Object not found.");
                                            }

                                            if (action == null) {
                                                action = getAction(currentElement, null, req);
                                            }

                                            if (action == null) {
                                                throw new NotFoundException("Action not found");
                                            }
                                        }
                                    } catch (NotFoundException notfound) {
                                        if (error != null) {

                                            // we already have an error and the error template wasn't found,
                                            // display it instead of notfound message
                                            throw new RuntimeException();
                                        }

                                        // The path could not be resolved. Check if there is a "not found" action
                                        // specified in the property file.
                                        res.setStatus(404);

                                        String notFoundAction = app.props.getProperty("notfound",
                                                "notfound");

                                        currentElement = root;
                                        action = getAction(currentElement, notFoundAction, req);

                                        if (action == null) {
                                            throw new NotFoundException(notfound.getMessage());
                                        }
                                    }

                                    // register path objects with their prototype names in
                                    // res.handlers
                                    Map macroHandlers = res.getMacroHandlers();
                                    int l = requestPath.size();
                                    Prototype[] protos = new Prototype[l];

                                    for (int i = 0; i < l; i++) {

                                        Object obj = requestPath.get(i);

                                        protos[i] = app.getPrototype(obj);

                                        // immediately register objects with their direct prototype name
                                        if (protos[i] != null) {
                                            macroHandlers.put(protos[i].getName(), obj);
                                            macroHandlers.put(protos[i].getLowerCaseName(), obj);
                                        }
                                    }

                                    // in a second pass, we register path objects with their indirect
                                    // (i.e. parent prototype) names, starting at the end and only
                                    // if the name isn't occupied yet.
                                    for (int i = l - 1; i >= 0; i--) {
                                        if (protos[i] != null) {
                                            protos[i].registerParents(macroHandlers, requestPath.get(i));
                                        }
                                    }

                                    /////////////////////////////////////////////////////////////////////////////
                                    // end of path resolution section
                                    /////////////////////////////////////////////////////////////////////////////
                                    // beginning of execution section

                                    // set the req.action property, cutting off the _action suffix
                                    req.setAction(action);

                                    // reset skin recursion detection counter
                                    skinDepth = 0;

                                    // try calling onRequest() function on object before
                                    // calling the actual action
                                    scriptingEngine.invoke(currentElement,
                                            "onRequest",
                                            EMPTY_ARGS,
                                            ScriptingEngine.ARGS_WRAP_DEFAULT,
                                            false);

                                    // reset skin recursion detection counter
                                    skinDepth = 0;

                                    Object actionProcessor = req.getActionHandler() != null ?
                                        req.getActionHandler() : action;

                                    // do the actual action invocation
                                    if (req.isXmlRpc()) {
                                        XmlRpcRequestProcessor xreqproc = new XmlRpcRequestProcessor();
                                        XmlRpcServerRequest xreq = xreqproc.decodeRequest(req.getServletRequest()
                                                .getInputStream());
                                        Vector args = xreq.getParameters();
                                        args.add(0, xreq.getMethodName());
                                        result = scriptingEngine.invoke(currentElement,
                                                actionProcessor,
                                                args.toArray(),
                                                ScriptingEngine.ARGS_WRAP_XMLRPC,
                                                false);
                                        res.writeXmlRpcResponse(result);
                                        app.xmlrpcCount += 1;
                                    } else {
                                        scriptingEngine.invoke(currentElement,
                                                actionProcessor,
                                                EMPTY_ARGS,
                                                ScriptingEngine.ARGS_WRAP_DEFAULT,
                                                false);
                                    }

                                    // try calling onResponse() function on object before
                                    // calling the actual action
                                    scriptingEngine.invoke(currentElement,
                                            "onResponse",
                                            EMPTY_ARGS,
                                            ScriptingEngine.ARGS_WRAP_DEFAULT,
                                            false);

                                } catch (RedirectException redirect) {
                                    // if there is a message set, save it on the user object for the next request
                                    if (res.getRedirect() != null)
                                        session.storeResponseMessages(res);
                                }

                                // check if request is still valid, or if the requesting thread has stopped waiting already
                                if (localThread != thread) {
                                    return;
                                }
                                commitTransaction();
                                done = true;

                                break;

                            case XMLRPC:
                            case EXTERNAL:

                                try {
                                    currentElement = root;

                                    if (functionName.indexOf('.') > -1) {
                                        StringTokenizer st = new StringTokenizer(functionName, ".");
                                        int cnt = st.countTokens();

                                        for (int i = 1; i < cnt; i++) {
                                            String next = st.nextToken();
                                            currentElement = getChildElement(currentElement, next);
                                        }

                                        if (currentElement == null) {
                                            throw new NotFoundException("Method name \"" +
                                                    function + "\" could not be resolved.");
                                        }

                                        functionName = st.nextToken();
                                    }

                                    if (reqtype == XMLRPC) {
                                        // check XML-RPC access permissions
                                        String proto = app.getPrototypeName(currentElement);
                                        app.checkXmlRpcAccess(proto, functionName);
                                    }

                                    // reset skin recursion detection counter
                                    skinDepth = 0;
                                    if (!scriptingEngine.hasFunction(currentElement, functionName, false)) {
                                        throw new NotFoundException(missingFunctionMessage(currentElement, functionName));
                                    }
                                    result = scriptingEngine.invoke(currentElement,
                                            functionName, args,
                                            ScriptingEngine.ARGS_WRAP_XMLRPC,
                                            false);
                                    // check if request is still valid, or if the requesting thread has stopped waiting already
                                    if (localThread != thread) {
                                        return;
                                    }
                                    commitTransaction();
                                } catch (Exception x) {
                                    // check if request is still valid, or if the requesting thread has stopped waiting already
                                    if (localThread != thread) {
                                        return;
                                    }
                                    abortTransaction();
                                    app.logError(txname + " " + error, x);

                                    // If the transactor thread has been killed by the invoker thread we don't have to
                                    // bother for the error message, just quit.
                                    if (localThread != thread) {
                                        return;
                                    }

                                    this.exception = x;
                                }

                                done = true;
                                break;

                            case INTERNAL:

                                try {
                                    // reset skin recursion detection counter
                                    skinDepth = 0;

                                    result = scriptingEngine.invoke(thisObject,
                                            function,
                                            args,
                                            ScriptingEngine.ARGS_WRAP_DEFAULT,
                                            true);
                                    // check if request is still valid, or if the requesting thread has stopped waiting already
                                    if (localThread != thread) {
                                        return;
                                    }
                                    commitTransaction();
                                } catch (Exception x) {
                                    // check if request is still valid, or if the requesting thread has stopped waiting already
                                    if (localThread != thread) {
                                        return;
                                    }
                                    abortTransaction();
                                    app.logError(txname + " " + error, x);

                                    // If the transactor thread has been killed by the invoker thread we don't have to
                                    // bother for the error message, just quit.
                                    if (localThread != thread) {
                                        return;
                                    }

                                    this.exception = x;
                                }

                                done = true;
                                break;

                        } // switch (reqtype)
                    } catch (AbortException x) {
                        // res.abort() just aborts the transaction and
                        // leaves the response untouched
                        // check if request is still valid, or if the requesting thread has stopped waiting already
                        if (localThread != thread) {
                            return;
                        }
                        abortTransaction();
                        done = true;
                    } catch (ConcurrencyException x) {
                        res.reset();

                        if (++tries < 8) {
                            // try again after waiting some period
                            // check if request is still valid, or if the requesting thread has stopped waiting already
                            if (localThread != thread) {
                                return;
                            }
                            abortTransaction();

                            try {
                                // wait a bit longer with each try
                                int base = 800 * tries;
                                Thread.sleep((long) (base + (Math.random() * base * 2)));
                            } catch (InterruptedException interrupt) {
                                // we got interrrupted, create minimal error message 
                                res.reportError(interrupt);
                                done = true;
                                // and release resources and thread
                                thread = null;
                                transactor = null;
                            }
                        } else {
                            // check if request is still valid, or if the requesting thread has stopped waiting already
                            if (localThread != thread) {
                                return;
                            }
                            abortTransaction();

                            // error in error action. use traditional minimal error message
                            res.reportError("Application too busy, please try again later");
                            done = true;
                        }
                    } catch (Throwable x) {
                        // check if request is still valid, or if the requesting thread has stopped waiting already
                        if (localThread != thread) {
                            return;
                        }
                        abortTransaction();

                        // If the transactor thread has been killed by the invoker thread we don't have to
                        // bother for the error message, just quit.
                        if (localThread != thread) {
                            return;
                        }

                        res.reset();

                        // check if we tried to process the error already,
                        // or if this is an XML-RPC request
                        if (error == null) {
                            if (!(x instanceof NotFoundException)) {
                                app.errorCount += 1;
                            }

                            // set done to false so that the error will be processed
                            done = false;
                            error = x;

                            app.logError(txname + " " + error, x);

                            if (req.isXmlRpc()) {
                                // if it's an XML-RPC exception immediately generate error response
                                if (!(x instanceof Exception)) {
                                    // we need an exception to pass to XML-RPC responder
                                    x = new Exception(x.toString(), x);
                                }
                                res.writeXmlRpcError((Exception) x);
                                done = true;
                            }
                        } else {
                            // error in error action. use traditional minimal error message
                            res.reportError(error);
                            done = true;
                        }
                    } finally {
                        app.setCurrentRequestEvaluator(null);
                        // exit execution context
                        if (scriptingEngine != null) {
                            try {
                                scriptingEngine.exitContext();
                            } catch (Throwable t) {
                                // broken rhino, just get out of here
                            }
                        }
                    }
                }

                notifyAndWait();

            }
        } finally {
            Transactor tx = Transactor.getInstance();
            if (tx != null) tx.closeConnections();
        }
    }

    /**
     * Called by the transactor thread when it has successfully fulfilled a request.
     * @throws Exception transaction couldn't be committed
     */
    synchronized void commitTransaction() throws Exception {
        Thread localThread = Thread.currentThread();

        if (localThread == thread) {
            Transactor tx = Transactor.getInstance();
            if (tx != null)
                tx.commit();
        } else {
            throw new TimeoutException();
        }
    }

    /**
     * Called by the transactor thread when the request didn't terminate successfully.
     */
    synchronized void abortTransaction() {
        Transactor tx = Transactor.getInstance();
        if (tx != null) tx.abort();
    }

    /**
     * Initialize and start the transactor thread.
     */
    private synchronized void startTransactor() {
        if (!app.isRunning()) {
            throw new ApplicationStoppedException();
        }

        if ((thread == null) || !thread.isAlive()) {
            // app.logEvent ("Starting Thread");
            thread = new Thread(app.threadgroup, this, app.getName() + "-" + (++threadId));
            thread.setContextClassLoader(app.getClassLoader());
            thread.start();
        } else {
            notifyAll();
        }
    }

    /**
     * Tell waiting thread that we're done, then wait for next request
     */
    synchronized void notifyAndWait() {
        Thread localThread = Thread.currentThread();

        // make sure there is only one thread running per instance of this class
        // if localrtx != rtx, the current thread has been aborted and there's no need to notify
        if (localThread != thread) {
            // A new request came in while we were finishing the last one.
            // Return to run() to get the work done.
            Transactor tx = Transactor.getInstance();
            if (tx != null) {
                tx.closeConnections();
            }
            return;
        }

        reqtype = NONE;
        notifyAll();

        try {
            // wait for request, max 10 min
            wait(1000 * 60 * 10);
        } catch (InterruptedException ix) {
            // we got interrrupted, releases resources and thread
            thread = null;
            transactor = null;
        }

        //  if no request arrived, release ressources and thread
        if ((reqtype == NONE) && (thread == localThread)) {
            // comment this in to release not just the thread, but also the scripting engine.
            // currently we don't do this because of the risk of memory leaks (objects from
            // framework referencing into the scripting engine)
            // scriptingEngine = null;
            thread = null;
            transactor = null;
        }
    }

    /**
     * Stop this request evaluator's current thread. This is called by the
     * waiting thread when it times out and stops waiting, or from an outside
     * thread. If currently active kill the request, otherwise just notify.
     */
    synchronized boolean stopTransactor() {
        Transactor t = transactor;
        thread = null;
        transactor = null;
        boolean stopped = false;
        if (t != null && t.isActive()) {
            // let the scripting engine know that the
            // current transaction is being aborted.
            if (scriptingEngine != null) {
                scriptingEngine.abort();
            }

            app.logEvent("Request timeout for thread " + t);

            reqtype = NONE;

            t.kill();
            t.abort();
            t.closeConnections();
            stopped = true;
        }
        notifyAll();
        return stopped;
    }

    /**
     * Invoke an action function for a HTTP request. The function is dispatched
     * in a new thread and waits for it to finish.
     *
     * @param req the incoming HTTP request
     * @param session the client's session
     * @return the result returned by the invocation
     * @throws Exception any exception thrown by the invocation
     */
    public synchronized ResponseTrans invokeHttp(RequestTrans req, Session session)
                                      throws Exception {
        initObjects(req, session);

        app.activeRequests.put(req, this);

        startTransactor();
        wait(app.requestTimeout);

        if (reqtype != NONE && stopTransactor()) {
            res.reset();
            res.reportError("Request timed out");
        }

        session.commit(this, app.sessionMgr);
        return res;
    }

    /**
     * This checks if the Evaluator is already executing an equal request.
     * If so, attach to it and wait for it to complete. Otherwise return null,
     * so the application knows it has to run the request.
     */
    public synchronized ResponseTrans attachHttpRequest(RequestTrans req)
                                             throws Exception {
        // Get a reference to the res object at the time we enter
        ResponseTrans localRes = res;

        if (localRes == null || !req.equals(this.req)) {
            return null;
        }

        if (reqtype != NONE) {
            wait(app.requestTimeout);
        }

        return localRes;
    }

    /*
     * TODO invokeXmlRpc(), invokeExternal() and invokeInternal() are basically the same
     * and should be unified
     */

    /**
     * Invoke a function for an XML-RPC request. The function is dispatched in a new thread
     * and waits for it to finish.
     *
     * @param functionName the name of the function to invoke
     * @param args the arguments
     * @return the result returned by the invocation
     * @throws Exception any exception thrown by the invocation
     */
    public synchronized Object invokeXmlRpc(String functionName, Object[] args)
                                     throws Exception {
        initObjects(functionName, XMLRPC, RequestTrans.XMLRPC);
        this.function = functionName;
        this.args = args;

        startTransactor();
        wait(app.requestTimeout);

        if (reqtype != NONE && stopTransactor()) {
            exception = new RuntimeException("Request timed out");
        }

        // reset res for garbage collection (res.data may hold reference to evaluator)
        res = null;

        if (exception != null) {
            throw (exception);
        }

        return result;
    }



    /**
     * Invoke a function for an external request. The function is dispatched
     * in a new thread and waits for it to finish.
     *
     * @param functionName the name of the function to invoke
     * @param args the arguments
     * @return the result returned by the invocation
     * @throws Exception any exception thrown by the invocation
     */
    public synchronized Object invokeExternal(String functionName, Object[] args)
                                     throws Exception {
        initObjects(functionName, EXTERNAL, RequestTrans.EXTERNAL);
        this.function = functionName;
        this.args = args;

        startTransactor();
        wait();

        if (reqtype != NONE && stopTransactor()) {
            exception = new RuntimeException("Request timed out");
        }

        // reset res for garbage collection (res.data may hold reference to evaluator)
        res = null;

        if (exception != null) {
            throw (exception);
        }

        return result;
    }

    /**
     * Invoke a function internally and directly, using the thread we're running on.
     *
     * @param obj the object to invoke the function on
     * @param function the function or name of the function to invoke
     * @param args the arguments
     * @return the result returned by the invocation
     * @throws Exception any exception thrown by the invocation
     */
    public Object invokeDirectFunction(Object obj, Object function, Object[] args)
                                throws Exception {
        return scriptingEngine.invoke(obj, function, args,
                ScriptingEngine.ARGS_WRAP_DEFAULT, true);
    }

    /**
     * Invoke a function internally. The function is dispatched in a new thread
     * and waits for it to finish.
     *
     * @param object the object to invoke the function on
     * @param function the function or name of the function to invoke
     * @param args the arguments
     * @return the result returned by the invocation
     * @throws Exception any exception thrown by the invocation
     */
    public synchronized Object invokeInternal(Object object, Object function,
                                              Object[] args)
                                       throws Exception {
        // give internal call more time (15 minutes) to complete
        return invokeInternal(object, function, args, 60000L * 15);
    }

    /**
     * Invoke a function internally. The function is dispatched in a new thread
     * and waits for it to finish.
     *
     * @param object the object to invoke the function on
     * @param function the function or name of the function to invoke
     * @param args the arguments
     * @param timeout the time in milliseconds to wait for the function to return, or
     * -1 to wait indefinitely
     * @return the result returned by the invocation
     * @throws Exception any exception thrown by the invocation
     */
    public synchronized Object invokeInternal(Object object, Object function,
                                              Object[] args, long timeout)
                                       throws Exception {
        initObjects(function, INTERNAL, RequestTrans.INTERNAL);
        thisObject = object;
        this.function = function;
        this.args = args;

        startTransactor();
        if (timeout < 0)
            wait();
        else
            wait(timeout);

        if (reqtype != NONE && stopTransactor()) {
            exception = new RuntimeException("Request timed out");
        }

        // reset res for garbage collection (res.data may hold reference to evaluator)
        res = null;

        if (exception != null) {
            throw (exception);
        }

        return result;
    }


    /**
     * Init this evaluator's objects from a RequestTrans for a HTTP request
     *
     * @param req
     * @param session
     */
    private synchronized void initObjects(RequestTrans req, Session session) {
        this.req = req;
        this.reqtype = HTTP;
        this.session = session;
        res = new ResponseTrans(app, req);
        result = null;
        exception = null;
    }

    /**
     * Init this evaluator's objects for an internal, external or XML-RPC type
     * request.
     *
     * @param function the function name or object
     * @param reqtype the request type
     * @param reqtypeName the request type name
     */
    private synchronized void initObjects(Object function, int reqtype, String reqtypeName) {
        this.reqtype = reqtype;
        String functionName = function instanceof String ?
                (String) function : "<function>";
        req = new RequestTrans(reqtypeName, functionName);
        session = new Session(functionName, app);
        res = new ResponseTrans(app, req);
        result = null;
        exception = null;
    }

    /**
     * Initialize the globals in the scripting engine for the current request.
     *
     * @param root
     * @throws ScriptingException
     */
    private synchronized void initGlobals(Object root, Object requestPath)
                throws ScriptingException {
        HashMap globals = new HashMap();

        globals.put("root", root);
        globals.put("session", new SessionBean(session));
        globals.put("req", new RequestBean(req));
        globals.put("res", new ResponseBean(res));
        globals.put("app", new ApplicationBean(app));
        globals.put("path", requestPath);

        // enter execution context
        scriptingEngine.setGlobals(globals);
    }

    /**
     * Get the child element with the given name from the given object.
     *
     * @param obj
     * @param name
     * @return
     * @throws ScriptingException
     */
    private Object getChildElement(Object obj, String name) throws ScriptingException {
        if (scriptingEngine.hasFunction(obj, "getChildElement", false)) {
            return scriptingEngine.invoke(obj, "getChildElement", new Object[] {name},
                                          ScriptingEngine.ARGS_WRAP_DEFAULT, false);
        }

        if (obj instanceof IPathElement) {
            return ((IPathElement) obj).getChildElement(name);
        }

        return null;
    }

    /**
     *  Null out some fields, mostly for the sake of garbage collection.
     */
    synchronized void recycle() {
        res = null;
        req = null;
        session = null;
        function = null;
        args = null;
        result = null;
        exception = null;
    }

    /**
     * Check if an action with a given name is defined for a scripted object. If it is,
     * return the action's function name. Otherwise, return null.
     */
    public String getAction(Object obj, String action, RequestTrans req) {
        if (obj == null)
            return null;

        if (action == null)
            action = "main";

        StringBuffer buffer = new StringBuffer(action).append("_action");
        // record length so we can check without method
        // afterwards for GET, POST, HEAD requests
        int length = buffer.length();

        if (req.checkXmlRpc()) {
            // append _methodname
            buffer.append("_xmlrpc");
            if (scriptingEngine.hasFunction(obj, buffer.toString(), false)) {
                // handle as XML-RPC request
                req.setMethod(RequestTrans.XMLRPC);
                return buffer.toString();
            }
            // cut off method in case it has been appended
            buffer.setLength(length);
        }

        String method = req.getMethod();
        // append HTTP method to action name
        if (method != null) {
            // append _methodname
            buffer.append('_').append(method.toLowerCase());
            if (scriptingEngine.hasFunction(obj, buffer.toString(), false))
                return buffer.toString();

            // cut off method in case it has been appended
            buffer.setLength(length);
        }

        // if no method specified or "ordinary" request try action without method
        if (method == null || "GET".equalsIgnoreCase(method) ||
                              "POST".equalsIgnoreCase(method) ||
                              "HEAD".equalsIgnoreCase(method)) {
            if (scriptingEngine.hasFunction(obj, buffer.toString(), false))
                return buffer.toString();
        }

        return null;
    }

    /**
     * Returns this evaluator's scripting engine
     */
    public ScriptingEngine getScriptingEngine() {
        if (scriptingEngine == null) {
            initScriptingEngine();
        }
        return scriptingEngine;
    }

    /**
     * Get the request object for the current request.
     *
     * @return the request object
     */
    public synchronized RequestTrans getRequest() {
        return req;
    }

    /**
     * Get the response object for the current request.
     *
     * @return the response object
     */
    public synchronized ResponseTrans getResponse() {
        return res;
    }

    /**
     * Get the current transactor thread
     *
     * @return the current transactor thread
     */
    public synchronized Thread getThread() {
        return thread;
    }

    /**
     * Return the current session
     *
     * @return the session for the current request
     */
    public synchronized Session getSession() {
        return session;
    }

    private String missingFunctionMessage(Object obj, String funcName) {
        if (obj == null)
            return "Function " + funcName + " not defined in global scope";
        else
            return "Function " + funcName + " not defined for " + obj;
    }
}
