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