/* * Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com * The software in this package is published under the terms of the CPAL v1.0 * license, a copy of which has been included with this distribution in the * LICENSE.txt file. */ package org.mule.plugin.scripting.component; import static java.util.stream.Collectors.toMap; import static org.mule.runtime.api.i18n.I18nMessageFactory.createStaticMessage; import static org.mule.runtime.core.config.i18n.CoreMessages.cannotLoadFromClasspath; import static org.mule.runtime.core.config.i18n.CoreMessages.propertiesNotSet; import static org.mule.runtime.core.util.IOUtils.getResourceAsStream; import org.mule.runtime.api.lifecycle.Initialisable; import org.mule.runtime.api.lifecycle.InitialisationException; import org.mule.runtime.api.message.Message; import org.mule.runtime.core.DefaultMuleEventContext; import org.mule.runtime.core.api.Event; import org.mule.runtime.core.api.MuleContext; import org.mule.runtime.core.api.construct.FlowConstruct; import org.mule.runtime.core.api.construct.FlowConstructAware; import org.mule.runtime.core.api.context.MuleContextAware; import org.mule.runtime.core.el.context.FlowVariableMapContext; import org.mule.runtime.core.el.context.SessionVariableMapContext; import org.mule.runtime.core.util.CollectionUtils; import org.mule.runtime.core.util.StringUtils; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.util.List; import javax.script.Bindings; import javax.script.Compilable; import javax.script.CompiledScript; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A JSR 223 Script service. Allows any JSR 223 compliant script engines such as JavaScript, Groovy or Rhino to be embedded as * Mule components. */ public class Scriptable implements Initialisable, MuleContextAware, FlowConstructAware { private static final String BINDING_LOG = "log"; private static final String BINDING_RESULT = "result"; private static final String BINDING_MULE_CONTEXT = "muleContext"; private static final String BINDING_REGISTRY = "registry"; private static final String BINDING_PAYLOAD = "payload"; private static final String BINDING_SRC = "src"; private static final String BINDING_EVENT_CONTEXT = "eventContext"; private static final String BINDING_ID = "id"; private static final String BINDING_FLOW_CONSTRUCT = "flowConstruct"; private static final String BINDING_FLOW_VARS = "flowVars"; private static final String BINDING_SESSION_VARS = "sessionVars"; private static final String BINDING_EXCEPTION = "exception"; public static final String BINDING_MESSAGE = "message"; /** The actual body of the script */ private String scriptText; /** A file from which the script will be loaded */ private String scriptFile; /** Parameters to be made available to the script as variables */ private List<ScriptingProperty> properties; /** The name of the JSR 223 scripting engine (e.g., "groovy") */ private String scriptEngineName; // /////////////////////////////////////////////////////////////////////////// // Internal variables, not exposed as properties // /////////////////////////////////////////////////////////////////////////// /** A compiled version of the script, if the scripting engine supports it */ private CompiledScript compiledScript; private ScriptEngine scriptEngine; private ScriptEngineManager scriptEngineManager; private MuleContext muleContext; private FlowConstruct flow; protected transient Logger logger = LoggerFactory.getLogger(getClass()); public Scriptable() { // For Spring } public Scriptable(MuleContext muleContext) { this.muleContext = muleContext; } @Override public void setMuleContext(MuleContext context) { this.muleContext = context; } @Override public void setFlowConstruct(FlowConstruct flowConstruct) { this.flow = flowConstruct; } @Override public void initialise() throws InitialisationException { scriptEngineManager = new ScriptEngineManager(this.getClass().getClassLoader()); // Create scripting engine if (scriptEngineName != null) { scriptEngine = createScriptEngineByName(scriptEngineName); if (scriptEngine == null) { throw new InitialisationException(createStaticMessage("Scripting engine '" + scriptEngineName + "' not found. Available engines are: " + listAvailableEngines()), this); } } // Determine scripting engine to use by file extension else if (scriptFile != null) { int i = scriptFile.lastIndexOf("."); if (i > -1) { logger.info("Script Engine name not set. Guessing by file extension."); String extension = scriptFile.substring(i + 1); scriptEngine = createScriptEngineByExtension(extension); if (scriptEngine == null) { throw new InitialisationException(createStaticMessage("File extension '" + extension + "' does not map to a scripting engine. Available engines are: " + listAvailableEngines()), this); } else { setScriptEngineName(extension); } } } Reader script = null; try { // Load script from variable if (StringUtils.isNotBlank(scriptText)) { script = new StringReader(scriptText); } // Load script from file else if (scriptFile != null) { InputStream is; try { is = getResourceAsStream(scriptFile, getClass()); } catch (IOException e) { throw new InitialisationException(cannotLoadFromClasspath(scriptFile), e, this); } if (is == null) { throw new InitialisationException(cannotLoadFromClasspath(scriptFile), this); } script = new InputStreamReader(is); } else { throw new InitialisationException(propertiesNotSet("scriptText, scriptFile"), this); } // Pre-compile script if scripting engine supports compilation. if (scriptEngine instanceof Compilable) { try { compiledScript = ((Compilable) scriptEngine).compile(script); } catch (ScriptException e) { throw new InitialisationException(e, this); } } } finally { if (script != null) { try { script.close(); } catch (IOException e) { throw new InitialisationException(e, this); } } } } protected void populatePropertyBindings(Bindings bindings) { if (properties != null) { bindings.putAll(properties.stream().collect(toMap(ScriptingProperty::getKey, ScriptingProperty::getValue))); } } protected void populatePropertyBindings(Bindings bindings, Event event) { if (properties != null) { for (ScriptingProperty property : properties) { String value = (String) property.getValue(); if (muleContext.getExpressionManager().isExpression(value)) { bindings.put(property.getKey(), muleContext.getExpressionManager().parse(value, event, flow)); } else { bindings.put(property.getKey(), value); } } } } public void populateDefaultBindings(Bindings bindings) { bindings.put(BINDING_LOG, logger); // A place holder for a returned result if the script doesn't return a result. // The script can overwrite this binding bindings.put(BINDING_RESULT, null); bindings.put(BINDING_MULE_CONTEXT, muleContext); bindings.put(BINDING_REGISTRY, muleContext.getRegistry()); } public void populateBindings(Bindings bindings, Event event, Event.Builder eventBuilder) { populatePropertyBindings(bindings, event); populateDefaultBindings(bindings); populateMessageBindings(bindings, event, eventBuilder); bindings.put(BINDING_EVENT_CONTEXT, new DefaultMuleEventContext(flow, event)); bindings.put(BINDING_FLOW_CONSTRUCT, flow); } protected void populateMessageBindings(Bindings bindings, Event event, Event.Builder eventBuilder) { Message message = event.getMessage(); populateVariablesInOrder(bindings, event); // TODO MULE-10121 Provide a MessageBuilder API in scripting components to improve usability bindings.put(BINDING_MESSAGE, event.getMessage()); // This will get overwritten if populateBindings(Bindings bindings, MuleEvent event) is called // and not this method directly. bindings.put(BINDING_PAYLOAD, message.getPayload().getValue()); // For backward compatibility bindings.put(BINDING_SRC, message.getPayload().getValue()); populateHeadersVariablesAndException(bindings, event, eventBuilder); } private void populateHeadersVariablesAndException(Bindings bindings, Event event, Event.Builder eventBuilder) { bindings.put(BINDING_FLOW_VARS, new FlowVariableMapContext(event, eventBuilder)); bindings.put(BINDING_SESSION_VARS, new SessionVariableMapContext(event.getSession())); // Only add exception is present if (event.getError().isPresent()) { bindings.put(BINDING_EXCEPTION, event.getError().get().getCause()); } else { bindings.put(BINDING_EXCEPTION, null); } } private void populateVariablesInOrder(Bindings bindings, Event event) { for (String key : event.getSession().getPropertyNamesAsSet()) { bindings.put(key, event.getSession().getProperty(key)); } for (String key : event.getVariableNames()) { bindings.put(key, event.getVariable(key).getValue()); } } public Object runScript(Bindings bindings) throws ScriptException { Object result; try { RegistryLookupBindings registryLookupBindings = new RegistryLookupBindings(muleContext.getRegistry(), bindings); if (compiledScript != null) { result = compiledScript.eval(registryLookupBindings); } else { result = scriptEngine.eval(scriptText, registryLookupBindings); } // The result of the script can be returned directly or it can // be set as the variable "result". if (result == null) { result = registryLookupBindings.get(BINDING_RESULT); } } catch (ScriptException e) { // re-throw throw e; } catch (Exception ex) { throw new ScriptException(ex); } return result; } protected ScriptEngine createScriptEngineByName(String name) { return scriptEngineManager.getEngineByName(name); } protected ScriptEngine createScriptEngineByExtension(String ext) { return scriptEngineManager.getEngineByExtension(ext); } protected String listAvailableEngines() { return CollectionUtils.toString(scriptEngineManager.getEngineFactories(), false); } // ////////////////////////////////////////////////////////////////////////////// // Getters and setters // ////////////////////////////////////////////////////////////////////////////// public String getScriptText() { return scriptText; } public void setScriptText(String scriptText) { this.scriptText = scriptText; } public String getScriptFile() { return scriptFile; } public void setScriptFile(String scriptFile) { this.scriptFile = scriptFile; } public void setScriptEngineName(String scriptEngineName) { this.scriptEngineName = scriptEngineName; } public String getScriptEngineName() { return scriptEngineName; } public List<ScriptingProperty> getProperties() { return properties; } public void setProperties(List<ScriptingProperty> properties) { this.properties = properties; } public ScriptEngine getScriptEngine() { return scriptEngine; } protected void setScriptEngine(ScriptEngine scriptEngine) { this.scriptEngine = scriptEngine; } protected CompiledScript getCompiledScript() { return compiledScript; } protected void setCompiledScript(CompiledScript compiledScript) { this.compiledScript = compiledScript; } }