View Javadoc

    1   /*
    2    * $Id: ScriptAction.java 357368 2005-12-17 19:17:51Z mrdon $
    3    *
    4    * Copyright 2000-2004 The Apache Software Foundation.
    5    *
    6    * Licensed under the Apache License, Version 2.0 (the "License");
    7    * you may not use this file except in compliance with the License.
    8    * You may obtain a copy of the License at
    9    *
    10   *      http://www.apache.org/licenses/LICENSE-2.0
    11   *
    12   * Unless required by applicable law or agreed to in writing, software
    13   * distributed under the License is distributed on an "AS IS" BASIS,
    14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15   * See the License for the specific language governing permissions and
    16   * limitations under the License.
    17   */
    18  package org.apache.struts.scripting;
    19
    20  // util imports:
    21  import java.util.ArrayList;
    22  import java.util.Enumeration;
    23  import java.util.Hashtable;
    24  import java.util.List;
    25  import java.util.Locale;
    26  import java.util.Map;
    27  import java.util.Properties;
    28  import java.util.StringTokenizer;
    29
    30  // io imports:
    31  import java.io.File;
    32  import java.io.FileReader;
    33  import java.io.IOException;
    34  import java.io.InputStream;
    35
    36  // logging imports:
    37  import org.apache.commons.logging.Log;
    38  import org.apache.commons.logging.LogFactory;
    39
    40  // struts imports:
    41  import org.apache.struts.action.Action;
    42  import org.apache.struts.action.ActionErrors;
    43  import org.apache.struts.action.ActionForm;
    44  import org.apache.struts.action.ActionForward;
    45  import org.apache.struts.action.ActionMapping;
    46  import org.apache.struts.action.ActionMessages;
    47
    48  // misc imports:
    49  import javax.servlet.ServletContext;
    50  import javax.servlet.http.HttpServletRequest;
    51  import javax.servlet.http.HttpServletResponse;
    52  import javax.servlet.http.HttpSession;
    53  import org.apache.bsf.BSFException;
    54  import org.apache.bsf.BSFManager;
    55  import org.apache.bsf.util.IOUtils;
    56
    57
    58  /***
    59   *  This Action uses scripts to perform its action. The scripting framework is
    60   *  Apache's Bean Scripting Framework which allows the scripts to be written
    61   *  many of the popular scripting languages including JavaScript, Perl, Python,
    62   *  and even VBA. <br />
    63   *  <br />
    64   *  To determine what script will be executed, the "parameter" attribute of the
    65   *  action mapping should contain the name of the script relative to the web
    66   *  application root directory (i.e. http://server/app). <br />
    67   *  <br />
    68   *  Before the script completes, the next ActionForward needs to be specified.
    69   *  This can be done one of two ways:
    70   *  <ol>
    71   *    <li> Set <code>struts.forwardName</code> to the name of the forward</li>
    72   *
    73   *    <li> Set <code>struts.forward</code> to the actual ActionForward object
    74   *    </li>
    75   *  </ol>
    76   *  A number of pre-defined variables are available to the script:
    77   *  <ul>
    78   *    <li> <code>request</code> - The HTTP request</li>
    79   *    <li> <code>response</code> - The HTTP response</li>
    80   *    <li> <code>session</code> - The session</li>
    81   *    <li> <code>application</code> - The servlet context</li>
    82   *    <li> <code>struts</code> - A grouping of all Struts-related objects</li>
    83   *
    84   *    <li> <code>log</code> - A logging instance</li>
    85   *  </ul>
    86   *  You can add your own variables by creating a BSFManagerFilter and
    87   *  configuring it in struts-scripting.properties:
    88   *  <ul>
    89   *    <li> <code>struts-scripting.filters.FILTER_NAME.class=FILTER_CLASS</code>
    90   *    - The class implementing BSFManagerFilter where FILTER_NAME is the name
    91   *    you are calling the filter.</li>
    92   *    <li> <code>
    93   *     struts-scripting.filters.FILTER_NAME.PROPERTY_NAME=PROPERTY_VALUE
    94   *      </code> - A property to be used by the filter.</li>
    95   *  </ul>
    96   *  <br />
    97   *  <br />
    98   *  To use other scripting engines other than BeanShell, create a file called
    99   *  <code>struts-scripting.properties</code> and add two properties for each
    100  *  engine:
    101  *  <ul>
    102  *    <li> <code>struts-scripting.engine.ENGINE_NAME.class</code> - The class of
    103  *    the BSF engine where ENGINE_NAME is the name you are calling the engine.
    104  *    </li>
    105  *    <li> <code>struts-scripting.engine.ENGINE_NAME.extensions</code> - A
    106  *    comma-delimited list of file extensions that will be used to identify the
    107  *    engine to use to execute the script.</li>
    108  *  </ul>
    109  *  This code was originally based off code from JPublish, but has since been
    110  *  almost completely rewritten.
    111  */
    112 public class ScriptAction extends Action {
    113
    114     /***  The logging instance. */
    115     protected static final Log LOG = LogFactory.getLog(ScriptAction.class);
    116
    117     /***  The default path to the properties file. */
    118     protected static final String PROPS_PATH = "/struts-scripting.properties";
    119
    120     /***  The base property for alternate BSF engines. */
    121     protected static final String ENGINE_BASE = "struts-scripting.engine.";
    122
    123     /***  The base property for classes that put new variables in the context. */
    124     protected static final String FILTERS_BASE = "struts-scripting.filters.";
    125
    126     /***  A list of initialized filters. */
    127     private static BSFManagerFilter[] filters = null;
    128
    129     /***  Holds the "compiled" scripts and their information. */
    130     private Map scripts = new Hashtable();
    131
    132     static {
    133         Properties props = new Properties();
    134         try {
    135             InputStream in =
    136                     ScriptAction.class.getClassLoader()
    137                     .getResourceAsStream(PROPS_PATH);
    138             if (in == null) {
    139                 in =
    140                         ScriptAction.class.getClassLoader().getResourceAsStream(
    141                         "/struts-bsf.properties");
    142                 if (in != null) {
    143                     LOG.warn("The struts-bsf.properties file has been "
    144                             + "deprecated.  Please use "
    145                             + "struts-scripting.properties instead.");
    146                 } else {
    147                     LOG.warn("struts-scripting.properties not found, using "
    148                              + "default engine mappings.");
    149                 }
    150             }
    151
    152             if (in != null) {
    153                 props.load(in);
    154             }
    155         } catch (Exception ex) {
    156             LOG.warn("Unable to load struts-scripting.properties, using "
    157                      + " default engine mappings.");
    158         }
    159         int pos = ENGINE_BASE.length();
    160         for (Enumeration e = props.propertyNames(); e.hasMoreElements();) {
    161             String name = (String) e.nextElement();
    162             if (name.startsWith(ENGINE_BASE) && name.endsWith(".class")) {
    163                 String type = name.substring(pos, name.indexOf('.', pos));
    164                 String cls = props.getProperty(name);
    165                 String ext = props.getProperty(ENGINE_BASE + type
    166                          + ".extensions", "");
    167                 String[] exts = split(ext, ",");
    168                 if (LOG.isInfoEnabled()) {
    169                     LOG.info("Loading BSF engine name:" + type + " class:"
    170                              + cls + " ext:" + ext);
    171                 }
    172                 BSFManager.registerScriptingEngine(type, cls, exts);
    173             }
    174         }
    175         filters = loadFilters(props);
    176     }
    177
    178
    179     /***
    180      *  Executes the script.
    181      *
    182      *@param  mapping        The action mapping
    183      *@param  form           The action form
    184      *@param  request        The request object
    185      *@param  response       The response object
    186      *@return                The action forward
    187      *@exception  Exception  If something goes wrong
    188      */
    189     public ActionForward execute(ActionMapping mapping,
    190             ActionForm form,
    191             HttpServletRequest request,
    192             HttpServletResponse response)
    193              throws Exception {
    194
    195         BSFManager bsfManager = new BSFManager();
    196
    197         String scriptName = null;
    198         try {
    199             scriptName = parseScriptName(mapping.getParameter(), bsfManager);
    200         } catch (Exception ex) {
    201             LOG.error("Unable to parse " + mapping.getParameter(), ex);
    202             throw new Exception("Unable to parse " + mapping.getParameter());
    203         }
    204         if (scriptName == null) {
    205             LOG.error("No script specified in the parameter attribute");
    206             throw new Exception("No script specified");
    207         }
    208
    209         if (LOG.isDebugEnabled()) {
    210             LOG.debug("Executing script: " + scriptName);
    211         }
    212
    213         HttpSession session = request.getSession();
    214         ServletContext application = getServlet().getServletContext();
    215
    216         Script script = loadScript(scriptName, application);
    217
    218         bsfManager.declareBean("request", request,
    219                 HttpServletRequest.class);
    220
    221         bsfManager.declareBean("response", response,
    222                 HttpServletResponse.class);
    223
    224         if (session == null) {
    225             LOG.debug("HTTP session is null");
    226         } else {
    227             bsfManager.declareBean("session", session, HttpSession.class);
    228         }
    229
    230         bsfManager.declareBean("application", application,
    231                 ServletContext.class);
    232
    233         bsfManager.declareBean("log", LOG, Log.class);
    234         StrutsInfo struts = new StrutsInfo(this, mapping, form,
    235                 getResources(request));
    236         bsfManager.declareBean("struts", struts, StrutsInfo.class);
    237
    238         for (int x = 0; x < filters.length; x++) {
    239             filters[x].apply(bsfManager);
    240         }
    241
    242         bsfManager.exec(script.lang, script.file.getCanonicalPath(), 0, 0,
    243                 script.string);
    244
    245         ActionForward af = struts.getForward();
    246         return af;
    247     }
    248
    249
    250     /***
    251      *  Parses the script name and puts any url parameters in the context.
    252      *
    253      *@param  url            The script url consisting of a path and optional
    254      *      parameters
    255      *@param  manager        The BSF manager to declare new parameters in
    256      *@return                The name of the script to execute
    257      *@exception  Exception  If something goes wrong
    258      */
    259     protected String parseScriptName(String url, BSFManager manager)
    260              throws Exception {
    261         if (LOG.isDebugEnabled()) {
    262             LOG.debug("Parsing " + url);
    263         }
    264         String name = null;
    265         if (url != null) {
    266             String[] parsed = split(url, "?");
    267             name = parsed[0];
    268             if (parsed.length == 2) {
    269                 if (LOG.isDebugEnabled()) {
    270                     LOG.debug("Found a query string");
    271                 }
    272                 String[] args = split(parsed[1], "&");
    273                 for (int x = 0; x < args.length; x++) {
    274                     String[] param = split(args[x], "=");
    275                     Object o = manager.lookupBean(param[0]);
    276                     if (o != null) {
    277                         LOG.warn("BSF variable " + param[0]
    278                                  + " already exists");
    279                         param[0] = "_" + param[0];
    280                     }
    281                     manager.declareBean(param[0], param[1], String.class);
    282                     if (LOG.isDebugEnabled()) {
    283                         LOG.debug("Registering param " + param[0]
    284                                  + " with value " + param[1]);
    285                     }
    286                 }
    287             } else {
    288                 if (LOG.isDebugEnabled()) {
    289                     LOG.debug("No query string:" + parsed.length);
    290                 }
    291             }
    292         }
    293         return name;
    294     }
    295
    296
    297     /***
    298      *  Loads the script from cache if possible. Reloads if the script has been
    299      *  recently modified.
    300      *
    301      *@param  name     The name of the script
    302      *@param  context  The servlet context
    303      *@return          The script object
    304      */
    305     protected Script loadScript(String name, ServletContext context) {
    306
    307         Script script = (Script) scripts.get(name);
    308         if (script == null) {
    309             script = new Script();
    310             script.file = new File(context.getRealPath(name));
    311             try {
    312                 script.lang =
    313                         BSFManager.getLangFromFilename(script.file.getName());
    314             } catch (BSFException ex) {
    315                 LOG.warn(ex, ex);
    316             }
    317         }
    318
    319         boolean reloadScript = false;
    320         long scriptLastModified = script.file.lastModified();
    321         if (scriptLastModified > script.timeLastLoaded) {
    322             if (LOG.isDebugEnabled()) {
    323                 LOG.debug("Loading updated or new script: "
    324                         + script.file.getName());
    325             }
    326             reloadScript = true;
    327         }
    328
    329         if (reloadScript || script.string == null) {
    330             synchronized (this) {
    331                 script.timeLastLoaded = System.currentTimeMillis();
    332                 FileReader reader = null;
    333                 try {
    334                     reader = new FileReader(script.file);
    335                     script.string = IOUtils.getStringFromReader(reader);
    336                 } catch (IOException ex) {
    337                     LOG.error("Unable to load script: " + script.file, ex);
    338                 } finally {
    339                     if (reader != null) {
    340                         try {
    341                             reader.close();
    342                         } catch (IOException ex) {
    343                             LOG.debug(ex, ex);
    344                         }
    345                     }
    346                 }
    347             }
    348         }
    349
    350         return script;
    351     }
    352
    353
    354     /***
    355      *  Loads and initializes the filters.
    356      *
    357      *@param  props  The properties defining the filters
    358      *@return        An array of the loaded filters
    359      */
    360     protected static BSFManagerFilter[] loadFilters(Properties props) {
    361         ArrayList list = new ArrayList();
    362         for (Enumeration e = props.propertyNames(); e.hasMoreElements();) {
    363             String prop = (String) e.nextElement();
    364             if (prop.startsWith(FILTERS_BASE) && prop.endsWith("class")) {
    365                 String type = prop.substring(FILTERS_BASE.length(),
    366                         prop.indexOf(".", FILTERS_BASE.length()));
    367                 String claz = props.getProperty(prop);
    368                 try {
    369                     Class cls = Class.forName(claz);
    370                     BSFManagerFilter f = (BSFManagerFilter) cls.newInstance();
    371                     f.init(type, props);
    372                     list.add(f);
    373                     if (LOG.isInfoEnabled()) {
    374                         LOG.info("Loaded " + type + " filter: " + claz);
    375                     }
    376                 } catch (Exception ex) {
    377                     LOG.error("Unable to load " + type + " filter: " + claz);
    378                 }
    379             }
    380         }
    381         BSFManagerFilter[] filters = new BSFManagerFilter[list.size()];
    382         filters = (BSFManagerFilter[]) list.toArray(filters);
    383         return filters;
    384     }
    385
    386
    387     /***
    388      *  Splits a line with the given delimiter.
    389      *
    390      *@param  line       The line to split
    391      *@param  delimiter  The string to split with
    392      *@return            An array of substrings
    393      */
    394     protected static String[] split(String line, String delimiter) {
    395         if (line == null || "".equals(line)) {
    396             return new String[]{};
    397         }
    398
    399         List lst = new ArrayList();
    400         for (Enumeration e = new StringTokenizer(line, delimiter);
    401             e.hasMoreElements();) {
    402             lst.add(e.nextElement());
    403         }
    404         String[] ret = new String[lst.size()];
    405         return (String[]) lst.toArray(ret);
    406     }
    407
    408
    409     // These methods seem necessary as some scripting engines are not able to
    410     // access Action's protected methods.  Ugly? yes... any suggestions?
    411
    412     /***
    413      *  Saves a token.
    414      *
    415      *@param  req  The request object
    416      */
    417     public void saveToken(HttpServletRequest req) {
    418         super.saveToken(req);
    419     }
    420
    421
    422     /***
    423      *  Checks to see if the request is cancelled.
    424      *
    425      *@param  req  The request object
    426      *@return      True if cancelled
    427      */
    428     public boolean isCancelled(HttpServletRequest req) {
    429         return super.isCancelled(req);
    430     }
    431
    432
    433     /***
    434      *  Checks to see if the token is valid.
    435      *
    436      *@param  req  The request object
    437      *@return      True if valid
    438      */
    439     public boolean isTokenValid(HttpServletRequest req) {
    440         return super.isTokenValid(req);
    441     }
    442
    443
    444     /***
    445      *  Resets the token.
    446      *
    447      *@param  req  The request object
    448      */
    449     public void resetToken(HttpServletRequest req) {
    450         super.resetToken(req);
    451     }
    452
    453
    454     /***
    455      *  Gets the locale.
    456      *
    457      *@param  req  The request object
    458      *@return      The locale value
    459      */
    460     public Locale getLocale(HttpServletRequest req) {
    461         return super.getLocale(req);
    462     }
    463
    464
    465     /***
    466      *  Saves the messages to the request.
    467      *
    468      *@param  req  The request object
    469      *@param  mes  The action messages
    470      */
    471     public void saveMessages(HttpServletRequest req, ActionMessages mes) {
    472         super.saveMessages(req, mes);
    473     }
    474
    475
    476     /***
    477      *  Saves the errors to the request.
    478      *
    479      *@param  req    The request object
    480      *@param  errs   The action errors
    481      *@deprecated    Use saveErrors(HttpServletRequest, ActionMessages) instead.
    482      *      This will be removed after Struts 1.2.
    483      */
    484     public void saveErrors(HttpServletRequest req, ActionErrors errs) {
    485         super.saveErrors(req, errs);
    486     }
    487
    488
    489     /***  Represents a saved script. */
    490     class Script {
    491
    492         /***  The script file. */
    493         public File file;
    494
    495         /***  The language the script is in. */
    496         public String lang = null;
    497
    498         /***  The time when the script was last used. */
    499         public long timeLastLoaded = 0;
    500
    501         /***  The contents of the script file. */
    502         public String string = null;
    503     }
    504 }
    505