View Javadoc

1   package org.tinyjee.maven.dim.extensions;
2   
3   import org.apache.maven.doxia.logging.Log;
4   import org.codehaus.plexus.util.FileUtils;
5   import org.tinyjee.maven.dim.spi.Globals;
6   import org.tinyjee.maven.dim.spi.UrlFetcher;
7   import org.tinyjee.maven.dim.utils.AbstractAliasHandler;
8   
9   import java.io.File;
10  import java.io.PrintWriter;
11  import java.io.StringWriter;
12  import java.net.URL;
13  import java.util.HashMap;
14  import java.util.Map;
15  import java.util.regex.Matcher;
16  import java.util.regex.Pattern;
17  
18  import static org.tinyjee.maven.dim.IncludeMacroSignature.PARAM_SOURCE_CONTENT;
19  import static org.tinyjee.maven.dim.spi.ResourceResolver.findSource;
20  
21  /**
22   * ScriptInvoker is a 'source-class' compatible helper class providing a runtime environment that allows to load and execute
23   * script code using the JSR-223 interface (scripting for java).
24   * <p/>
25   * Scripts can provide input for velocity templates or they can produce content directly. When invoked, this extension first
26   * populates all given macro parameters within the script context, evaluates the script and copies any changes or newly allocated
27   * variables back into the original parameters map.
28   * When an additional {@code source} was specified these resulting parameters are available in the template via {@code $paramName}.
29   * <br/>
30   * Any text output that is produced by scripts is set into "{@code source-content}" and is displayed when <b>no</b> {@code source} is given.
31   * <p/>
32   * The following global variables are exposed in the script context, in addition to the standard template parameters:
33   * <ul>
34   * <li>"{@code globals}" points to an instance of "{@link Globals}" which allows to access base path, path resolution logic,
35   * loading &amp; attaching content, etc.</li>
36   * <li>"{@code scriptName}" is a string that is filled with the absolute path to the script file.</li>
37   * </ul>
38   * <p/>
39   * This extension is only useful when JSR-223 is available. Starting from Java 6, JavaScript is the only script engine
40   * that is available by default. Adding other script engines to the dependency set of module or site-plugin enables scripting
41   * languages like: Groovy, Ruby, Python and others that offer integration with JSR-223.
42   * <p/>
43   * <b>Usage:</b><div><code>
44   * %{include|source=template.vm|source-class=org.tinyjee.maven.dim.extensions.ScriptInvoker|script=my-script.js}
45   * <br/>
46   * %{include|source-class=org.tinyjee.maven.dim.extensions.ScriptInvoker|script=my-script.js}
47   * </code></div>
48   * or when using the alias:<div><code>
49   * %{include|source=template.vm|source-script=my-script.js}
50   * <br/>
51   * %{include|source-script=my-script.js}
52   * </code></div>
53   * <p/>
54   * <b>Notes:</b>
55   * <ul>
56   * <li>Under normal circumstances it is preferable to create your own 'source-class' compatible helper class using Java as
57   * it adds unit testing and compiler checks. Scripting however, when combined with "{@code site:run}" allows to create content with
58   * scripts very quickly as no additional compilation step is needed to see live changes to the site.</li>
59   * <li>Scripts run in a request scope with global bindings (global variables) being shared amongst executions and script engines.
60   * Whether this has an effect on the scripts depends on the implementation of the script interpreter and the language itself.</li>
61   * </ul>
62   *
63   * @author Juergen_Kellerer, 2011-10-15
64   */
65  public class ScriptInvoker extends HashMap<String, Object> {
66  
67  	private static final long serialVersionUID = 8955658194210732920L;
68  
69  	private static final Pattern EVAL_PREFIX_PATTERN = Pattern.compile("^eval\\[([^\\[\\]\\s]+)\\]:(.+)$");
70  
71  	/**
72  	 * Implements the "{@link org.tinyjee.maven.dim.extensions.ScriptInvoker#PARAM_ALIAS}" alias functionality.
73  	 */
74  	public static class AliasHandler extends AbstractAliasHandler {
75  		/**
76  		 * Constructs the handler (Note: Is called by {@link org.tinyjee.maven.dim.spi.RequestParameterTransformer#TRANSFORMERS}).
77  		 */
78  		public AliasHandler() {
79  			super(PARAM_ALIAS, PARAM_SCRIPT, ScriptInvoker.class.getName());
80  		}
81  	}
82  
83  	/**
84  	 * Defines a shortcut for the request properties 'source-class' and 'script'.
85  	 * <p/>
86  	 * If this parameter is present inside the request parameters list, the system will effectively behave as if
87  	 * 'source-class' and 'script' where set separately.
88  	 */
89  	public static final String PARAM_ALIAS = "source-script";
90  
91  	/**
92  	 * Sets the URL or local path to the script file to load and evaluate.
93  	 * <p/>
94  	 * The specified file is resolved in exactly the same way as when using the parameter "<code>source</code>" (see Macro reference).
95  	 * <p/>
96  	 * <i>Notes:</i><ul>
97  	 * <li>In addition to setting a file path, the parameter can be used to define an inline script snippet using the syntax
98  	 * "{@code eval[type]:script...}".
99  	 * E.g. for javascript this looks like {@code %{include|source-script=eval[js]:print("Hello World");}}.</li>
100 	 * <li>When no further source is specified, any text output produced by the script (e.g. via {@code print "some text"})
101 	 * is included into the current page. STDERR is always sent to both locations, the build logs and the page. Unhandled script errors
102 	 * will break the build.</li>
103 	 * <li>As velocity template processing is applied at a later stage, the script output is processed by velocity unless the parameter
104 	 * "{@code source-is-template}" is set to <i>false</i> or another "{@code source}" was specified. Thus a script can dynamically
105 	 * generate a velocity template and provide the parameters for the template context (though this might be kind of a overkill).</li>
106 	 * </ul>
107 	 */
108 	public static final String PARAM_SCRIPT = "script";
109 
110 	/**
111 	 * Defines the invocation interface to enable pluging a executor.
112 	 */
113 	interface ScriptExecutor {
114 		void execute(Map<String, Object> context, String scriptExtension, String scriptContent, String scriptName) throws Exception;
115 	}
116 
117 	private static ScriptExecutor executor;
118 
119 	private static synchronized ScriptExecutor getExecutor() {
120 		if (executor == null) {
121 			try {
122 				final String executorClass = System.getProperty("dim.script.invoker.engine.class",
123 						"org.tinyjee.maven.dim.extensions.ScriptExecutorJSR223");
124 				executor = (ScriptExecutor) Class.forName(executorClass).newInstance();
125 			} catch (Throwable e) {
126 				StringWriter buffer = new StringWriter();
127 				e.printStackTrace(new PrintWriter(buffer));
128 				final String errorMessage = e.toString(), stackTrace = buffer.toString();
129 
130 				executor = new ScriptExecutor() {
131 					public void execute(Map<String, Object> context,
132 					                    String scriptExtension, String scriptContent, String scriptName) throws Exception {
133 						final Log log = Globals.getLog();
134 						log.error("Failed executing script '" + scriptName + "':\n----\n" + scriptContent + "\n----\n, " +
135 								"caused by: " + errorMessage + "\n" + stackTrace);
136 						context.put(PARAM_SOURCE_CONTENT, "Failed executing script '" + scriptName + "', caused by: " + errorMessage);
137 					}
138 				};
139 			}
140 		}
141 		return executor;
142 	}
143 
144 	/**
145 	 * Constructs a new Script Invoker.
146 	 *
147 	 * @param baseDir       the base dir to of the maven module.
148 	 * @param requestParams the request params of the macro call.
149 	 * @throws Exception In case of loading or transforming fails.
150 	 */
151 	public ScriptInvoker(File baseDir, Map<String, Object> requestParams) throws Exception {
152 
153 		putAll(requestParams);
154 
155 		final String script = (String) get(PARAM_SCRIPT);
156 		if (script == null) throw new IllegalArgumentException("No script (parameter '" + PARAM_SCRIPT + "') was defined.");
157 
158 		final String scriptExtension, scriptContent, scriptName;
159 
160 		final Matcher evalMatcher = EVAL_PREFIX_PATTERN.matcher(script);
161 		if (evalMatcher.matches()) {
162 			scriptExtension = evalMatcher.group(1);
163 			scriptContent = evalMatcher.group(2);
164 			scriptName = baseDir + File.separator + "local." + scriptExtension;
165 		} else {
166 			final URL scriptURL = findSource(baseDir, script);
167 			scriptContent = UrlFetcher.readerToString(UrlFetcher.getReadableSource(scriptURL), true);
168 			scriptName = "file".equals(scriptURL.getProtocol()) ? new File(scriptURL.toURI()).toString() : scriptURL.toString();
169 			scriptExtension = FileUtils.extension(scriptName);
170 		}
171 
172 		put("globals", Globals.getInstance());
173 		put("scriptName", scriptName);
174 
175 		getExecutor().execute(this, scriptExtension, scriptContent, scriptName);
176 	}
177 }