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 & 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 }