com.inverse2.ajaxtoaster.AjaxToasterServlet.java Source code

Java tutorial

Introduction

Here is the source code for com.inverse2.ajaxtoaster.AjaxToasterServlet.java

Source

/**
 *  This class is the servlet that controls the AJAX toaster.
 *
 *  Copyright (C) 2007  Stephen Harding
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *  Please send inquiries to; steve@inverse2.com
 *
 * $Revision: 1.21 $
 *
 * $Log: AjaxToasterServlet.java,v $
 * Revision 1.21  2008/07/24 11:23:36  stevewdh
 * The servlet will not write a blank string to the client as a response.
 * This is to enable an asynchronous custom service to write a response and then monitor for an event.
 *
 * Revision 1.20  2008/07/16 09:32:11  stevewdh
 * Made SMD work from top level URL, so you don't have to know a service name before you can ask for SMD.
 *
 * Revision 1.19  2008/07/11 14:59:43  stevewdh
 * The SMD for STX scripts is now automtically generated.
 *
 * Revision 1.18  2008/07/10 14:27:23  stevewdh
 * The list of users authorised to invoke a service is now properly checked.
 *
 * Revision 1.17  2008/07/08 16:06:24  stevewdh
 * The client may now specify that JSON data is returned via a callback function, using the parameter "callback" on the URL.
 *
 * Revision 1.16  2008/07/01 17:31:53  stevewdh
 * Moved generic logger into com.inverse.util package.
 *
 * Revision 1.15  2008/06/29 13:36:57  stevewdh
 * Changed so that we can setup AjaxToaster to log messages using either; Java, Log4J or buffered HTML logging.
 *
 * Revision 1.14  2008/06/25 14:21:25  stevewdh
 * Changed logging to LOG4J
 *
 * Revision 1.13  2008/06/25 13:36:57  stevewdh
 * Changes to the AjaxToaster.properties file are now detected by the servlet and cause it to refresh itself.
 *
 * Revision 1.12  2008/06/23 09:57:42  stevewdh
 * Updated version number.
 *
 * Revision 1.11  2008/06/20 15:52:23  stevewdh
 * Changed so that web context does not form part of the URI.
 *
 * Revision 1.10  2008/06/20 15:50:52  stevewdh
 * Changes for URI mapping.
 *
 * Revision 1.9  2008/06/19 14:58:10  stevewdh
 * Corrected build version number.
 *
 * Revision 1.8  2008/06/12 10:06:12  stevewdh
 * Added output of the version of XMLToaster we are using.
 *
 * Revision 1.7  2008/06/12 10:02:05  stevewdh
 * Try again with the version stuff!
 *
 * Revision 1.6  2008/06/12 09:59:39  stevewdh
 * Added version number information.
 *
 * Revision 1.5  2008/06/11 20:09:51  stevewdh
 * Added CONTEXTDIR substitution variable to database URL.
 * Had another go at getting the referenced libraries sorted out!
 *
 * Revision 1.4  2008/06/11 14:01:24  stevewdh
 * Implemented a more generic way of getting information about the available service operations.
 * Implemented automatic discovery of STX and XST parameters for the SMD.
 *
 * Revision 1.3  2008/06/04 14:59:08  stevewdh
 * Rationalised the response format stuff a little bit... and added RAW as a response type (text/plain).
 *
 * Revision 1.2  2008/06/03 14:34:59  stevewdh
 * *** empty log message ***
 *
 * Revision 1.1  2008/05/29 09:45:52  stevewdh
 * *** empty log message ***
 *
 * Revision 1.7  2008/04/17 13:48:53  stevewdh
 * Implemented custom services and fixed a couple of problems with the password implementation.
 *
 * Revision 1.6  2008/04/10 15:03:12  stevewdh
 * Refactored a bit and implemented service pool processing.
 *
 * Revision 1.5  2008/03/28 15:31:12  stevewdh
 * Fixed some inconsistencies in the admin functions.
 *
 * Revision 1.4  2008/03/26 18:24:58  stevewdh
 * Services can now be in a heirarchical directory structure.
 *
 * Revision 1.3  2008/03/11 18:03:29  stevewdh
 * Set ATTRIB_TOASTER_INITIALIZED at the end of the init method... this should allow us to set it to false to force refresh...
 *
 * The servlet now searches for the toaster script directory as follows;
 *
 *    use the value of the servlet initialisation param; toaster.script.path
 *    use the web directory + "/toaster" if that exists
 *    use the web directory
 *
 * If the servlet has not been initialised then return error response...
 *
 * Revision 1.2  2008/03/10 15:59:47  stevewdh
 * Standardised headers.
 *
 *
 */

package com.inverse2.ajaxtoaster;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.naming.InitialContext;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.sql.DataSource;

import org.apache.log4j.PropertyConfigurator;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.XML;

import com.inverse2.ajaxtoaster.interfaces.PriviledgedServiceHelper;
import com.inverse2.ajaxtoaster.interfaces.ServiceOperationInterface;
import com.inverse2.ajaxtoaster.interfaces.ServiceScriptHelper;
import com.inverse2.util.Logger;
import com.inverse2.ajaxtoaster.servicedescription.ServiceMappingDescription;
import com.inverse2.ajaxtoaster.urimapping.ServiceMapping;
import com.inverse2.ajaxtoaster.urimapping.ServiceURIMapper;

/**
 *
 * @author Terry B and Steve H
 * @version
 */

