View Javadoc

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