1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.struts.scripting;
19
20
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
31 import java.io.File;
32 import java.io.FileReader;
33 import java.io.IOException;
34 import java.io.InputStream;
35
36
37 import org.apache.commons.logging.Log;
38 import org.apache.commons.logging.LogFactory;
39
40
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
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
410
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