public class AjaxToasterServlet extends HttpServlet
        implements ServiceScriptHelper, PriviledgedServiceHelper, Runnable {

    public static final int VERSION_MAJOR = 2;
    public static final int VERSION_MINOR = 0;
    public static final String VERSION_BUILD = "0.beta";

    public static final int DEFAULT_POOL_CHECK_INTERVAL = 30000;

    private static Logger log;

    static final long serialVersionUID = 1L; // not really required

    /** Properties file */
    /* This is the name of the file that specifies the name of the default database connection file. */
    public static String PROPERTIES_FILE = "AjaxToaster.properties";

    /* This is the name of the file that maps URI patterns to particulat AjaxToaster services - used for RESTful services. */
    public static String URI_MAPPINGS_FILE = "AjaxToaster.urimappings";

    public static String SCRIPT_PATH_INIT_PARAM = "toaster.script.path";
    public static String PROP_DEFAULT_JNDI_DATABASE = "default_db_jndi_name";
    public static String PROP_DEFAULT_JDBC_POOL = "default_db_jdbc_pool";
    public static String PROP_RESPONSE_FORMAT = "response_format";
    public static String PROP_LOGIN_CLASS = "LoginClass";
    public static String PROP_TOASTERPOOL_REFRESH_INTERVAL = "ToasterPoolRefreshInterval";
    public static String LOGGING_TYPE = "toaster.logging";

    /** Servlet Context attributes */
    /* These are the names of the global attributes used by the servlet context... */
    public static String ATTRIB_SERVICE_POOL = "ServicePool";
    public static String ATTRIB_LOGGED_IN = "LoggedIn"; // is the user logged in?
    public static String ATTRIB_LOGGED_IN_USER = "LoggedUser"; // user login name
    public static String ATTRIB_JDBC_CONN_POOL = "DBConnectionPool"; // JDBC Database connection pool - used if JNDI lookup to app-server conn pool isn't used.

    /** Request Parameters **/
    public static String PARAM_SCRIPTNAME1 = "service"; // service=xxxx      Specifies which toast script to run
    public static String PARAM_SCRIPTNAME2 = "toast"; // toast=xxxx        Specifies which toast script to run
    public static String PARAM_INPUTXML = "inputxml"; // inputxml=xxxx     XML message to apply against a toaster update script
    public static String PARAM_INPUTJSON = "inputjson"; // inputjson=xxxx    Input JSON message to apply to a toaster update script
    public static String PARAM_RETURNXML = "returnxml"; // returnxml=true    Returns output in XML format instead of the default JSON
    public static String PARAM_RETURNJSON = "returnjson"; // returnjson=true   Returns output in JSON format
    public static String PARAM_RETURNRAW = "returnraw"; // returnraw=true    Returns output in plain text format
    public static String PARAM_CALLBACK = "callback"; // callback=function Specifies that a JSON callback should be returned
    public static String PARAM_SMD = "smd"; // ?smd              Return Service Mapping Description

    /** Private Globals  **/
    private String default_db_jndi_name = null; // holds the name of the DB connection to use by default
    private String default_db_jdbc_name = null; // holds the name of the DB connection to use by default
    private String response_format_prop = null;
    private ServicePool servicePool;
    private ServiceURIMapper serviceMapper;
    private Thread monitorPropertiesThread;
    private boolean monitorProperties;
    private String scriptPath;
    private String propertiesPath;
    private long sleepInterval;
    private long propertiesFileLastChanged;
    private String loggingType;

    public static String getVersion() {
        return (VERSION_MAJOR + "." + VERSION_MINOR + "." + VERSION_BUILD);
    }

    public String getWorkingDirectory() {
        return (scriptPath);
    }

    /**
     * Processes requests from the client for both HTTP <code>GET</code>
     * and <code>POST</code> methods.
     *
     * @param request servlet request
     * @param response servlet response
     */
    protected void processRequest(String requestType, HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String responseFormat = response_format_prop;
        // flags that the user has not set the response format
        boolean defaultResponseFormat = response_format_prop.equals("XML") ? true : false;
        ServiceOperationInterface service = null;
        String callbackFunction = null;

        log.info(">> Start processRequest(" + requestType + ") at " + new Date());

        try {
            ServletContext context = getServletContext();

            String scriptName = request.getParameter(PARAM_SCRIPTNAME1); // look for "service=xxxx"
            String contextPath = "";

            /* If the service parameter is not specified then use the URL to get the service name... */

            if (scriptName == null) {
                scriptName = request.getPathInfo();
                contextPath = request.getContextPath();

                /*
                //Put this in for debugging...
                System.out.println("****** -> pathInfo       [" + request.getPathInfo() + "]");
                System.out.println("****** -> pathTranslated [" + request.getPathTranslated() + "]");
                System.out.println("****** -> contextPath    [" + request.getContextPath() + "]");
                System.out.println("****** -> localAddr      [" + request.getLocalAddr() + "]");
                System.out.println("****** -> localName      [" + request.getLocalName() + "]");
                System.out.println("****** -> requestURI     [" + request.getRequestURI() + "]");//*****
                System.out.println("****** -> servletPath    [" + request.getServletPath() + "]");
                */

                if (scriptName == null) {
                    scriptName = "UNSPECIFIED_SERVICE";
                }
            }

            /* See if the URI is mapped to another service... */
            ServiceMapping serviceMapping;
            serviceMapping = serviceMapper.getURIMapping(""/*contextPath*/, scriptName, requestType);

            if (serviceMapping != null) {
                log.info("Redirect URI to [" + serviceMapping.getServiceName() + "]");

                scriptName = serviceMapping.getServiceName();

                /* If the URI has been mapped then see if the "Accept" header specifies the return type required... */
                String accept = request.getHeader("Accept");

                if (accept.indexOf("text/xml") != -1) {
                    responseFormat = "XML";
                    defaultResponseFormat = false;
                }
                if (accept.indexOf("text/json") != -1) {
                    responseFormat = "JSON";
                    defaultResponseFormat = false;
                }

            }

            if (scriptName.startsWith("/")) {
                scriptName = scriptName.substring(1, scriptName.length());
            }

            /**
             * If "log" service invoked then process it...
             */
            if (scriptName.equals("log")) {
                returnHTMLLog(response);
                return;
            }

            /**
             * If "health" service invoked then process it...
             */
            if (scriptName.equals("health")) {
                returnHealth(response);
                return;
            }

            /* Check for the flag to return XML or JSON objects... */

            if (request.getParameter(PARAM_RETURNXML) != null) {
                println(">> Servlet will return XML object.");
                responseFormat = "XML";
                defaultResponseFormat = false;
            } else if (request.getParameter(PARAM_RETURNJSON) != null) {
                println(">> Servlet will return XML object.");
                responseFormat = "JSON";
                defaultResponseFormat = false;
            } else if (request.getParameter(PARAM_RETURNRAW) != null) {
                println(">> Servlet will return raw text object.");
                responseFormat = "RAW";
                defaultResponseFormat = false;
            }

            /* Check for the callback function parameter... */

            callbackFunction = request.getParameter(PARAM_CALLBACK);

            /**
             * Check to see if the client wants a "Service Mapping Description" (SMD) for the 'service'...
             */

            if (request.getParameter(PARAM_SMD) != null) {

                log.info("Client wants SMD for [" + scriptName + "]");

                try {
                    ServicePool pool = null;
                    Map availableServices = null;
                    ServiceMappingDescription smd = null;
                    ServiceScriptPool serviceScriptPool = null;
                    String serviceScriptName = null;
                    String returnString = null;

                    pool = (ServicePool) context.getAttribute(ATTRIB_SERVICE_POOL);
                    availableServices = pool.getAvailableServices();
                    smd = new ServiceMappingDescription(request.getRequestURL().toString(),
                            request.getRequestURL().toString() + "?smd", null);

                    for (Iterator it = availableServices.values().iterator(); it.hasNext();) {

                        serviceScriptPool = (ServiceScriptPool) it.next();

                        serviceScriptName = serviceScriptPool.getPoolName();

                        /**
                         * If the service script name begins with the passed in script name then add it to the
                         * service mapping description...
                         */

                        log.debug("scriptName = [" + scriptName + "], serviceScriptName = [" + serviceScriptName
                                + "]");

                        if (scriptName.equals("") || serviceScriptName.startsWith(scriptName + "/")
                                || serviceScriptName.equals(scriptName)) {

                            smd.addOperation(serviceScriptName);

                            service = serviceScriptPool.getService();

                            smd.setOperationDescription(service.getScriptDescription());
                            smd.setOperationTransport(service.getHTTPMethods());
                            smd.setOperationEnvelope("URL");
                            smd.setOperationContentType(service.getResponseFormat());
                            smd.setOperationParameters(serviceScriptPool.getServiceParameters());
                            smd.setOperationReturns(serviceScriptPool.getServiceReturns());

                        }

                    }

                    returnString = smd.getSMDJSONString();

                    writeResponse(returnString, "JSONRAW", callbackFunction, response);

                } catch (Exception ex) {
                    log.error("Exception getting SMD: " + ex.toString());
                    ex.printStackTrace();
                }

                return;
            }

            /**
             * Get the service and run it...
             */

            println(">> Client wants to invoke the service [" + scriptName + "]");

            try {
                service = getServiceScript(scriptName);
            } catch (Exception ex) {
                errorResponse(response,
                        "Could not get an instance of the service [" + scriptName + "]: " + ex.toString(),
                        responseFormat, callbackFunction);
                return;
            }

            if (service == null) {
                errorResponse(response, "Service [" + scriptName + "] not found.", responseFormat,
                        callbackFunction);
                return;
            }

            /**
             * If the script exists in the toaster pool then invoke it
             */

            println(">> Checking login required");

            try {
                if (service.getLoginRequired().equals("true")) {

                    HttpSession session = request.getSession(false);
                    Object loggedIn = null;

                    if (session != null) {
                        loggedIn = session.getAttribute(ATTRIB_LOGGED_IN);
                    }

                    log.trace("**** SESSION   = " + session);
                    log.trace("**** Logged In = " + loggedIn);

                    if (session == null || loggedIn == null || loggedIn.equals("true") == false) {
                        errorResponse(response,
                                "The service " + scriptName + " requires you to be logged in to run it.",
                                responseFormat, callbackFunction);
                        freeServiceScript(service);
                        return;
                    }

                    /* Check that the logged in user is authorised to run the service... */

                    String validUsers;
                    String[] validUsersArray;
                    String user;
                    String loggedInUser;
                    boolean validUser;

                    validUsers = service.getValidUsers();
                    validUsersArray = validUsers.split("[,]");

                    loggedInUser = (String) session.getAttribute(ATTRIB_LOGGED_IN_USER);

                    validUser = false;

                    for (int idx = 0; idx < validUsersArray.length; idx++) {
                        user = validUsersArray[idx].trim();
                        if (user.equals("*")) {
                            validUser = true;
                            break;
                        }
                        if (user.equals(loggedInUser)) {
                            validUser = true;
                            break;
                        }
                    }

                    if (validUser == false) {
                        log.error("The user [" + loggedInUser + "] is not authorised to invoke the service ["
                                + scriptName + "]");
                        errorResponse(response, "You are not authorised to invoke the service [" + scriptName + "]",
                                responseFormat, callbackFunction);
                        freeServiceScript(service);
                        return;
                    }

                }
            } catch (Exception ex) {
                errorResponse(response, "Could not check if login required for this service. " + ex.toString(),
                        responseFormat, callbackFunction);
                return;
            }

            boolean scriptInputSet = false;

            /*
             * Go through the set of parameters passed to us and set them up in the service instance...
             */
            for (Enumeration e = request.getParameterNames(); e.hasMoreElements();) {

                String parameterName = (String) e.nextElement();

                if (parameterName.equals(PARAM_SCRIPTNAME1) == true
                        || parameterName.equals(PARAM_SCRIPTNAME2) == true
                        || parameterName.equals(PARAM_RETURNXML) == true
                        || parameterName.equals(PARAM_RETURNJSON) == true
                        || parameterName.equals(PARAM_CALLBACK) == true) {
                    continue;
                }

                String parameterValue = (String) request.getParameter(parameterName);

                if (parameterName.equals(PARAM_INPUTXML) == true) {
                    service.setInputXML(parameterValue);
                    scriptInputSet = true;
                    continue;
                }

                if (parameterName.equals(PARAM_INPUTJSON) == true) {

                    try {
                        // The input object is a JSON object... so convert it into XML...
                        JSONObject json = new JSONObject(parameterValue);

                        service.setInputXML(XML.toString(json));
                        scriptInputSet = true;
                        println("JSON converted to \n" + XML.toString(json));
                    } catch (JSONException ex) {
                        errorResponse(response,
                                "Could not create JSON object." + ex.toString() + ". " + ex.getStackTrace(),
                                responseFormat, callbackFunction);
                        freeServiceScript(service);
                        return;
                    }
                    continue;
                }

                /* Any leftover parameters are query parameters. */
                println("Query Parameter found... Setting " + parameterName + " to " + parameterValue);
                service.setParameter(parameterName, parameterValue);

            } // End of parameters for loop

            /* If there is content in the request then, unless we have already set it, this is the input to the script... */

            if (requestType.equals("POST") && scriptInputSet == false) {

                try {
                    BufferedReader reader = request.getReader();
                    StringBuffer buf = new StringBuffer();
                    String line;
                    String postData;

                    while ((line = reader.readLine()) != null) {
                        buf.append(line);
                    }

                    postData = buf.toString();

                    log.debug("POST DATA: " + postData);

                    if (postData.startsWith("<")) {
                        service.setInputXML(postData);
                        scriptInputSet = true;
                    } else {
                        try {
                            // The input object is a JSON object... so convert it into XML...
                            JSONObject json = new JSONObject(postData);

                            service.setInputXML(XML.toString(json));
                            scriptInputSet = true;
                            log.debug("POST JSON converted to \n" + XML.toString(json));
                        } catch (JSONException ex) {
                            errorResponse(response, "Could not convert POSTed JSON object." + ex.toString() + ". "
                                    + ex.getStackTrace(), responseFormat, callbackFunction);
                            freeServiceScript(service);
                            return;
                        }
                    }

                } catch (Exception ex) {
                    log.warn("Exception getting posted data: " + ex.toString());
                    errorResponse(response, "Could not convert posted data.", responseFormat, callbackFunction);
                    freeServiceScript(service);
                    return;
                }

            }

            /* If the service name has been redirected then set any parameters that where embedded in the URI... */
            if (serviceMapping != null) {
                Properties serviceParameters = serviceMapping.getParameters();
                String paramName;
                String paramValue;
                for (Enumeration<Object> en = serviceParameters.keys(); en.hasMoreElements();) {
                    paramName = (String) en.nextElement();
                    paramValue = (String) serviceParameters.get(paramName);
                    service.setParameter(paramName, paramValue);
                }
            }

            String serviceResultString = null;

            /**
             * Run the service script...
             */

            service.setSessionRequest(request);
            service.setSessionResponse(response);
            service.setCallbackFunction(callbackFunction);

            /* Check if the service has a predefined output format... */
            /* If the user has specified a format then that is used.. */

            String operationResponseFormat;

            operationResponseFormat = service.getResponseFormat();

            if (defaultResponseFormat == true && operationResponseFormat != null
                    && operationResponseFormat.equals("") == false) {
                responseFormat = operationResponseFormat;
            }

            service.setInvokeResponseFormat(responseFormat);

            /* If this is a priviledged operation then pass in a reference to the servlet... */

            String priviledgedOperation = service.getPriviledged();

            if (priviledgedOperation.compareToIgnoreCase("true") == 0
                    || priviledgedOperation.compareToIgnoreCase("yes") == 0
                    || priviledgedOperation.compareToIgnoreCase("y") == 0) {

                service.setPriviledgedHelper(this);
            }

            serviceResultString = service.invokeOperation();

            if (serviceResultString == null) {
                errorResponse(response,
                        "Error invoking the operation.<br><b>" + service.getScriptMessage() + "</b>",
                        responseFormat, callbackFunction);
                freeServiceScript(service);
                return;
            }

            /* Return the results... */

            if (serviceResultString != null && serviceResultString.equals("") == false) {
                writeResponse(serviceResultString, responseFormat, callbackFunction, response);
            }

            println(">> Service script executed successfully.");

            /* Free the service instance... */

            freeServiceScript(service);

        } catch (Exception ex) {
            errorResponse(response, "Exception processing request: " + ex.toString(), responseFormat,
                    callbackFunction);
            ex.printStackTrace();
            try {
                freeServiceScript(service);
            } catch (Exception x) {
                log.warn("Exception freeing a service instance: " + x.toString());
            }
            return;
        }

        println(">> Finished processRequest() at " + new Date());

    } // processRequest()

    private void returnHealth(HttpServletResponse response) throws Exception {

        StringBuffer healthInfo = new StringBuffer();

        healthInfo.append("AjaxToaster version " + getVersion() + "<br>");
        healthInfo.append("Using XMLToaster version " + com.inverse2.xmltoaster.Version.getVersion() + "<br>");
        healthInfo.append("Properties file name [" + propertiesPath + "]<br>");
        healthInfo.append("Script directory [" + scriptPath + "]<br>");
        healthInfo.append("Logging method [" + loggingType + "]<br>");
        healthInfo.append("Sleep interval [" + sleepInterval + "]<br>");
        healthInfo.append("<br>");
        healthInfo.append(serviceMapper.getURIMappings().replaceAll("\\n", "<br>"));
        healthInfo.append("<br>");

        healthInfo.append("List of operations available;<br><br>");

        List<ServiceOperationInfo> serviceList = servicePool.getOperationInfo();
        ServiceOperationInfo operation;

        for (Iterator<ServiceOperationInfo> it = serviceList.iterator(); it.hasNext();) {
            operation = it.next();
            healthInfo.append("-- " + operation.getServiceName() + "/" + operation.getOperationName() + "<br>");
        }

        Runtime rt = Runtime.getRuntime();

        healthInfo.append("<br>JVM: Total Mem [" + rt.totalMemory() + "], Free Mem [" + rt.freeMemory()
                + "], Max Mem [" + rt.maxMemory() + "]<br>");

        writeResponse(healthInfo.toString(), "HTML", null, response);
    }

    private void returnHTMLLog(HttpServletResponse response) throws Exception {
        String logdata = Logger.getHTMLLog();
        writeResponse(logdata, "HTML", null, response);
    }

    /** Handles the HTTP <code>GET</code> method.
      * @param request servlet request
      * @param response servlet response
      */
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest("GET", request, response);
    }

    /** Handles the HTTP <code>POST</code> method.
     * @param request servlet request
     * @param response servlet response
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest("POST", request, response);
    }

    /**
     * Returns a short description of the servlet.
     */
    public String getServletInfo() {
        return ("AjaxToaster Servlet");
    }

    /**
     * Tora! Tora! Tora!
     */
    public void destroy() {

        log.info("Destroy the servlet...");

        /* Stop the script pool... */
        servicePool.stopMonitoring();

        /* Stop the database connection pools... */
        List poolList = getJDCBConnectionPoolList();
        DBConnectionPool pool;

        for (Iterator it = poolList.iterator(); it.hasNext();) {
            pool = (DBConnectionPool) it.next();
            pool.closePool();
        }

        /* Stop monitoring for changes in the properties file... */
        monitorProperties = false;
        try {
            monitorPropertiesThread.interrupt();
        } catch (Exception ex) {
        }

        super.destroy();
    }

    /**
     * Initialise the ToasterServlet - build the required service and database connection pools.
     */
    public void init(ServletConfig config) throws ServletException {

        /* Perform servlet initialisation... */
        super.init(config);

        ServletContext context = getServletContext();
        Properties servletProperties = null;

        try {
            servletProperties = getAjaxToasterProperties();
            loggingType = servletProperties.getProperty(LOGGING_TYPE);
            Logger.setLoggerType(loggingType);
            log = Logger.getLogger(AjaxToasterServlet.class.getName());

            if (loggingType.equals(Logger.LOG4J_LOGGING_STR)) {
                PropertyConfigurator.configureAndWatch(propertiesPath);
            }

        } catch (Exception ex) {
            log = Logger.getLogger(AjaxToasterServlet.class.getName());
            log.error("Exception initialising", ex);
        }

        log.info("AjaxToaster version " + getVersion());
        log.info("Using XMLToaster version " + com.inverse2.xmltoaster.Version.getVersion());

        if (servletProperties == null) {
            println("**** ERROR: Could not cache the AjaxToaster properties file.");
            return;
        }

        // Read response format of output returned to client - XML or JSON.
        response_format_prop = servletProperties.getProperty(PROP_RESPONSE_FORMAT);
        if (response_format_prop != null && response_format_prop.equalsIgnoreCase("JSON")) {
            response_format_prop = "JSON";
        } else {
            response_format_prop = "XML";
        }
        log.info(" Response format >> [" + response_format_prop + "]");

        sleepInterval = getSleepInterval(servletProperties);

        // Create a new service pool

        servicePool = new ServicePool(sleepInterval, this);
        context.setAttribute(ATTRIB_SERVICE_POOL, servicePool);

        try {
            println("**** INFO: CREATING NEW SERVICE POOL - sleepInterval = " + sleepInterval + ", scriptPath="
                    + scriptPath);
            servicePool.initialise(scriptPath);
            servicePool.start();
        } catch (Exception ex) {
            println("**** ERROR: Exception creating AjaxToaster service pool: " + ex.toString());
        }

        /* Initialise the service mapping object... */
        try {
            serviceMapper = new ServiceURIMapper(scriptPath, URI_MAPPINGS_FILE);
            serviceMapper.start();
        } catch (Exception ex) {
            log.error("Exception setting up the Service URI Mapper: " + ex.toString());
            ex.printStackTrace();
        }

        initDBPools(servletProperties, context);

        /* Start a thread that will watch for changes in the servlet's properties file */
        try {
            monitorProperties = true;
            monitorPropertiesThread = new Thread(this);
            monitorPropertiesThread.start();
        } catch (Exception ex) {
            log.warn("Could not start a thread to watch for changes in the properties file: " + ex.toString());
        }

    } // end init()

    private Properties getAjaxToasterProperties() throws Exception {
        return (getAjaxToasterProperties(false));
    }

    private Properties getAjaxToasterProperties(boolean withMessages) throws Exception {

        /* Get a File that represents the directory where the toaster scripts are held... */

        File scriptDirectory = getScriptDirectory();

        if (scriptDirectory == null) {
            throw new Exception("**** ERROR: Could not locate the toaster script directory.");
        }

        if (withMessages) {
            println(">> TOASTER SCRIPT DIRECTORY: [" + scriptDirectory + "]");
        }

        scriptPath = scriptDirectory.toString();
        propertiesPath = scriptDirectory + File.separator + PROPERTIES_FILE;

        // Open the AjaxToaster Servlet properties file
        Properties servletProperties = new Properties();
        File servletPropertiesFile;
        try {
            servletPropertiesFile = new File(propertiesPath);
            propertiesFileLastChanged = servletPropertiesFile.lastModified();
            servletProperties.load(new FileInputStream(servletPropertiesFile));
        } catch (Exception ex) {
            throw new Exception(
                    "ERROR: Exception reading properties file (" + propertiesPath + ") : " + ex.toString());
        }

        return (servletProperties);
    }

    private long getSleepInterval(Properties servletProperties) {

        String sleepIntervalRaw = "";

        sleepInterval = DEFAULT_POOL_CHECK_INTERVAL;

        try {
            sleepIntervalRaw = "0" + servletProperties.getProperty(PROP_TOASTERPOOL_REFRESH_INTERVAL).trim();
            println("sleepInterval==" + sleepIntervalRaw);
            sleepInterval = Long.parseLong(sleepIntervalRaw);
        } catch (Exception ex) {
            println("Exception reading the toaster pool refresh interval: " + ex.toString() + " - defaulting.");
            sleepInterval = DEFAULT_POOL_CHECK_INTERVAL;
        }

        return (sleepInterval);
    }

    /**
     * Perform initialisation necessary to setup AjaxToaster database pools...
     */
    private void initDBPools(Properties servletProperties, ServletContext context) {

        // Read the default JNDI name or connection pool name to use when creating database connections.
        // held in AjaxToaster.properties as the "default_db_jndi_name" property.
        // This is used if there is no properties file with an overriding "db_jndi_name" property supplied for a toaster script.

        default_db_jndi_name = servletProperties.getProperty(PROP_DEFAULT_JNDI_DATABASE);
        default_db_jdbc_name = servletProperties.getProperty(PROP_DEFAULT_JDBC_POOL);

        println("**** Default JDBC pool = " + default_db_jdbc_name);
        println("**** Default JNDI name = " + default_db_jndi_name);

        /**
         *  Create any jdbc connection pools
         *
         * Properties file format for connection pools is :
         *        jdbc.class.[poolId]    = [driver classname]
         *        jdbc.database.[poolId] = [database to connect to]
         *        jdbc.username.[poolId] = [user to connect as]
         *        jdbc.password.[poolId] = [password]
           *
         * ...Where [id] is a unique identifier for the pool. It can be anything as long as it's unique per pool.
         *
         * There is no restriction on the number of connection pools that can be defined.
           */

        for (Enumeration e = servletProperties.propertyNames(); e.hasMoreElements();) {

            String propertyName = (String) e.nextElement();

            println("**** checking property.... " + propertyName);

            if (propertyName.startsWith("jdbc.database.")) {

                String poolId = propertyName.substring(propertyName.lastIndexOf("."));

                poolId = poolId.replaceFirst("\\.", "");

                String driver = servletProperties.getProperty("jdbc.driver." + poolId);
                String url = servletProperties.getProperty("jdbc.database." + poolId);
                String username = servletProperties.getProperty("jdbc.username." + poolId);
                String password = servletProperties.getProperty("jdbc.password." + poolId);

                // Not using a container managed database connection pool.... lets make our own!
                // Create a database connection pool...

                println("**** INFO: JDBC CONNECTION PROPERTIES FOR " + poolId + " - ("
                        + servletProperties.getProperty(propertyName) + ")");
                println("****       driver=" + driver);
                println("****       url=" + url);
                println("****       username=" + username);
                println("****       password=" + password);

                try {
                    String contextDirectory = context.getRealPath("/");
                    DBConnectionPool dbpool = new DBConnectionPool(poolId, driver, url, username, password,
                            contextDirectory);
                    String attr = ATTRIB_JDBC_CONN_POOL + "." + poolId;

                    println("Storing JDBC pool in " + attr);

                    context.setAttribute(attr, dbpool);
                } catch (Exception ex) {
                    println("**** ERROR: Exception creating a database connection pool: " + ex.toString());
                    // continue... some pools might be ok...
                }

            }

        }

    } // end initDBPools()

    /**
     * This method returns a File that points to the location of the toaster scripts...
     */
    private File getScriptDirectory() throws Exception {

        ServletContext context = getServletContext();
        String tmpScriptPath;
        File scriptDirectory;

        /* Check if the servlet initialisation parameter defines the script path... */
        tmpScriptPath = context.getInitParameter(SCRIPT_PATH_INIT_PARAM);

        if (tmpScriptPath != null) {
            scriptDirectory = checkScriptPath(tmpScriptPath);
            if (scriptDirectory != null) {
                return (scriptDirectory);
            }
        }

        /* Get the system path to the web directory... check for the toaster scripts */
        /* under this.                                                               */
        tmpScriptPath = context.getRealPath("/");
        if (tmpScriptPath == null) {
            /* null means that we cannot get the system directory... which probably */
            /* means that the servlet is running from a WAR file.                   */
            throw new Exception("Could not get the directory that the XMLToaster scripts are in.");
        }

        /* If a directory called "services" exists under the web directory.. then use that... */
        String tmpScriptPathWithToaster = tmpScriptPath + File.separator + "services";
        scriptDirectory = checkScriptPath(tmpScriptPathWithToaster);

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

        /* If the "services" directory does not exist then use the web directory... */
        scriptDirectory = checkScriptPath(tmpScriptPath);
        if (scriptDirectory != null) {
            return (scriptDirectory);
        }

        /* We have run out of options... */
        return (null);
    }

    private File checkScriptPath(String path) throws Exception {
        /* Expand the script path... */
        /* Check that the script directory exists and is accessible... */
        File scriptDirectory = new File(path);

        if (scriptDirectory.exists() == false) {
            /* Directory does not exist... */
            throw new Exception("The AjaxToaster script directory [" + path + "] does not exist!");
        }
        if (scriptDirectory.isDirectory() == false) {
            /* The path does not point to a directory... */
            throw new Exception("The AjaxToaster script directory [" + path + "] is not a directory!");
        }
        if (scriptDirectory.canRead() == false) {
            /* Directory is not readable... */
            throw new Exception("The AjaxToaster script directory [" + path + "] is not readable!");
        }

        return (scriptDirectory);
    }

    private void println(String msg) {
        log.info("{" + this.hashCode() + "} " + msg);
    }

    private void respondWithMessage(HttpServletResponse response, String messageType, String description,
            Exception exception, String responseFormat, String callbackFunction)
            throws ServletException, IOException {
        try {
            PrintWriter out = response.getWriter();
            String format = null;
            String callbackFunctionTail = ");";

            if (responseFormat == null) {
                responseFormat = "JSON";
            }

            if (callbackFunction == null) {
                callbackFunction = "";
                callbackFunctionTail = "";
            } else {
                callbackFunction = callbackFunction + "(";
            }

            format = convertResponseFormat(responseFormat);

            response.setContentType(format);
            response.setHeader("Cache-Control", "no-cache, must-revalidate");
            response.setHeader("Expires", "Mon, 26 Jul 1997 05:00:00 GMT");

            if (responseFormat.equals("XML")) {
                // print the error message in XML format

                out.println(
                        callbackFunction + "<" + messageType + "><description>" + description + "</description>");

                if (exception != null) {
                    out.println("<detail>" + exception.toString() + "</detail>");
                } else {
                    out.println("<detail/>");
                }

                out.println("</" + messageType + ">" + callbackFunctionTail);
            } else if (responseFormat.equals("JSON")) {
                // print the error message in JSON format
                out.println(callbackFunction + "{\"" + messageType + "\":{\"detail\":\"" + description
                        + "\",\"description\":\"" + (exception == null ? "" : exception.toString()) + "\"}}"
                        + callbackFunctionTail);
            } else {
                // output raw
                out.println(messageType + ":" + description);
                out.println("Exception: " + exception.toString());
            }

            out.close();
        } catch (Exception ex) {
            log.error("ERROR printing error - " + ex.toString() + ". " + ex.getStackTrace());
        }
    }

    public void errorResponse(HttpServletResponse response, String errString, String responseFormat,
            String callbackFunction) {

        println(errString);

        try {
            respondWithMessage(response, "error", errString, null, responseFormat, callbackFunction);
        } catch (ServletException ex) {
            log.error("ERROR Servlet exception printing error - " + ex.toString() + ". " + ex.getStackTrace());
        } catch (IOException ex) {
            log.error("ERROR IO Exception printing error - " + ex.toString() + ". " + ex.getStackTrace());
        }

        return;
    }

    public void successResponse(HttpServletResponse response, String message, String responseFormat,
            String callbackFunction) {

        println(message);

        try {
            respondWithMessage(response, "success", message, null, responseFormat, callbackFunction);
        } catch (ServletException ex) {
            log.error("ERROR Servlet exception printing error - " + ex.toString() + ". " + ex.getStackTrace());
        } catch (IOException ex) {
            log.error("ERROR IO Exception printing error - " + ex.toString() + ". " + ex.getStackTrace());
        }

        return;
    }

    public void writeResponse(String responseMessage, String responseFormat, String callbackFunction,
            HttpServletResponse response) throws Exception {

        String format;
        String callbackFunctionTail = ");";

        if (responseFormat == null) {
            responseFormat = "JSON";
        }

        if (callbackFunction == null) {
            callbackFunction = "";
            callbackFunctionTail = "";
        } else {
            callbackFunction = callbackFunction + "(";
        }

        format = convertResponseFormat(responseFormat);

        response.setContentType(format);
        response.setHeader("Cache-Control", "no-cache, must-revalidate");
        response.setHeader("Expires", "Mon, 26 Jul 1997 05:00:00 GMT");

        PrintWriter out = response.getWriter();

        if (responseFormat.equals("JSON")) {

            try {
                /* TODO The "force singleton objects to arrays flag should be specified by the script properties... */
                JSONObject json = XML.toJSONObject(responseMessage, // result JSON
                        true, // force singleton objects to be arrays
                        "rows" // array exception list
                );

                println(">> Return JSON: " + json.toString());

                out.println(callbackFunction + json.toString() + callbackFunctionTail);

            } catch (JSONException ex) {
                errorResponse(response, "ERROR - JSON Exception converting XML to JSON object. " + ex.toString()
                        + ". " + ex.getStackTrace(), responseFormat, callbackFunction);
                return;
            }

        } else {
            out.println(callbackFunction + responseMessage + callbackFunctionTail);
            log.debug(">> Return Data: " + responseMessage);
        }

        out.close();

    }

    private String convertResponseFormat(String responseFormat) {

        String convertedFormat;

        if (responseFormat.equals("XML")) {
            convertedFormat = "text/xml;charset=UTF-8";
        } else if (responseFormat.equals("JSON") || responseFormat.equals("JSONRAW")) {
            convertedFormat = "text/json;charset=UTF-8";
        } else if (responseFormat.equals("RAW")) {
            convertedFormat = "text/plain";
        } else if (responseFormat.equals("HTML")) {
            convertedFormat = "text/html";
        } else {
            convertedFormat = "text/plain";
        }

        return (convertedFormat);
    }

    public ServiceOperationInterface getServiceScript(String scriptName) throws Exception {

        ServletContext context = getServletContext();
        ServicePool pool = null;
        ServiceOperationInterface toaster = null;
        Connection connection = null;
        DBConnectionPool dbpool = null;
        String db_jndi_name = null;
        String db_jdbc_poolname = null;

        try {
            pool = (ServicePool) context.getAttribute(ATTRIB_SERVICE_POOL);
        } catch (Exception ex) {
            log.error("Error getting the toaster pool from the servlet context [" + ATTRIB_SERVICE_POOL + "]");
            throw new Exception("ERROR - could not get toast pool from server context [" + ATTRIB_SERVICE_POOL
                    + "]: " + ex.toString(), ex);
        }

        if (pool == null) {
            /* Toaster pool is not set-up - this is very bad...! */
            log.error("The toaster pool does not exists in the servlet context.");
            throw new Exception("The toaster pool does not exist in the servlet context.");
        }

        try {
            toaster = pool.getService(scriptName);
        } catch (Exception ex) {
            log.error("Exception getting a toaster instance: " + ex.toString());
            throw new Exception(
                    "ERROR - could not get toaster instance from pool for " + scriptName + ": " + ex.toString(),
                    ex);
        }

        /**
         * DATABASE CONNECTION...
         *   if the DbJDNI property is set to non null value, or the default connection is a JNDI one,
         *   then try to use JNDI.
         */

        println(">> Starting database connection");

        db_jndi_name = toaster.getDbJNDI(); // get jndi name from the scripts' properties file (if any)
        db_jdbc_poolname = toaster.getJDBCpoolname(); // get jdbc name from the scripts' properties file (if any)

        if (db_jndi_name != null || default_db_jndi_name != null) {

            // JNDI - Container managed connection pool.
            // Lookup the JNDI name for the connection pool and create the database connection.

            if (db_jndi_name == null) {
                db_jndi_name = default_db_jndi_name;
            }

            println("Looking up database connection for JNDI name " + db_jndi_name + "... "
                    + "An exception here probably indicates that the JNDI name or corresponing connection pool isn't setup on your application server.");

            try {
                // The following code for Websphere 6...
                InitialContext ic = new InitialContext();
                DataSource dataSource = null;

                dataSource = (DataSource) ic.lookup(db_jndi_name);
                connection = dataSource.getConnection();
            } catch (Exception ex) {
                log.error("Exception getting JNDI database connection: " + ex.toString());
                throw new Exception("ERROR - getting a connection to the database (using JNDI name " + db_jndi_name
                        + "): " + ex.toString(), ex);
            }

        } else if (db_jdbc_poolname != null || default_db_jdbc_name != null) {

            if (db_jdbc_poolname == null) {
                db_jdbc_poolname = default_db_jdbc_name; // it isn't, so use the default one.
            }

            // find the connection pool in the servlet context
            try {
                String attr = ATTRIB_JDBC_CONN_POOL + "." + db_jdbc_poolname;
                println(">> Trying to get DB connection pool (" + attr + ")");
                dbpool = (DBConnectionPool) context.getAttribute(attr);
            } catch (Exception ex) {
                log.error("Could not get a database connection from the pool: " + ex.toString());
                throw new Exception("ERROR - could not get DB connection pool from server context ("
                        + ATTRIB_JDBC_CONN_POOL + "." + db_jdbc_poolname + ") " + ex.toString(), ex);
            }

            if (dbpool == null) {
                if (toaster != null) {
                    pool.freeService(scriptName, toaster);
                }
                throw new Exception("ERROR - The database connection pool has not been created (dbpool for "
                        + ATTRIB_JDBC_CONN_POOL + "." + db_jdbc_poolname + " is null).");
            }
            try {
                connection = dbpool.getConnection();
            } catch (Exception ex) {
                if (toaster != null) {
                    pool.freeService(scriptName, toaster);
                }
                throw new Exception("ERROR - could not get DB connection from DB connection pool. " + ex.toString(),
                        ex);
            }

        }

        /*
         *  Give the Toaster the database connection we created earlier...
         */
        toaster.setConnection(connection);

        return (toaster);
    }

    public void freeServiceScript(ServiceOperationInterface toaster) throws Exception {

        ServletContext context = getServletContext();
        ServicePool pool = null;
        DBConnectionPool dbpool = null;

        if (toaster != null) {

            Connection connection = toaster.getConnection();

            if (connection != null) {

                String attr = ATTRIB_JDBC_CONN_POOL + "." + toaster.getJDBCpoolname();

                dbpool = (DBConnectionPool) context.getAttribute(attr);

                toaster.setConnection(null);

                if (dbpool != null) {
                    dbpool.freeConnection(connection);
                }

            }

            pool = (ServicePool) context.getAttribute(ATTRIB_SERVICE_POOL);

            pool.freeService(toaster.getScriptName(), toaster);

        }

        return;
    }

    public void errorResponse(String errString, String responseFormat, ServiceOperationInterface service) {
        errorResponse(service.getSessionResponse(), errString, responseFormat, service.getCallbackFunction());
    }

    public void successResponse(String message, String responseFormat, ServiceOperationInterface service) {
        successResponse(service.getSessionResponse(), message, responseFormat, service.getCallbackFunction());
    }

    public void writeResponse(String responseMessage, String responseFormat, ServiceOperationInterface service)
            throws Exception {
        writeResponse(responseMessage, responseFormat, service.getCallbackFunction(), service.getSessionResponse());
    }

    public String getLoggedInUser(ServiceOperationInterface service) {

        String loggedInUser;
        HttpServletRequest request;
        HttpSession session;

        request = service.getSessionRequest();
        session = request.getSession();

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

        loggedInUser = (String) session.getAttribute(ATTRIB_LOGGED_IN_USER);

        return (loggedInUser);
    }

    public void setLoggedInUser(ServiceOperationInterface service, String username) {

        HttpServletRequest request;
        HttpSession session;

        request = service.getSessionRequest();
        session = request.getSession(true);

        session.setAttribute(ATTRIB_LOGGED_IN, "true");
        session.setAttribute(ATTRIB_LOGGED_IN_USER, username);
    }

    public void logout(ServiceOperationInterface service) {
        HttpServletRequest request;
        HttpSession session;

        request = service.getSessionRequest();
        session = request.getSession(true);

        session.setAttribute(ATTRIB_LOGGED_IN, "false");
        session.removeAttribute(ATTRIB_LOGGED_IN_USER);
    }

    public void resetServices(ServiceOperationInterface service) throws Exception {
        // TODO Auto-generated method stub
        throw new Exception("This method is not currently supported.");
    }

    public ServicePool getServicePool() {
        ServletContext context = getServletContext();
        ServicePool pool = null;

        pool = (ServicePool) context.getAttribute(ATTRIB_SERVICE_POOL);

        return (pool);
    }

    public List getJDCBConnectionPoolList() {
        ServletContext context = getServletContext();
        DBConnectionPool pool = null;
        ArrayList poolList = new ArrayList();

        for (Enumeration e = context.getAttributeNames(); e.hasMoreElements();) {
            String attribName = (String) e.nextElement();
            if (attribName.startsWith(ATTRIB_JDBC_CONN_POOL)) {
                pool = (DBConnectionPool) context.getAttribute(attribName);
                if (pool != null) {
                    poolList.add(pool);
                }
            }
        }

        return (poolList);
    }

    /**
     * Monitor the servlet's properties file... and refresh if the file changes...
     */
    public void run() {

        long propertiesFileLastRefreshed;
        Properties servletProperties;

        propertiesFileLastRefreshed = propertiesFileLastChanged;

        while (monitorProperties) {

            try {
                Thread.sleep(sleepInterval);
            } catch (Exception ex) {
                log.warn("Properties monitor thread had exception: " + ex.toString());
                continue;
            }

            try {
                /* Calling this method caches the properties file and gets when it last changed... */
                servletProperties = getAjaxToasterProperties();

                if (propertiesFileLastChanged > propertiesFileLastRefreshed) {
                    log.info("Servlet properties file has changed... will refresh...");

                    /* Refresh the sleep interval... */
                    long sleep;
                    sleep = getSleepInterval(servletProperties);
                    servicePool.setSleepInterval(sleep);

                    /* Refresh the JDBC database connection pools... */
                    initDBPools(servletProperties, getServletContext());

                    /* Remember when we last refreshed... */
                    propertiesFileLastRefreshed = propertiesFileLastChanged;
                }
            } catch (Exception ex) {
                log.error("Exception monitoring the properties file.", ex);
            }

        } // end while monitoring

    } // end run()
}