/*
 * $Id: ScriptAction.java 471754 2006-11-06 14:55:09Z husted $
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.struts.scripting;

// util imports:
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;

// io imports:
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;

// logging imports:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

// struts imports:
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionErrors;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionMessages;

// misc imports:
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.bsf.BSFException;
import org.apache.bsf.BSFManager;
import org.apache.bsf.util.IOUtils;


/**
 *  This Action uses scripts to perform its action. The scripting framework is
 *  Apache's Bean Scripting Framework which allows the scripts to be written
 *  many of the popular scripting languages including JavaScript, Perl, Python,
 *  and even VBA. <br />
 *  <br />
 *  To determine what script will be executed, the "parameter" attribute of the
 *  action mapping should contain the name of the script relative to the web
 *  application root directory (i.e. http://server/app). <br />
 *  <br />
 *  Before the script completes, the next ActionForward needs to be specified.
 *  This can be done one of two ways:
 *  <ol>
 *    <li> Set <code>struts.forwardName</code> to the name of the forward</li>
 *
 *    <li> Set <code>struts.forward</code> to the actual ActionForward object
 *    </li>
 *  </ol>
 *  A number of pre-defined variables are available to the script:
 *  <ul>
 *    <li> <code>request</code> - The HTTP request</li>
 *    <li> <code>response</code> - The HTTP response</li>
 *    <li> <code>session</code> - The session</li>
 *    <li> <code>application</code> - The servlet context</li>
 *    <li> <code>struts</code> - A grouping of all Struts-related objects</li>
 *
 *    <li> <code>log</code> - A logging instance</li>
 *  </ul>
 *  You can add your own variables by creating a BSFManagerFilter and
 *  configuring it in struts-scripting.properties:
 *  <ul>
 *    <li> <code>struts-scripting.filters.FILTER_NAME.class=FILTER_CLASS</code>
 *    - The class implementing BSFManagerFilter where FILTER_NAME is the name
 *    you are calling the filter.</li>
 *    <li> <code>
 *     struts-scripting.filters.FILTER_NAME.PROPERTY_NAME=PROPERTY_VALUE
 *      </code> - A property to be used by the filter.</li>
 *  </ul>
 *  <br />
 *  <br />
 *  To use other scripting engines other than BeanShell, create a file called
 *  <code>struts-scripting.properties</code> and add two properties for each
 *  engine:
 *  <ul>
 *    <li> <code>struts-scripting.engine.ENGINE_NAME.class</code> - The class of
 *    the BSF engine where ENGINE_NAME is the name you are calling the engine.
 *    </li>
 *    <li> <code>struts-scripting.engine.ENGINE_NAME.extensions</code> - A
 *    comma-delimited list of file extensions that will be used to identify the
 *    engine to use to execute the script.</li>
 *  </ul>
 *  This code was originally based off code from JPublish, but has since been
 *  almost completely rewritten.
 */
public class ScriptAction extends Action {

    /**  The logging instance. */
    protected static final Log LOG = LogFactory.getLog(ScriptAction.class);

    /**  The default path to the properties file. */
    protected static final String PROPS_PATH = "/struts-scripting.properties";

    /**  The base property for alternate BSF engines. */
    protected static final String ENGINE_BASE = "struts-scripting.engine.";

    /**  The base property for classes that put new variables in the context. */
    protected static final String FILTERS_BASE = "struts-scripting.filters.";

    /**  A list of initialized filters. */
    private static BSFManagerFilter[] filters = null;

    /**  Holds the "compiled" scripts and their information. */
    private Map scripts = new Hashtable();

    static {
        Properties props = new Properties();
        try {
            InputStream in =
                    ScriptAction.class.getClassLoader()
                    .getResourceAsStream(PROPS_PATH);
            if (in == null) {
                in =
                        ScriptAction.class.getClassLoader().getResourceAsStream(
                        "/struts-bsf.properties");
                if (in != null) {
                    LOG.warn("The struts-bsf.properties file has been "
                            + "deprecated.  Please use "
                            + "struts-scripting.properties instead.");
                } else {
                    LOG.warn("struts-scripting.properties not found, using "
                             + "default engine mappings.");
                }
            }

            if (in != null) {
                props.load(in);
            }
        } catch (Exception ex) {
            LOG.warn("Unable to load struts-scripting.properties, using "
                     + " default engine mappings.");
        }
        int pos = ENGINE_BASE.length();
        for (Enumeration e = props.propertyNames(); e.hasMoreElements();) {
            String name = (String) e.nextElement();
            if (name.startsWith(ENGINE_BASE) && name.endsWith(".class")) {
                String type = name.substring(pos, name.indexOf('.', pos));
                String cls = props.getProperty(name);
                String ext = props.getProperty(ENGINE_BASE + type
                         + ".extensions", "");
                String[] exts = split(ext, ",");
                if (LOG.isInfoEnabled()) {
                    LOG.info("Loading BSF engine name:" + type + " class:"
                             + cls + " ext:" + ext);
                }
                BSFManager.registerScriptingEngine(type, cls, exts);
            }
        }
        filters = loadFilters(props);
    }


