/* * 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.synapse.mediators.bsf; import com.google.gson.JsonElement; import com.google.gson.JsonParser; import com.google.gson.stream.JsonReader; import com.sun.phobos.script.javascript.RhinoScriptEngineFactory; import com.sun.script.groovy.GroovyScriptEngineFactory; import com.sun.script.jruby.JRubyScriptEngineFactory; import com.sun.script.jython.JythonScriptEngineFactory; import org.apache.axiom.om.OMElement; import org.apache.axiom.om.OMText; import org.apache.bsf.xml.XMLHelper; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.synapse.MessageContext; import org.apache.synapse.SynapseException; import org.apache.synapse.SynapseLog; import org.apache.synapse.commons.json.JsonUtil; import org.apache.synapse.commons.util.MiscellaneousUtil; import org.apache.synapse.config.Entry; import org.apache.synapse.core.axis2.Axis2MessageContext; import org.apache.synapse.mediators.AbstractMediator; import org.apache.synapse.mediators.Value; import org.mozilla.javascript.Context; import javax.activation.DataHandler; import javax.script.*; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.Map; import java.util.Properties; import java.util.TreeMap; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; /** * A Synapse mediator that calls a function in any scripting language supported by the BSF. * The ScriptMediator supports scripts specified in-line or those loaded through a registry * <p/> * <pre> * <script [key="entry-key"] * [function="script-function-name"] language="javascript|groovy|ruby"> * (text | xml)? * </script> * </pre> * <p/> * <p/> * The function is an optional attribute defining the name of the script function to call, * if not specified it defaults to a function named 'mediate'. The function takes a single * parameter which is the Synapse MessageContext. The function may return a boolean, if it * does not then true is assumed. */ public class ScriptMediator extends AbstractMediator { private static final Log logger = LogFactory.getLog(ScriptMediator.class.getName()); /** * The name of the variable made available to the scripting language to access the message */ private static final String MC_VAR_NAME = "mc"; /** * Name of the java script language */ private static final String JAVA_SCRIPT = "js"; /** * The registry entry key for a script loaded from the registry * Handle both static and dynamic(Xpath) Keys */ private Value key; /** * The language of the script code */ private String language; /** * The map of included scripts; key = registry entry key, value = script source */ private final Map<Value, Object> includes; /** * The optional name of the function to be invoked, defaults to mediate */ private String function = "mediate"; /** * The source code of the script */ private String scriptSourceCode; /** * The BSF engine created to process each message through the script */ protected ScriptEngine scriptEngine; /** * The BSF engine created to validate each JSON payload */ protected ScriptEngine jsEngine; /** * Does the ScriptEngine support multi-threading */ private boolean multiThreadedEngine; /** * The compiled script. Only used for inline scripts */ private CompiledScript compiledScript; /** * The BSF helper to convert between the XML representations used by Java * and the scripting language */ private XMLHelper xmlHelper; /** * Script Engine Manger */ private ScriptEngineManager engineManager; /** * Default Pool Size */ private int DEFAULT_POOL_SIZE = 15; /** * Pool size */ private int poolSize = DEFAULT_POOL_SIZE; /** * Pool size property name */ private static String POOL_SIZE_PROPERTY = "synapse.script.mediator.pool.size"; /** * Pool ScriptEngine Resources */ private BlockingQueue<ScriptEngineWrapper> pool; /** * JSON parser used to parse JSON strings */ private JsonParser jsonParser; /** * Store the class loader from properties */ private ClassLoader loader; /** * Create a script mediator for the given language and given script source * * @param language the BSF language * @param scriptSourceCode the source code of the script */ public ScriptMediator(String language, String scriptSourceCode,ClassLoader classLoader) { this.language = language; this.scriptSourceCode = scriptSourceCode; this.setLoader(classLoader); this.includes = new TreeMap<Value, Object>(); initInlineScript(); } /** * Create a script mediator for the given language and given script entry key and function * * @param language the BSF language * @param includeKeysMap Include script keys * @param key the registry entry key to load the script * @param function the function to be invoked */ public ScriptMediator(String language, Map<Value, Object> includeKeysMap, Value key, String function,ClassLoader classLoader) { this.language = language; this.key = key; this.setLoader(classLoader); this.includes = includeKeysMap; if (function != null) { this.function = function; } Properties properties = MiscellaneousUtil.loadProperties("synapse.properties"); poolSize = Integer.parseInt(properties.getProperty(POOL_SIZE_PROPERTY, String.valueOf(DEFAULT_POOL_SIZE))); initScriptEngine(); if (!(scriptEngine instanceof Invocable)) { throw new SynapseException("Script engine is not an Invocable" + " engine for language: " + language); } } /** * Perform Script mediation * * @param synCtx the Synapse message context * @return the boolean result from the script invocation */ public boolean mediate(MessageContext synCtx) { if (synCtx.getEnvironment().isDebuggerEnabled()) { if (super.divertMediationRoute(synCtx)) { return true; } } SynapseLog synLog = getLog(synCtx); if (synLog.isTraceOrDebugEnabled()) { synLog.traceOrDebug("Start : Script mediator"); if (synLog.isTraceTraceEnabled()) { synLog.traceTrace("Message : " + synCtx.getEnvelope()); } } if (synLog.isTraceOrDebugEnabled()) { synLog.traceOrDebug("Scripting language : " + language + " source " + (key == null ? ": specified inline " : " loaded with key : " + key) + (function != null ? " function : " + function : "")); } boolean returnValue; if (multiThreadedEngine) { returnValue = invokeScript(synCtx); } else { // TODO: change to use a pool of script engines (requires an update to BSF) synchronized (scriptEngine.getClass()) { returnValue = invokeScript(synCtx); } } if (synLog.isTraceTraceEnabled()) { synLog.traceTrace("Result message after execution of script : " + synCtx.getEnvelope()); } if (synLog.isTraceOrDebugEnabled()) { synLog.traceOrDebug("End : Script mediator return value : " + returnValue); } return returnValue; } private boolean invokeScript(MessageContext synCtx) { boolean returnValue; try { //if the engine is Rhino then needs to set the class loader specifically if (language.equals("js")) { Context cx = Context.enter(); cx.setApplicationClassLoader(this.loader); } Object returnObject; if (key != null) { returnObject = mediateWithExternalScript(synCtx); } else { returnObject = mediateForInlineScript(synCtx); } returnValue = !(returnObject != null && returnObject instanceof Boolean) || (Boolean) returnObject; } catch (ScriptException e) { handleException("The script engine returned an error executing the " + (key == null ? "inlined " : "external ") + language + " script" + (key != null ? " : " + key : "") + (function != null ? " function " + function : ""), e, synCtx); returnValue = false; } catch (NoSuchMethodException e) { handleException("The script engine returned a NoSuchMethodException executing the " + "external " + language + " script" + " : " + key + (function != null ? " function " + function : ""), e, synCtx); returnValue = false; } catch (Exception e) { handleException("The script engine returned an Exception executing the " + "external " + language + " script" + " : " + key + (function != null ? " function " + function : ""), e, synCtx); returnValue = false; } finally { if (language.equals("js")) { Context.exit(); } } return returnValue; } /** * Mediation implementation when the script to be executed should be loaded from the registry * * @param synCtx the message context * @return script result * @throws ScriptException For any errors , when compile, run the script * @throws NoSuchMethodException If the function is not defined in the script */ private Object mediateWithExternalScript(MessageContext synCtx) throws ScriptException, NoSuchMethodException { ScriptEngineWrapper sew = null; Object obj; try { sew = prepareExternalScript(synCtx); XMLHelper helper; if (language.equalsIgnoreCase(JAVA_SCRIPT)) { helper = xmlHelper; } else { helper = XMLHelper.getArgHelper(sew.getEngine()); } ScriptMessageContext scriptMC = new ScriptMessageContext(synCtx, helper); processJSONPayload(synCtx, scriptMC); Invocable invocableScript = (Invocable) sew.getEngine(); obj = invocableScript.invokeFunction(function, new Object[]{scriptMC}); } finally { if(sew != null){ // return engine to front of queue or drop if queue is full (i.e. if getNewScriptEngine() spawns a new engine) pool.offer(sew); } } return obj; } /** * Perform mediation with static inline script of the given scripting language * * @param synCtx message context * @return true, or the script return value * @throws ScriptException For any errors , when compile , run the script */ private Object mediateForInlineScript(MessageContext synCtx) throws ScriptException { ScriptMessageContext scriptMC = new ScriptMessageContext(synCtx, xmlHelper); processJSONPayload(synCtx, scriptMC); Bindings bindings = scriptEngine.createBindings(); bindings.put(MC_VAR_NAME, scriptMC); Object response; if (compiledScript != null) { response = compiledScript.eval(bindings); } else { response = scriptEngine.eval(scriptSourceCode, bindings); } return response; } private void processJSONPayload(MessageContext synCtx, ScriptMessageContext scriptMC) throws ScriptException { if (!(synCtx instanceof Axis2MessageContext)) { return; } org.apache.axis2.context.MessageContext messageContext = ((Axis2MessageContext) synCtx).getAxis2MessageContext(); String jsonString = (String) messageContext.getProperty("JSON_STRING"); Object jsonObject = null; prepareForJSON(scriptMC); if (JsonUtil.hasAJsonPayload(messageContext)) { try { JsonElement o = jsonParser.parse(new JsonReader(JsonUtil.newJsonPayloadReader(messageContext))); // first, check if the stream is valid. if (o.isJsonNull()) { logger.error("#processJSONPayload. JSON stream is not valid."); return; } jsonObject = this.jsEngine.eval(JsonUtil.newJavaScriptSourceReader(messageContext)); } catch (Exception e) { handleException("Failed to get the JSON payload from the input stream. Error>>>\n" + e.getLocalizedMessage()); } } else if (jsonString != null) { String jsonPayload = jsonParser.parse(jsonString).toString(); jsonObject = this.jsEngine.eval('(' + jsonPayload + ')'); } if (jsonObject != null) { scriptMC.setJsonObject(synCtx, jsonObject); } } private void prepareForJSON(ScriptMessageContext scriptMC) { if (jsonParser == null) { jsonParser = new JsonParser(); } scriptMC.setScriptEngine(this.jsEngine); } /** * Initialise the Mediator for the inline script */ protected void initInlineScript() { try { initScriptEngine(); if (scriptEngine instanceof Compilable) { if (log.isDebugEnabled()) { log.debug("Script engine supports Compilable interface, " + "compiling script code.."); } compiledScript = ((Compilable) scriptEngine).compile(scriptSourceCode); } else { // do nothing. If the script engine doesn't support Compilable then // the inline script will be evaluated on each invocation if (log.isDebugEnabled()) { log.debug("Script engine does not support the Compilable interface, " + "in-lined script would be evaluated on each invocation.."); } } } catch (ScriptException e) { throw new SynapseException("Exception initializing inline script", e); } } /** * Prepares the mediator for the invocation of an external script * * @param synCtx MessageContext script * @throws ScriptException For any errors , when compile the script */ protected ScriptEngineWrapper prepareExternalScript(MessageContext synCtx) throws ScriptException { // Derive actual key from xpath expression or get static key String generatedScriptKey = key.evaluateValue(synCtx); Entry entry = synCtx.getConfiguration().getEntryDefinition(generatedScriptKey); boolean needsReload = (entry != null) && entry.isDynamic() && (!entry.isCached() || entry.isExpired()); ScriptEngineWrapper sew = getNewScriptEngine(); Bindings engineBinding = sew.getEngine().getBindings(ScriptContext.ENGINE_SCOPE); engineBinding.clear(); // if we don't do this, previous state can affect successive executions! ESBJAVA-4583 if (scriptSourceCode == null || needsReload || !sew.isInitialized()) { Object o = synCtx.getEntry(generatedScriptKey); if (o instanceof OMElement) { scriptSourceCode = ((OMElement) (o)).getText(); sew.getEngine().eval(scriptSourceCode, engineBinding); } else if (o instanceof String) { scriptSourceCode = (String) o; sew.getEngine().eval(scriptSourceCode, engineBinding); } else if (o instanceof OMText) { DataHandler dataHandler = (DataHandler) ((OMText) o).getDataHandler(); if (dataHandler != null) { BufferedReader reader = null; try { reader = new BufferedReader( new InputStreamReader(dataHandler.getInputStream())); StringBuilder scriptSB = new StringBuilder(); String currentLine; while ((currentLine = reader.readLine()) != null) { scriptSB.append(currentLine).append('\n'); } scriptSourceCode = scriptSB.toString(); sew.getEngine().eval(scriptSourceCode, engineBinding); } catch (IOException e) { handleException("Error in reading script as a stream ", e, synCtx); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { handleException("Error in closing input stream ", e, synCtx); } } } } } } else { sew.getEngine().eval(scriptSourceCode, engineBinding); // Will drop TPS, but is required for ESBJAVA-4583 } // load <include /> scripts; reload each script if needed for (Value includeKey : includes.keySet()) { String includeSourceCode = (String) includes.get(includeKey); String generatedKey = includeKey.evaluateValue(synCtx); Entry includeEntry = synCtx.getConfiguration().getEntryDefinition(generatedKey); boolean includeEntryNeedsReload = (includeEntry != null) && includeEntry.isDynamic() && (!includeEntry.isCached() || includeEntry.isExpired()); if (includeSourceCode == null || includeEntryNeedsReload || !sew.isInitialized()) { log.debug("Re-/Loading the include script with key " + includeKey); Object o = synCtx.getEntry(generatedKey); if (o instanceof OMElement) { includeSourceCode = ((OMElement) (o)).getText(); sew.getEngine().eval(includeSourceCode, engineBinding); } else if (o instanceof String) { includeSourceCode = (String) o; sew.getEngine().eval(includeSourceCode, engineBinding); } else if (o instanceof OMText) { DataHandler dataHandler = (DataHandler) ((OMText) o).getDataHandler(); if (dataHandler != null) { BufferedReader reader = null; try { reader = new BufferedReader( new InputStreamReader(dataHandler.getInputStream())); StringBuilder scriptSB = new StringBuilder(); String currentLine; while ((currentLine = reader.readLine()) != null) { scriptSB.append(currentLine).append('\n'); } includeSourceCode = scriptSB.toString(); sew.getEngine().eval(includeSourceCode, engineBinding); } catch (IOException e) { handleException("Error in reading script as a stream ", e, synCtx); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { handleException("Error in closing input" + " stream ", e, synCtx); } } } } } includes.put(includeKey, includeSourceCode); } else { sew.getEngine().eval(includeSourceCode, engineBinding); // Will drop TPS, but required for ESBJAVA-4583 } } sew.setInitialized(true); return sew; } protected void initScriptEngine() { if (log.isDebugEnabled()) { log.debug("Initializing script mediator for language : " + language); } engineManager = new ScriptEngineManager(); engineManager.registerEngineExtension("js", new RhinoScriptEngineFactory()); engineManager.registerEngineExtension("groovy", new GroovyScriptEngineFactory()); engineManager.registerEngineExtension("rb", new JRubyScriptEngineFactory()); engineManager.registerEngineExtension("jsEngine", new RhinoScriptEngineFactory()); engineManager.registerEngineExtension("py", new JythonScriptEngineFactory()); this.scriptEngine = engineManager.getEngineByExtension(language); pool = new LinkedBlockingQueue<ScriptEngineWrapper>(poolSize); for (int i = 0; i< poolSize; i++) { ScriptEngineWrapper sew = new ScriptEngineWrapper(engineManager.getEngineByExtension(language)); pool.add(sew); } this.jsEngine = engineManager.getEngineByExtension("jsEngine"); if (scriptEngine == null) { handleException("No script engine found for language: " + language); } //Invoking a custom Helper class since there is an api change in rhino17 for js if (language.equalsIgnoreCase(JAVA_SCRIPT)) { xmlHelper = new JavaScriptXmlHelper(); } else { xmlHelper = XMLHelper.getArgHelper(scriptEngine); } this.multiThreadedEngine = scriptEngine.getFactory().getParameter("THREADING") != null; log.debug("Script mediator for language : " + language + " supports multithreading? : " + multiThreadedEngine); } public String getLanguage() { return language; } public Value getKey() { return key; } public String getFunction() { return function; } public String getScriptSrc() { return scriptSourceCode; } private void handleException(String msg) { log.error(msg); throw new SynapseException(msg); } public Map<Value, Object> getIncludeMap() { return includes; } public ClassLoader getLoader() { return loader; } public void setLoader(ClassLoader loader) { this.loader = loader; } public ScriptEngineWrapper getNewScriptEngine() { ScriptEngineWrapper scriptEngineWrapper = pool.poll(); if (scriptEngineWrapper == null) { scriptEngineWrapper = new ScriptEngineWrapper(engineManager.getEngineByExtension(language)); } // fall back return scriptEngineWrapper; } public boolean isContentAltering() { return true; } }