/******************************************************************************* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. *******************************************************************************/ package org.apache.ofbiz.base.util; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.ServiceLoader; import java.util.Set; import javax.script.Bindings; import javax.script.Compilable; import javax.script.CompiledScript; import javax.script.Invocable; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import javax.script.SimpleBindings; import javax.script.SimpleScriptContext; import org.apache.ofbiz.base.location.FlexibleLocation; import org.apache.ofbiz.base.util.cache.UtilCache; import org.codehaus.groovy.runtime.InvokerHelper; /** * Scripting utility methods. This is a facade class that is used to connect OFBiz to JSR-223 scripting engines. * <p><b>Important:</b> To avoid a lot of <code>Map</code> copying, all methods that accept a context * <code>Map</code> argument will pass that <code>Map</code> directly to the scripting engine. Any variables that * are declared or modified in the script will affect the original <code>Map</code>. Client code that wishes to preserve * the state of the <code>Map</code> argument should pass a copy of the <code>Map</code>.</p> * */ public final class ScriptUtil { public static final String module = ScriptUtil.class.getName(); /** The screen widget context map bindings key. */ public static final String WIDGET_CONTEXT_KEY = "widget"; /** The service/servlet/request parameters map bindings key. */ public static final String PARAMETERS_KEY = "parameters"; /** The result map bindings key. */ public static final String RESULT_KEY = "result"; /** The <code>ScriptHelper</code> key. */ public static final String SCRIPT_HELPER_KEY = "ofbiz"; private static final UtilCache<String, CompiledScript> parsedScripts = UtilCache.createUtilCache("script.ParsedScripts", 0, 0, false); private static final Object[] EMPTY_ARGS = {}; private static ScriptHelperFactory helperFactory = null; /** A set of script names - derived from the JSR-223 scripting engines. */ public static final Set<String> SCRIPT_NAMES; static { Set<String> writableScriptNames = new HashSet<String>(); ScriptEngineManager manager = new ScriptEngineManager(); List<ScriptEngineFactory> engines = manager.getEngineFactories(); if (engines.isEmpty()) { Debug.logInfo("No scripting engines were found.", module); } else { Debug.logInfo("The following " + engines.size() + " scripting engines were found:", module); for (ScriptEngineFactory engine : engines) { Debug.logInfo("Engine name: " + engine.getEngineName(), module); Debug.logInfo(" Version: " + engine.getEngineVersion(), module); Debug.logInfo(" Language: " + engine.getLanguageName(), module); List<String> extensions = engine.getExtensions(); if (extensions.size() > 0) { Debug.logInfo(" Engine supports the following extensions:", module); for (String e : extensions) { Debug.logInfo(" " + e, module); } } List<String> shortNames = engine.getNames(); if (shortNames.size() > 0) { Debug.logInfo(" Engine has the following short names:", module); for (String name : engine.getNames()) { writableScriptNames.add(name); Debug.logInfo(" " + name, module); } } } } SCRIPT_NAMES = Collections.unmodifiableSet(writableScriptNames); Iterator<ScriptHelperFactory> iter = ServiceLoader.load(ScriptHelperFactory.class).iterator(); if (iter.hasNext()) { helperFactory = iter.next(); if (Debug.verboseOn()) { Debug.logVerbose("ScriptHelper factory set to " + helperFactory.getClass().getName(), module); } } else { Debug.logWarning("ScriptHelper factory not found", module); } } /** * Returns a compiled script. * * @param filePath Script path and file name. * @return The compiled script, or <code>null</code> if the script engine does not support compilation. * @throws IllegalArgumentException * @throws ScriptException * @throws IOException */ public static CompiledScript compileScriptFile(String filePath) throws ScriptException, IOException { Assert.notNull("filePath", filePath); CompiledScript script = parsedScripts.get(filePath); if (script == null) { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByExtension(getFileExtension(filePath)); if (engine == null) { throw new IllegalArgumentException("The script type is not supported for location: " + filePath); } try { Compilable compilableEngine = (Compilable) engine; URL scriptUrl = FlexibleLocation.resolveLocation(filePath); BufferedReader reader = new BufferedReader(new InputStreamReader(scriptUrl.openStream())); script = compilableEngine.compile(reader); if (Debug.verboseOn()) { Debug.logVerbose("Compiled script " + filePath + " using engine " + engine.getClass().getName(), module); } } catch (ClassCastException e) { if (Debug.verboseOn()) { Debug.logVerbose("Script engine " + engine.getClass().getName() + " does not implement Compilable", module); } } if (script != null) { parsedScripts.putIfAbsent(filePath, script); } } return script; } /** * Returns a compiled script. * * @param language * @param script * @return The compiled script, or <code>null</code> if the script engine does not support compilation. * @throws IllegalArgumentException * @throws ScriptException */ public static CompiledScript compileScriptString(String language, String script) throws ScriptException { Assert.notNull("language", language, "script", script); String cacheKey = language.concat("://").concat(script); CompiledScript compiledScript = parsedScripts.get(cacheKey); if (compiledScript == null) { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName(language); if (engine == null) { throw new IllegalArgumentException("The script type is not supported for language: " + language); } try { Compilable compilableEngine = (Compilable) engine; compiledScript = compilableEngine.compile(script); if (Debug.verboseOn()) { Debug.logVerbose("Compiled script [" + script + "] using engine " + engine.getClass().getName(), module); } } catch (ClassCastException e) { if (Debug.verboseOn()) { Debug.logVerbose("Script engine " + engine.getClass().getName() + " does not implement Compilable", module); } } if (compiledScript != null) { parsedScripts.putIfAbsent(cacheKey, compiledScript); } } return compiledScript; } /** * Returns a <code>ScriptContext</code> that contains the members of <code>context</code>. * <p>If a <code>CompiledScript</code> instance is to be shared by multiple threads, then * each thread must create its own <code>ScriptContext</code> and pass it to the * <code>CompiledScript</code> eval method.</p> * * @param context * @return */ public static ScriptContext createScriptContext(Map<String, Object> context) { Assert.notNull("context", context); Map<String, Object> localContext = new HashMap<String, Object>(context); localContext.put(WIDGET_CONTEXT_KEY, context); localContext.put("context", context); ScriptContext scriptContext = new SimpleScriptContext(); ScriptHelper helper = createScriptHelper(scriptContext); if (helper != null) { localContext.put(SCRIPT_HELPER_KEY, helper); } Bindings bindings = new SimpleBindings(localContext); scriptContext.setBindings(bindings, ScriptContext.ENGINE_SCOPE); return scriptContext; } /** * Returns a <code>ScriptContext</code> that contains the members of <code>context</code>. * <p>If a <code>CompiledScript</code> instance is to be shared by multiple threads, then * each thread must create its own <code>ScriptContext</code> and pass it to the * <code>CompiledScript</code> eval method.</p> * * @param context * @param protectedKeys * @return */ public static ScriptContext createScriptContext(Map<String, Object> context, Set<String> protectedKeys) { Assert.notNull("context", context, "protectedKeys", protectedKeys); Map<String, Object> localContext = new HashMap<String, Object>(context); localContext.put(WIDGET_CONTEXT_KEY, context); localContext.put("context", context); ScriptContext scriptContext = new SimpleScriptContext(); Bindings bindings = new ProtectedBindings(localContext, Collections.unmodifiableSet(protectedKeys)); scriptContext.setBindings(bindings, ScriptContext.ENGINE_SCOPE); ScriptHelper helper = createScriptHelper(scriptContext); if (helper != null) { localContext.put(SCRIPT_HELPER_KEY, helper); } return scriptContext; } public static ScriptHelper createScriptHelper(ScriptContext context) { if (helperFactory != null) { return helperFactory.getInstance(context); } return null; } /** * Executes a script <code>String</code> and returns the result. * * @param language * @param script * @param scriptClass * @param context * @return The script result. * @throws Exception */ public static Object evaluate(String language, String script, Class<?> scriptClass, Map<String, Object> context) throws Exception { Assert.notNull("context", context); if (scriptClass != null) { return InvokerHelper.createScript(scriptClass, GroovyUtil.getBinding(context)).run(); } try { CompiledScript compiledScript = compileScriptString(language, script); if (compiledScript != null) { return executeScript(compiledScript, null, createScriptContext(context), null); } ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName(language); if (engine == null) { throw new IllegalArgumentException("The script type is not supported for language: " + language); } if (Debug.verboseOn()) { Debug.logVerbose("Begin processing script [" + script + "] using engine " + engine.getClass().getName(), module); } ScriptContext scriptContext = createScriptContext(context); return engine.eval(script, scriptContext); } catch (Exception e) { String errMsg = "Error running " + language + " script [" + script + "]: " + e.toString(); Debug.logWarning(e, errMsg, module); throw new IllegalArgumentException(errMsg); } } /** * Executes a compiled script and returns the result. * * @param script Compiled script. * @param functionName Optional function or method to invoke. * @param scriptContext Script execution context. * @return The script result. * @throws IllegalArgumentException */ public static Object executeScript(CompiledScript script, String functionName, ScriptContext scriptContext, Object[] args) throws ScriptException, NoSuchMethodException { Assert.notNull("script", script, "scriptContext", scriptContext); Object result = script.eval(scriptContext); if (UtilValidate.isNotEmpty(functionName)) { if (Debug.verboseOn()) { Debug.logVerbose("Invoking function/method " + functionName, module); } ScriptEngine engine = script.getEngine(); try { Invocable invocableEngine = (Invocable) engine; result = invocableEngine.invokeFunction(functionName, args == null ? EMPTY_ARGS : args); } catch (ClassCastException e) { throw new ScriptException("Script engine " + engine.getClass().getName() + " does not support function/method invocations"); } } return result; } /** * Executes the script at the specified location and returns the result. * * @param filePath Script path and file name. * @param functionName Optional function or method to invoke. * @param context Script execution context. * @return The script result. * @throws IllegalArgumentException */ public static Object executeScript(String filePath, String functionName, Map<String, Object> context) { return executeScript(filePath, functionName, context, new Object[] { context }); } /** * Executes the script at the specified location and returns the result. * * @param filePath Script path and file name. * @param functionName Optional function or method to invoke. * @param context Script execution context. * @param args Function/method arguments. * @return The script result. * @throws IllegalArgumentException */ public static Object executeScript(String filePath, String functionName, Map<String, Object> context, Object[] args) { try { // Enabled to run Groovy data preparation scripts using GroovyUtil rather than the generic JSR223 that doesn't support debug mode // and does not return a stack trace with line number on error if (filePath.endsWith(".groovy")) { return GroovyUtil.runScriptAtLocation(filePath, functionName, context); } return executeScript(filePath, functionName, createScriptContext(context), args); } catch (Exception e) { String errMsg = "Error running script at location [" + filePath + "]: " + e.toString(); Debug.logWarning(e, errMsg, module); throw new IllegalArgumentException(errMsg, e); } } /** * Executes the script at the specified location and returns the result. * * @param filePath Script path and file name. * @param functionName Optional function or method to invoke. * @param scriptContext Script execution context. * @param args Function/method arguments. * @return The script result. * @throws ScriptException * @throws NoSuchMethodException * @throws IOException * @throws IllegalArgumentException */ public static Object executeScript(String filePath, String functionName, ScriptContext scriptContext, Object[] args) throws ScriptException, NoSuchMethodException, IOException { Assert.notNull("filePath", filePath, "scriptContext", scriptContext); scriptContext.setAttribute(ScriptEngine.FILENAME, filePath, ScriptContext.ENGINE_SCOPE); if (functionName == null) { // The Rhino script engine will not work when invoking a function on a compiled script. // The test for null can be removed when the engine is fixed. CompiledScript script = compileScriptFile(filePath); if (script != null) { return executeScript(script, functionName, scriptContext, args); } } String fileExtension = getFileExtension(filePath); ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByExtension(fileExtension); if (engine == null) { throw new IllegalArgumentException("The script type is not supported for location: " + filePath); } if (Debug.verboseOn()) { Debug.logVerbose("Begin processing script [" + filePath + "] using engine " + engine.getClass().getName(), module); } engine.setContext(scriptContext); URL scriptUrl = FlexibleLocation.resolveLocation(filePath); FileReader reader = new FileReader(new File(scriptUrl.getFile())); Object result = engine.eval(reader); if (UtilValidate.isNotEmpty(functionName)) { try { Invocable invocableEngine = (Invocable) engine; result = invocableEngine.invokeFunction(functionName, args == null ? EMPTY_ARGS : args); } catch (ClassCastException e) { throw new ScriptException("Script engine " + engine.getClass().getName() + " does not support function/method invocations"); } } return result; } private static String getFileExtension(String filePath) { int pos = filePath.lastIndexOf("."); if (pos == -1) { throw new IllegalArgumentException("Extension missing in script file name: " + filePath); } return filePath.substring(pos + 1); } public static Class<?> parseScript(String language, String script) { Class<?> scriptClass = null; if ("groovy".equals(language)) { try { scriptClass = GroovyUtil.parseClass(script); } catch (IOException e) { Debug.logError(e, module); return null; } } return scriptClass; } private ScriptUtil() {} private static final class ProtectedBindings implements Bindings { private final Map<String, Object> bindings; private final Set<String> protectedKeys; private ProtectedBindings(Map<String, Object> bindings, Set<String> protectedKeys) { this.bindings = bindings; this.protectedKeys = protectedKeys; } public void clear() { for (String key : bindings.keySet()) { if (!protectedKeys.contains(key)) { bindings.remove(key); } } } public boolean containsKey(Object key) { return bindings.containsKey(key); } public boolean containsValue(Object value) { return bindings.containsValue(value); } public Set<java.util.Map.Entry<String, Object>> entrySet() { return bindings.entrySet(); } @Override public boolean equals(Object o) { return bindings.equals(o); } public Object get(Object key) { return bindings.get(key); } @Override public int hashCode() { return bindings.hashCode(); } public boolean isEmpty() { return bindings.isEmpty(); } public Set<String> keySet() { return bindings.keySet(); } public Object put(String key, Object value) { Assert.notNull("key", key); if (protectedKeys.contains(key)) { UnsupportedOperationException e = new UnsupportedOperationException("Variable " + key + " is read-only"); Debug.logWarning(e, module); throw e; } return bindings.put(key, value); } public void putAll(Map<? extends String, ? extends Object> map) { for (Map.Entry<? extends String, ? extends Object> entry : map.entrySet()) { Assert.notNull("key", entry.getKey()); if (!protectedKeys.contains(entry.getKey())) { bindings.put(entry.getKey(), entry.getValue()); } } } public Object remove(Object key) { if (protectedKeys.contains(key)) { UnsupportedOperationException e = new UnsupportedOperationException("Variable " + key + " is read-only"); Debug.logWarning(e, module); throw e; } return bindings.remove(key); } public int size() { return bindings.size(); } public Collection<Object> values() { return bindings.values(); } } }