    /**
     *  Executes the script.
     *
     *@param  mapping        The action mapping
     *@param  form           The action form
     *@param  request        The request object
     *@param  response       The response object
     *@return                The action forward
     *@exception  Exception  If something goes wrong
     */
    public ActionForward execute(ActionMapping mapping,
            ActionForm form,
            HttpServletRequest request,
            HttpServletResponse response)
             throws Exception {

        BSFManager bsfManager = new BSFManager();

        String scriptName = null;
        try {
            scriptName = parseScriptName(mapping.getParameter(), bsfManager);
        } catch (Exception ex) {
            LOG.error("Unable to parse " + mapping.getParameter(), ex);
            throw new Exception("Unable to parse " + mapping.getParameter());
        }
        if (scriptName == null) {
            LOG.error("No script specified in the parameter attribute");
            throw new Exception("No script specified");
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("Executing script: " + scriptName);
        }

        HttpSession session = request.getSession();
        ServletContext application = getServlet().getServletContext();

        Script script = loadScript(scriptName, application);

        bsfManager.declareBean("request", request,
                HttpServletRequest.class);

        bsfManager.declareBean("response", response,
                HttpServletResponse.class);

        if (session == null) {
            LOG.debug("HTTP session is null");
        } else {
            bsfManager.declareBean("session", session, HttpSession.class);
        }

        bsfManager.declareBean("application", application,
                ServletContext.class);

        bsfManager.declareBean("log", LOG, Log.class);
        StrutsInfo struts = new StrutsInfo(this, mapping, form,
                getResources(request));
        bsfManager.declareBean("struts", struts, StrutsInfo.class);

        for (int x = 0; x < filters.length; x++) {
            filters[x].apply(bsfManager);
        }

        bsfManager.exec(script.lang, script.file.getCanonicalPath(), 0, 0,
                script.string);

        ActionForward af = struts.getForward();
        return af;
    }


