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 }