001: /*
002: * $Id: ScriptAction.java 471754 2006-11-06 14:55:09Z husted $
003: *
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021: package org.apache.struts.scripting;
022:
023: // util imports:
024: import java.util.ArrayList;
025: import java.util.Enumeration;
026: import java.util.Hashtable;
027: import java.util.List;
028: import java.util.Locale;
029: import java.util.Map;
030: import java.util.Properties;
031: import java.util.StringTokenizer;
032:
033: // io imports:
034: import java.io.File;
035: import java.io.FileReader;
036: import java.io.IOException;
037: import java.io.InputStream;
038:
039: // logging imports:
040: import org.apache.commons.logging.Log;
041: import org.apache.commons.logging.LogFactory;
042:
043: // struts imports:
044: import org.apache.struts.action.Action;
045: import org.apache.struts.action.ActionErrors;
046: import org.apache.struts.action.ActionForm;
047: import org.apache.struts.action.ActionForward;
048: import org.apache.struts.action.ActionMapping;
049: import org.apache.struts.action.ActionMessages;
050:
051: // misc imports:
052: import javax.servlet.ServletContext;
053: import javax.servlet.http.HttpServletRequest;
054: import javax.servlet.http.HttpServletResponse;
055: import javax.servlet.http.HttpSession;
056: import org.apache.bsf.BSFException;
057: import org.apache.bsf.BSFManager;
058: import org.apache.bsf.util.IOUtils;
059:
060: /**
061: * This Action uses scripts to perform its action. The scripting framework is
062: * Apache's Bean Scripting Framework which allows the scripts to be written
063: * many of the popular scripting languages including JavaScript, Perl, Python,
064: * and even VBA. <br />
065: * <br />
066: * To determine what script will be executed, the "parameter" attribute of the
067: * action mapping should contain the name of the script relative to the web
068: * application root directory (i.e. http://server/app). <br />
069: * <br />
070: * Before the script completes, the next ActionForward needs to be specified.
071: * This can be done one of two ways:
072: * <ol>
073: * <li> Set <code>struts.forwardName</code> to the name of the forward</li>
074: *
075: * <li> Set <code>struts.forward</code> to the actual ActionForward object
076: * </li>
077: * </ol>
078: * A number of pre-defined variables are available to the script:
079: * <ul>
080: * <li> <code>request</code> - The HTTP request</li>
081: * <li> <code>response</code> - The HTTP response</li>
082: * <li> <code>session</code> - The session</li>
083: * <li> <code>application</code> - The servlet context</li>
084: * <li> <code>struts</code> - A grouping of all Struts-related objects</li>
085: *
086: * <li> <code>log</code> - A logging instance</li>
087: * </ul>
088: * You can add your own variables by creating a BSFManagerFilter and
089: * configuring it in struts-scripting.properties:
090: * <ul>
091: * <li> <code>struts-scripting.filters.FILTER_NAME.class=FILTER_CLASS</code>
092: * - The class implementing BSFManagerFilter where FILTER_NAME is the name
093: * you are calling the filter.</li>
094: * <li> <code>
095: * struts-scripting.filters.FILTER_NAME.PROPERTY_NAME=PROPERTY_VALUE
096: * </code> - A property to be used by the filter.</li>
097: * </ul>
098: * <br />
099: * <br />
100: * To use other scripting engines other than BeanShell, create a file called
101: * <code>struts-scripting.properties</code> and add two properties for each
102: * engine:
103: * <ul>
104: * <li> <code>struts-scripting.engine.ENGINE_NAME.class</code> - The class of
105: * the BSF engine where ENGINE_NAME is the name you are calling the engine.
106: * </li>
107: * <li> <code>struts-scripting.engine.ENGINE_NAME.extensions</code> - A
108: * comma-delimited list of file extensions that will be used to identify the
109: * engine to use to execute the script.</li>
110: * </ul>
111: * This code was originally based off code from JPublish, but has since been
112: * almost completely rewritten.
113: */
114: public class ScriptAction extends Action {
115:
116: /** The logging instance. */
117: protected static final Log LOG = LogFactory
118: .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 = ScriptAction.class.getClassLoader()
139: .getResourceAsStream(PROPS_PATH);
140: if (in == null) {
141: in = ScriptAction.class.getClassLoader()
142: .getResourceAsStream("/struts-bsf.properties");
143: if (in != null) {
144: LOG.warn("The struts-bsf.properties file has been "
145: + "deprecated. Please use "
146: + "struts-scripting.properties instead.");
147: } else {
148: LOG
149: .warn("struts-scripting.properties not found, using "
150: + "default engine mappings.");
151: }
152: }
153:
154: if (in != null) {
155: props.load(in);
156: }
157: } catch (Exception ex) {
158: LOG
159: .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
167: .indexOf('.', pos));
168: String cls = props.getProperty(name);
169: String ext = props.getProperty(ENGINE_BASE + type
170: + ".extensions", "");
171: String[] exts = split(ext, ",");
172: if (LOG.isInfoEnabled()) {
173: LOG.info("Loading BSF engine name:" + type
174: + " class:" + cls + " ext:" + ext);
175: }
176: BSFManager.registerScriptingEngine(type, cls, exts);
177: }
178: }
179: filters = loadFilters(props);
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, HttpServletRequest request,
194: HttpServletResponse response) throws Exception {
195:
196: BSFManager bsfManager = new BSFManager();
197:
198: String scriptName = null;
199: try {
200: scriptName = parseScriptName(mapping.getParameter(),
201: bsfManager);
202: } catch (Exception ex) {
203: LOG.error("Unable to parse " + mapping.getParameter(), ex);
204: throw new Exception("Unable to parse "
205: + 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,
231: HttpSession.class);
232: }
233:
234: bsfManager.declareBean("application", application,
235: ServletContext.class);
236:
237: bsfManager.declareBean("log", LOG, Log.class);
238: StrutsInfo struts = new StrutsInfo(this , mapping, form,
239: getResources(request));
240: bsfManager.declareBean("struts", struts, StrutsInfo.class);
241:
242: for (int x = 0; x < filters.length; x++) {
243: filters[x].apply(bsfManager);
244: }
245:
246: bsfManager.exec(script.lang, script.file.getCanonicalPath(), 0,
247: 0, script.string);
248:
249: ActionForward af = struts.getForward();
250: return af;
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],
285: String.class);
286: if (LOG.isDebugEnabled()) {
287: LOG.debug("Registering param " + param[0]
288: + " with value " + param[1]);
289: }
290: }
291: } else {
292: if (LOG.isDebugEnabled()) {
293: LOG.debug("No query string:" + parsed.length);
294: }
295: }
296: }
297: return name;
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 = BSFManager
316: .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,
341: ex);
342: } finally {
343: if (reader != null) {
344: try {
345: reader.close();
346: } catch (IOException ex) {
347: LOG.debug(ex, ex);
348: }
349: }
350: }
351: }
352: }
353:
354: return script;
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
374: .newInstance();
375: f.init(type, props);
376: list.add(f);
377: if (LOG.isInfoEnabled()) {
378: LOG.info("Loaded " + type + " filter: " + claz);
379: }
380: } catch (Exception ex) {
381: LOG.error("Unable to load " + type + " filter: "
382: + claz);
383: }
384: }
385: }
386: BSFManagerFilter[] filters = new BSFManagerFilter[list.size()];
387: filters = (BSFManagerFilter[]) list.toArray(filters);
388: return filters;
389: }
390:
391: /**
392: * Splits a line with the given delimiter.
393: *
394: *@param line The line to split
395: *@param delimiter The string to split with
396: *@return An array of substrings
397: */
398: protected static String[] split(String line, String delimiter) {
399: if (line == null || "".equals(line)) {
400: return new String[] {};
401: }
402:
403: List lst = new ArrayList();
404: for (Enumeration e = new StringTokenizer(line, delimiter); e
405: .hasMoreElements();) {
406: lst.add(e.nextElement());
407: }
408: String[] ret = new String[lst.size()];
409: return (String[]) lst.toArray(ret);
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: * Checks to see if the request is cancelled.
426: *
427: *@param req The request object
428: *@return True if cancelled
429: */
430: public boolean isCancelled(HttpServletRequest req) {
431: return super .isCancelled(req);
432: }
433:
434: /**
435: * Checks to see if the token is valid.
436: *
437: *@param req The request object
438: *@return True if valid
439: */
440: public boolean isTokenValid(HttpServletRequest req) {
441: return super .isTokenValid(req);
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: * Gets the locale.
455: *
456: *@param req The request object
457: *@return The locale value
458: */
459: public Locale getLocale(HttpServletRequest req) {
460: return super .getLocale(req);
461: }
462:
463: /**
464: * Saves the messages to the request.
465: *
466: *@param req The request object
467: *@param mes The action messages
468: */
469: public void saveMessages(HttpServletRequest req, ActionMessages mes) {
470: super .saveMessages(req, mes);
471: }
472:
473: /**
474: * Saves the errors to the request.
475: *
476: *@param req The request object
477: *@param errs The action errors
478: *@deprecated Use saveErrors(HttpServletRequest, ActionMessages) instead.
479: * This will be removed after Struts 1.2.
480: */
481: public void saveErrors(HttpServletRequest req, ActionErrors errs) {
482: super .saveErrors(req, errs);
483: }
484:
485: /** Represents a saved script. */
486: class Script {
487:
488: /** The script file. */
489: public File file;
490:
491: /** The language the script is in. */
492: public String lang = null;
493:
494: /** The time when the script was last used. */
495: public long timeLastLoaded = 0;
496:
497: /** The contents of the script file. */
498: public String string = null;
499: }
500: }
|