/**
* 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.camel.builder.script;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.Expression;
import org.apache.camel.Message;
import org.apache.camel.Predicate;
import org.apache.camel.Processor;
import org.apache.camel.spi.ClassResolver;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.ResourceHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A builder class for creating {@link Processor}, {@link Expression} and
* {@link Predicate} objects using the JSR 223 scripting engine.
*
* @version
*/
public class ScriptBuilder implements Expression, Predicate, Processor {
/**
* Additional arguments to {@link ScriptEngine} provided as a header on the IN {@link org.apache.camel.Message}
* using the key {@link #ARGUMENTS}
*/
public static final String ARGUMENTS = "CamelScriptArguments";
private static final Logger LOG = LoggerFactory.getLogger(ScriptBuilder.class);
private Map<String, Object> attributes;
private final CamelContext camelContext;
private final ScriptEngineFactory scriptEngineFactory;
private final ScriptEngine scriptEngine;
private final String scriptLanguage;
private final String scriptResource;
private final String scriptText;
private CompiledScript compiledScript;
/**
* Constructor.
*
* @param scriptLanguage the name of the scripting language
* @param scriptText the script text to be evaluated, or a reference to a script resource
*/
public ScriptBuilder(String scriptLanguage, String scriptText) {
this(null, scriptLanguage, scriptText, null);
}
/**
* Constructor.
*
* @param scriptLanguage the name of the scripting language
* @param scriptText the script text to be evaluated, or a reference to a script resource
*/
public ScriptBuilder(CamelContext camelContext, String scriptLanguage, String scriptText) {
this(camelContext, scriptLanguage, scriptText, null);
}
/**
* Constructor.
*
* @param scriptLanguage the name of the scripting language
* @param scriptText the script text to be evaluated, or a reference to a script resource
* @param scriptEngineFactory the script engine factory
*/
public ScriptBuilder(CamelContext camelContext, String scriptLanguage, String scriptText, ScriptEngineFactory scriptEngineFactory) {
this.camelContext = camelContext;
this.scriptLanguage = scriptLanguage;
if (ResourceHelper.hasScheme(scriptText)) {
this.scriptResource = scriptText;
this.scriptText = null;
} else {
this.scriptResource = null;
this.scriptText = scriptText;
}
if (scriptEngineFactory == null) {
this.scriptEngine = createScriptEngine(scriptLanguage, false);
this.scriptEngineFactory = lookupScriptEngineFactory(scriptLanguage);
} else {
this.scriptEngineFactory = scriptEngineFactory;
this.scriptEngine = scriptEngineFactory.getScriptEngine();
}
if (this.scriptEngineFactory == null) {
throw new IllegalArgumentException("Cannot lookup ScriptEngineFactory for script language: " + scriptLanguage);
}
// bean shell is not compileable
if (isBeanShell(scriptLanguage)) {
return;
}
// pre-compile script which would execute faster if possible
Reader reader = null;
try {
// if we have camel context then load resources
if (camelContext != null && scriptResource != null) {
reader = createScriptReader(camelContext.getClassResolver(), scriptResource);
} else if (this.scriptText != null) {
reader = new StringReader(this.scriptText);
}
// pre-compile script if we have it as text
if (reader != null) {
if (compileScripte(camelContext) && scriptEngine instanceof Compilable) {
Compilable compilable = (Compilable) scriptEngine;
this.compiledScript = compilable.compile(reader);
LOG.debug("Using compiled script: {}", this.compiledScript);
}
}
} catch (IOException e) {
throw new ScriptEvaluationException("Cannot load " + scriptLanguage + " script from resource: " + scriptResource, e);
} catch (ScriptException e) {
throw new ScriptEvaluationException("Error compiling " + scriptLanguage + " script: " + scriptText, e);
} finally {
IOHelper.close(reader);
}
}
@Override
public String toString() {
return getScriptDescription();
}
public Object evaluate(Exchange exchange) {
return evaluateScript(exchange);
}
public <T> T evaluate(Exchange exchange, Class<T> type) {
Object result = evaluate(exchange);
return exchange.getContext().getTypeConverter().convertTo(type, result);
}
public boolean matches(Exchange exchange) {
Object scriptValue = evaluateScript(exchange);
return matches(exchange, scriptValue);
}
public void assertMatches(String text, Exchange exchange) throws AssertionError {
Object scriptValue = evaluateScript(exchange);
if (!matches(exchange, scriptValue)) {
throw new AssertionError(this + " failed on " + exchange + " as script returned <" + scriptValue + ">");
}
}
public void process(Exchange exchange) {
evaluateScript(exchange);
}
// Builder API
// -------------------------------------------------------------------------
/**
* Sets the attribute on the context so that it is available to the script
* as a variable in the {@link ScriptContext#ENGINE_SCOPE}
*
* @param name the name of the attribute
* @param value the attribute value
* @return this builder
*/
public ScriptBuilder attribute(String name, Object value) {
if (attributes == null) {
attributes = new HashMap<String, Object>();
}
attributes.put(name, value);
return this;
}
/**
* Creates a script builder for the named language and script contents
*
* @param language the language to use for the script
* @param scriptText the script text to be evaluated, or a reference to a script resource
* @return the builder
*/
public static ScriptBuilder script(String language, String scriptText) {
return new ScriptBuilder(language, scriptText);
}
/**
* Creates a script builder for the groovy script contents
*
* @param scriptText the script text to be evaluated, or a reference to a script resource
* @return the builder
*/
public static ScriptBuilder groovy(String scriptText) {
return new ScriptBuilder("groovy", scriptText);
}
/**
* Creates a script builder for the JavaScript/ECMAScript script contents
*
* @param scriptText the script text to be evaluated, or a reference to a script resource
* @return the builder
*/
public static ScriptBuilder javaScript(String scriptText) {
return new ScriptBuilder("js", scriptText);
}
/**
* Creates a script builder for the PHP script contents
*
* @param scriptText the script text to be evaluated, or a reference to a script resource
* @return the builder
*/
public static ScriptBuilder php(String scriptText) {
return new ScriptBuilder("php", scriptText);
}
/**
* Creates a script builder for the Python script contents
*
* @param scriptText the script text to be evaluated, or a reference to a script resource
* @return the builder
*/
public static ScriptBuilder python(String scriptText) {
return new ScriptBuilder("python", scriptText);
}
/**
* Creates a script builder for the Ruby/JRuby script contents
*
* @param scriptText the script text to be evaluated, or a reference to a script resource
* @return the builder
*/
public static ScriptBuilder ruby(String scriptText) {
return new ScriptBuilder("jruby", scriptText);
}
/**
* Whether the given language is a language that is supported by a scripting engine.
*/
public static boolean supportScriptLanguage(String language) {
return createScriptEngine(language, true) != null;
}
// Properties
// -------------------------------------------------------------------------
public CompiledScript getCompiledScript() {
return compiledScript;
}
public String getScriptLanguage() {
return scriptLanguage;
}
/**
* Returns a description of the script
*
* @return the script description
*/
public String getScriptDescription() {
if (scriptText != null) {
return scriptLanguage + ": " + scriptText;
} else if (scriptResource != null) {
return scriptLanguage + ": " + scriptResource;
} else {
return scriptLanguage + ": null script";
}
}
// Implementation methods
// -------------------------------------------------------------------------
protected boolean matches(Exchange exchange, Object scriptValue) {
return exchange.getContext().getTypeConverter().convertTo(boolean.class, scriptValue);
}
private static String[] getScriptNames(String name) {
if (name.equals("js")) {
return new String[]{"js", "javaScript", "ECMAScript"};
} else if (name.equals("javaScript")) {
return new String[]{"javaScript", "js", "ECMAScript"};
} else if (name.equals("ECMAScript")) {
return new String[]{"ECMAScript", "javaScript", "js"};
}
return new String[]{name};
}
protected static ScriptEngineFactory lookupScriptEngineFactory(String language) {
ScriptEngineManager manager = new ScriptEngineManager();
for (ScriptEngineFactory factory : manager.getEngineFactories()) {
// some script names has alias
String[] names = getScriptNames(language);
for (String name : names) {
if (factory.getLanguageName().equals(name)) {
return factory;
}
}
}
// fallback to get engine by name
ScriptEngine engine = createScriptEngine(language, true);
if (engine != null) {
return engine.getFactory();
}
return null;
}
protected static ScriptEngine createScriptEngine(String language, boolean allowNull) {
ScriptEngine engine = tryCreateScriptEngine(language, ScriptBuilder.class.getClassLoader());
if (engine == null) {
engine = tryCreateScriptEngine(language, Thread.currentThread().getContextClassLoader());
}
if (engine == null && !allowNull) {
throw new IllegalArgumentException("No script engine could be created for: " + language);
}
return engine;
}
/**
* Attemps to create the script engine for the given langauge using the classloader
*
* @return the engine, or <tt>null</tt> if not able to create
*/
private static ScriptEngine tryCreateScriptEngine(String language, ClassLoader classLoader) {
ScriptEngineManager manager = new ScriptEngineManager(classLoader);
ScriptEngine engine = null;
// some script names has alias
String[] names = getScriptNames(language);
for (String name : names) {
try {
engine = manager.getEngineByName(name);
if (engine != null) {
break;
}
} catch (NoClassDefFoundError ex) {
LOG.warn("Cannot load ScriptEngine for " + name + ". Please ensure correct JARs is provided on classpath (stacktrace in DEBUG logging).");
// include stacktrace in debug logging
LOG.debug("Cannot load ScriptEngine for " + name + ". Please ensure correct JARs is provided on classpath.", ex);
}
}
if (engine == null) {
engine = checkForOSGiEngine(language);
}
if (engine != null && isPython(language)) {
ScriptContext context = engine.getContext();
context.setAttribute("com.sun.script.jython.comp.mode", "eval", ScriptContext.ENGINE_SCOPE);
}
return engine;
}
private static ScriptEngine checkForOSGiEngine(String language) {
LOG.debug("No script engine found for {} using standard javax.script auto-registration. Checking OSGi registry.", language);
try {
// Test the OSGi environment with the Activator
Class<?> c = Class.forName("org.apache.camel.script.osgi.Activator");
Method mth = c.getDeclaredMethod("getBundleContext");
Object ctx = mth.invoke(null);
LOG.debug("Found OSGi BundleContext: {}", ctx);
if (ctx != null) {
Method resolveScriptEngine = c.getDeclaredMethod("resolveScriptEngine", String.class);
return (ScriptEngine)resolveScriptEngine.invoke(null, language);
}
} catch (Throwable t) {
LOG.debug("Unable to detect OSGi. ScriptEngine for " + language + " cannot be resolved.", t);
}
return null;
}
protected Object evaluateScript(Exchange exchange) {
try {
if (reuseScriptEngine(exchange)) {
// It's not safe to do the evaluation with a single scriptEngine
synchronized (this) {
LOG.debug("Calling doEvaluateScript without creating a new scriptEngine");
return doEvaluateScript(exchange, scriptEngine);
}
} else {
LOG.debug("Calling doEvaluateScript with a new scriptEngine");
// get a new engine which we must for each exchange
ScriptEngine engine = scriptEngineFactory.getScriptEngine();
return doEvaluateScript(exchange, engine);
}
} catch (ScriptException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Script evaluation failed: " + e.getMessage(), e);
}
if (e.getCause() != null) {
throw createScriptEvaluationException(e.getCause());
} else {
throw createScriptEvaluationException(e);
}
} catch (IOException e) {
throw createScriptEvaluationException(e);
}
}
protected Object doEvaluateScript(Exchange exchange, ScriptEngine scriptEngine) throws ScriptException, IOException {
ScriptContext context = populateBindings(scriptEngine, exchange, attributes);
addScriptEngineArguments(scriptEngine, exchange);
Object result = runScript(scriptEngine, exchange, context);
LOG.debug("The script evaluation result is: {}", result);
return result;
}
// To check the camel context property to decide if we need to reuse the ScriptEngine
private boolean reuseScriptEngine(Exchange exchange) {
CamelContext camelContext = exchange.getContext();
if (camelContext != null) {
return getCamelContextProperty(camelContext, Exchange.REUSE_SCRIPT_ENGINE);
} else {
// default value is false
return false;
}
}
private boolean compileScripte(CamelContext camelContext) {
if (camelContext != null) {
return getCamelContextProperty(camelContext, Exchange.COMPILE_SCRIPT);
} else {
return false;
}
}
private boolean getCamelContextProperty(CamelContext camelContext, String propertyKey) {
String propertyValue = camelContext.getProperty(propertyKey);
if (propertyValue != null) {
return camelContext.getTypeConverter().convertTo(boolean.class, propertyValue);
} else {
return false;
}
}
protected Object runScript(ScriptEngine engine, Exchange exchange, ScriptContext context) throws ScriptException, IOException {
Object result = null;
if (compiledScript != null) {
LOG.trace("Evaluate using compiled script for context: {} on exchange: {}", context, exchange);
result = compiledScript.eval(context);
} else {
if (scriptText != null) {
LOG.trace("Evaluate script for context: {} on exchange: {}", context, exchange);
result = engine.eval(scriptText, context);
} else if (scriptResource != null) {
Reader reader = createScriptReader(exchange.getContext().getClassResolver(), scriptResource);
try {
LOG.trace("Evaluate script for context: {} on exchange: {}", context, exchange);
result = engine.eval(reader, context);
} finally {
IOHelper.close(reader);
}
}
}
// As the script could have multiple statement, we need to look up the result from the engine value set
if (result == null) {
result = engine.get("result");
}
return result;
}
protected ScriptContext populateBindings(ScriptEngine engine, Exchange exchange, Map<String, Object> attributes) {
ScriptContext context = engine.getContext();
int scope = ScriptContext.ENGINE_SCOPE;
context.setAttribute("context", exchange.getContext(), scope);
context.setAttribute("camelContext", exchange.getContext(), scope);
context.setAttribute("exchange", exchange, scope);
Message in = exchange.getIn();
context.setAttribute("request", in, scope);
context.setAttribute("headers", in.getHeaders(), scope);
context.setAttribute("body", in.getBody(), scope);
if (exchange.hasOut()) {
Message out = exchange.getOut();
context.setAttribute("out", out, scope);
context.setAttribute("response", out, scope);
}
// to make using properties component easier
context.setAttribute("properties", new ScriptPropertiesFunction(exchange.getContext()), scope);
// any additional attributes
if (attributes != null) {
for (Map.Entry<String, Object> entry : attributes.entrySet()) {
context.setAttribute(entry.getKey(), entry.getValue(), scope);
}
}
return context;
}
@SuppressWarnings("unchecked")
protected void addScriptEngineArguments(ScriptEngine engine, Exchange exchange) {
if (!exchange.getIn().hasHeaders()) {
return;
}
Map<Object, Object> args = exchange.getIn().getHeader(ARGUMENTS, Map.class);
if (args != null) {
for (Map.Entry<Object, Object> entry : args.entrySet()) {
String key = exchange.getContext().getTypeConverter().convertTo(String.class, entry.getKey());
Object value = entry.getValue();
if (!ObjectHelper.isEmpty(key) && value != null) {
LOG.trace("Putting {} -> {} on ScriptEngine", key, value);
engine.put(key, value);
}
}
}
}
protected static InputStreamReader createScriptReader(ClassResolver classResolver, String resource) throws IOException {
InputStream is = ResourceHelper.resolveMandatoryResourceAsInputStream(classResolver, resource);
return new InputStreamReader(is);
}
protected ScriptEvaluationException createScriptEvaluationException(Throwable e) {
if (e.getClass().getName().equals("org.jruby.exceptions.RaiseException")) {
// Only the nested exception has the specific problem
try {
Object ex = e.getClass().getMethod("getException").invoke(e);
return new ScriptEvaluationException("Failed to evaluate: " + getScriptDescription() + ". Error: " + ex + ". Cause: " + e, e);
} catch (Exception e1) {
// do nothing here
}
}
return new ScriptEvaluationException("Failed to evaluate: " + getScriptDescription() + ". Cause: " + e, e);
}
private static boolean isPython(String language) {
return "python".equals(language) || "jython".equals(language);
}
private static boolean isBeanShell(String language) {
return "beanshell".equals(language) || "bsh".equals(language);
}
}