    /**
     *  Parses the script name and puts any url parameters in the context.
     *
     *@param  url            The script url consisting of a path and optional
     *      parameters
     *@param  manager        The BSF manager to declare new parameters in
     *@return                The name of the script to execute
     *@exception  Exception  If something goes wrong
     */
    protected String parseScriptName(String url, BSFManager manager)
             throws Exception {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Parsing " + url);
        }
        String name = null;
        if (url != null) {
            String[] parsed = split(url, "?");
            name = parsed[0];
            if (parsed.length == 2) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Found a query string");
                }
                String[] args = split(parsed[1], "&");
                for (int x = 0; x < args.length; x++) {
                    String[] param = split(args[x], "=");
                    Object o = manager.lookupBean(param[0]);
                    if (o != null) {
                        LOG.warn("BSF variable " + param[0]
                                 + " already exists");
                        param[0] = "_" + param[0];
                    }
                    manager.declareBean(param[0], param[1], String.class);
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Registering param " + param[0]
                                 + " with value " + param[1]);
                    }
                }
            } else {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("No query string:" + parsed.length);
                }
            }
        }
        return name;
    }


    /**
     *  Loads the script from cache if possible. Reloads if the script has been
     *  recently modified.
     *
     *@param  name     The name of the script
     *@param  context  The servlet context
     *@return          The script object
     */
    protected Script loadScript(String name, ServletContext context) {

        Script script = (Script) scripts.get(name);
        if (script == null) {
            script = new Script();
            script.file = new File(context.getRealPath(name));
            try {
                script.lang =
                        BSFManager.getLangFromFilename(script.file.getName());
            } catch (BSFException ex) {
                LOG.warn(ex, ex);
            }
        }

        boolean reloadScript = false;
        long scriptLastModified = script.file.lastModified();
        if (scriptLastModified > script.timeLastLoaded) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Loading updated or new script: "
                        + script.file.getName());
            }
            reloadScript = true;
        }

        if (reloadScript || script.string == null) {
            synchronized (this) {
                script.timeLastLoaded = System.currentTimeMillis();
                FileReader reader = null;
                try {
                    reader = new FileReader(script.file);
                    script.string = IOUtils.getStringFromReader(reader);
                } catch (IOException ex) {
                    LOG.error("Unable to load script: " + script.file, ex);
                } finally {
                    if (reader != null) {
                        try {
                            reader.close();
                        } catch (IOException ex) {
                            LOG.debug(ex, ex);
                        }
                    }
                }
            }
        }

        return script;
    }


    /**
     *  Loads and initializes the filters.
     *
     *@param  props  The properties defining the filters
     *@return        An array of the loaded filters
     */
    protected static BSFManagerFilter[] loadFilters(Properties props) {
        ArrayList list = new ArrayList();
        for (Enumeration e = props.propertyNames(); e.hasMoreElements();) {
            String prop = (String) e.nextElement();
            if (prop.startsWith(FILTERS_BASE) && prop.endsWith("class")) {
                String type = prop.substring(FILTERS_BASE.length(),
                        prop.indexOf(".", FILTERS_BASE.length()));
                String claz = props.getProperty(prop);
                try {
                    Class cls = Class.forName(claz);
                    BSFManagerFilter f = (BSFManagerFilter) cls.newInstance();
                    f.init(type, props);
                    list.add(f);
                    if (LOG.isInfoEnabled()) {
                        LOG.info("Loaded " + type + " filter: " + claz);
                    }
                } catch (Exception ex) {
                    LOG.error("Unable to load " + type + " filter: " + claz);
                }
            }
        }
        BSFManagerFilter[] filters = new BSFManagerFilter[list.size()];
        filters = (BSFManagerFilter[]) list.toArray(filters);
        return filters;
    }


    /**
     *  Splits a line with the given delimiter.
     *
     *@param  line       The line to split
     *@param  delimiter  The string to split with
     *@return            An array of substrings
     */
    protected static String[] split(String line, String delimiter) {
        if (line == null || "".equals(line)) {
            return new String[]{};
        }

        List lst = new ArrayList();
        for (Enumeration e = new StringTokenizer(line, delimiter);
            e.hasMoreElements();) {
            lst.add(e.nextElement());
        }
        String[] ret = new String[lst.size()];
        return (String[]) lst.toArray(ret);
    }


    // These methods seem necessary as some scripting engines are not able to
    // access Action's protected methods.  Ugly? yes... any suggestions?

    /**
     *  Saves a token.
     *
     *@param  req  The request object
     */
    public void saveToken(HttpServletRequest req) {
        super.saveToken(req);
    }


    /**
     *  Checks to see if the request is cancelled.
     *
     *@param  req  The request object
     *@return      True if cancelled
     */
    public boolean isCancelled(HttpServletRequest req) {
        return super.isCancelled(req);
    }


    /**
     *  Checks to see if the token is valid.
     *
     *@param  req  The request object
     *@return      True if valid
     */
    public boolean isTokenValid(HttpServletRequest req) {
        return super.isTokenValid(req);
    }


    /**
     *  Resets the token.
     *
     *@param  req  The request object
     */
    public void resetToken(HttpServletRequest req) {
        super.resetToken(req);
    }


    /**
     *  Gets the locale.
     *
     *@param  req  The request object
     *@return      The locale value
     */
    public Locale getLocale(HttpServletRequest req) {
        return super.getLocale(req);
    }


    /**
     *  Saves the messages to the request.
     *
     *@param  req  The request object
     *@param  mes  The action messages
     */
    public void saveMessages(HttpServletRequest req, ActionMessages mes) {
        super.saveMessages(req, mes);
    }


    /**
     *  Saves the errors to the request.
     *
     *@param  req    The request object
     *@param  errs   The action errors
     *@deprecated    Use saveErrors(HttpServletRequest, ActionMessages) instead.
     *      This will be removed after Struts 1.2.
     */
    public void saveErrors(HttpServletRequest req, ActionErrors errs) {
        super.saveErrors(req, errs);
    }


    /**  Represents a saved script. */
    class Script {

        /**  The script file. */
        public File file;

        /**  The language the script is in. */
        public String lang = null;

        /**  The time when the script was last used. */
        public long timeLastLoaded = 0;

        /**  The contents of the script file. */
        public String string = null;
    }
}
