/* * ProActive Parallel Suite(TM): * The Open Source library for parallel and distributed * Workflows & Scheduling, Orchestration, Cloud Automation * and Big Data Analysis on Enterprise Grids & Clouds. * * Copyright (c) 2007 - 2017 ActiveEon * Contact: contact@activeeon.com * * This library is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License * as published by the Free Software Foundation: version 3 of * the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * If needed, contact us to obtain a release under GPL Version 2 or 3 * or a different license than the AGPL. */ package org.ow2.proactive.scripting; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.PrintWriter; import java.io.Reader; import java.io.Serializable; import java.io.StringReader; import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Map; import java.util.Map.Entry; import javax.script.Bindings; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; import javax.script.ScriptEngineManager; import org.apache.log4j.Logger; import org.objectweb.proactive.annotation.PublicAPI; import org.ow2.proactive.utils.BoundedStringWriter; import org.ow2.proactive.utils.FileUtils; import com.google.common.base.Throwables; /** * A simple script to evaluate using java 6 scripting API. * * @author The ProActive Team * @since ProActive Scheduling 0.9 * * * @param <E> Template class's type of the result. */ @PublicAPI public abstract class Script<E> implements Serializable { // default output size in chars public static final int DEFAULT_OUTPUT_MAX_SIZE = 1024 * 1024; // 1 million characters ~ 2 Mb /** Loggers */ public static final Logger logger = Logger.getLogger(Script.class); /** Variable name for script arguments */ public static final String ARGUMENTS_NAME = "args"; public static final String MD5 = "MD5"; /** Name of the script engine or file path to script file (extension will be used to lookup) */ protected String scriptEngineLookup; /** The script to evaluate */ protected String script; /** Id of this script */ protected String id; /** The parameters of the script */ protected Serializable[] parameters; /** Name of the script **/ private String scriptName; /** ProActive needed constructor */ public Script() { } /** Directly create a script with a string. * @param script String representing the script's source code * @param engineName String representing the execution engine * @param parameters script's execution arguments. * @throws InvalidScriptException if the creation fails. */ public Script(String script, String engineName, Serializable[] parameters) throws InvalidScriptException { this.scriptEngineLookup = engineName; this.script = script; this.id = script; this.parameters = parameters; this.scriptName = getDefaultScriptName(); } protected abstract String getDefaultScriptName(); /** Directly create a script with a string. * @param script String representing the script's source code * @param engineName String representing the execution engine * @param parameters script's execution arguments. * @param scriptName name of the script * @throws InvalidScriptException if the creation fails. */ public Script(String script, String engineName, Serializable[] parameters, String scriptName) throws InvalidScriptException { this.scriptEngineLookup = engineName; this.script = script; this.id = script; this.parameters = parameters; this.scriptName = scriptName; } /** Directly create a script with a string. * @param script String representing the script's source code * @param engineName String representing the execution engine * @throws InvalidScriptException if the creation fails. */ public Script(String script, String engineName) throws InvalidScriptException { this(script, engineName, (Serializable[]) null); } /** Directly create a script with a string. * @param script String representing the script's source code * @param engineName String representing the execution engine * @param scriptName name of the script * @throws InvalidScriptException if the creation fails. */ public Script(String script, String engineName, String scriptName) throws InvalidScriptException { this(script, engineName, null, scriptName); } /** Create a script from a file. * * @param file a file containing the script's source code. * @param parameters script's execution arguments. * @throws InvalidScriptException if the creation fails. */ public Script(File file, Serializable[] parameters) throws InvalidScriptException { this.scriptEngineLookup = FileUtils.getExtension(file.getPath()); try { script = readFile(file); } catch (IOException e) { throw new InvalidScriptException("Unable to read script : " + file.getAbsolutePath(), e); } this.id = file.getPath(); this.parameters = parameters; this.scriptName = file.getName(); } /** Create a script from a file. * @param file a file containing a script's source code. * @throws InvalidScriptException if Constructor fails. */ public Script(File file) throws InvalidScriptException { this(file, null); } /** Create a script from an URL. * @param url representing a script source code. * @param parameters execution arguments. * @throws InvalidScriptException if the creation fails. */ public Script(URL url, Serializable[] parameters) throws InvalidScriptException { this.scriptEngineLookup = FileUtils.getExtension(url.getFile()); try { storeScript(url); } catch (IOException e) { throw new InvalidScriptException("Unable to read script : " + url.getPath(), e); } this.id = url.toExternalForm(); this.parameters = parameters; this.scriptName = url.getFile(); } /** Create a script from an URL. * @param url representing a script source code. * @throws InvalidScriptException if the creation fails. */ public Script(URL url) throws InvalidScriptException { this(url, null); } /** Create a script from another script object * @param script2 script object source * @throws InvalidScriptException if the creation fails. */ public Script(Script<?> script2) throws InvalidScriptException { this(script2.getScript(), script2.scriptEngineLookup, script2.getParameters(), script2.getScriptName()); } /** Create a script from another script object * @param script2 script object source * @throws InvalidScriptException if the creation fails. */ public Script(Script<?> script2, String scriptName) throws InvalidScriptException { this(script2.script, script2.scriptEngineLookup, script2.parameters, scriptName); } /** * Get the script. * * @return the script. */ public String getScript() { return script; } /** * Get the script name. * * @return the script name. */ public String getScriptName() { return scriptName; } /** * Set the script content, ie the executed code * * @param script the new script content */ public void setScript(String script) { this.script = script; } /** * Get the parameters. * * @return the parameters. */ public Serializable[] getParameters() { return parameters; } /** * Execute the script and return the ScriptResult corresponding. * Will use {@link java.lang.System#out} for output. * * @return a ScriptResult object. */ public ScriptResult<E> execute() { return execute(null, System.out, System.err); } /** * Execute the script and return the ScriptResult corresponding. * This method can add an additional user bindings if needed. * * @param aBindings the additional user bindings to add if needed. Can be null or empty. * @param outputSink where the script output is printed to. * @param errorSink where the script error stream is printed to. * @return a ScriptResult object. */ public ScriptResult<E> execute(Map<String, Object> aBindings, PrintStream outputSink, PrintStream errorSink) { ScriptEngine engine = createScriptEngine(); if (engine == null) return new ScriptResult<>(new Exception("No Script Engine Found for name or extension " + scriptEngineLookup)); // SCHEDULING-1532: redirect script output to a buffer (keep the latest DEFAULT_OUTPUT_MAX_SIZE) BoundedStringWriter outputBoundedWriter = new BoundedStringWriter(outputSink, DEFAULT_OUTPUT_MAX_SIZE); BoundedStringWriter errorBoundedWriter = new BoundedStringWriter(errorSink, DEFAULT_OUTPUT_MAX_SIZE); engine.getContext().setWriter(new PrintWriter(outputBoundedWriter)); engine.getContext().setErrorWriter(new PrintWriter(errorBoundedWriter)); Reader closedInput = new Reader() { @Override public int read(char[] cbuf, int off, int len) throws IOException { throw new IOException("closed"); } @Override public void close() throws IOException { } }; engine.getContext().setReader(closedInput); engine.getContext().setAttribute(ScriptEngine.FILENAME, scriptName, ScriptContext.ENGINE_SCOPE); try { Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE); //add additional bindings if (aBindings != null) { for (Entry<String, Object> e : aBindings.entrySet()) { bindings.put(e.getKey(), e.getValue()); } } prepareBindings(bindings); Object evalResult = engine.eval(getReader()); engine.getContext().getErrorWriter().flush(); engine.getContext().getWriter().flush(); // Add output to the script result ScriptResult<E> result = this.getResult(evalResult, bindings); result.setOutput(outputBoundedWriter.toString()); return result; } catch (javax.script.ScriptException e) { // drop exception cause as it might not be serializable ScriptException scriptException = new ScriptException(e.getMessage()); scriptException.setStackTrace(e.getStackTrace()); return new ScriptResult<>(scriptException); } catch (Throwable t) { String stack = Throwables.getStackTraceAsString(t); if (t.getMessage() != null) { stack = t.getMessage() + System.lineSeparator() + stack; } return new ScriptResult<>(new Exception(stack)); } } /** String identifying the script. * @return a String identifying the script. */ public String getId() { return this.id; } /** The reader used to read the script. */ protected Reader getReader() { return new StringReader(this.script); } /** The Script Engine used to evaluate the script. */ protected ScriptEngine createScriptEngine() { for (ScriptEngineFactory factory : new ScriptEngineManager().getEngineFactories()) { for (String name : factory.getNames()) { if (name.equalsIgnoreCase(scriptEngineLookup)) { return factory.getScriptEngine(); } } for (String ext : factory.getExtensions()) { String scriptEngineLookupLowercase = scriptEngineLookup.toLowerCase(); if (scriptEngineLookupLowercase.equalsIgnoreCase(ext.toLowerCase())) { return factory.getScriptEngine(); } } } return null; } /** * Specify the variable awaited from the script execution * @param bindings **/ protected abstract void prepareSpecialBindings(Bindings bindings); /** Return the variable awaited from the script execution */ protected abstract ScriptResult<E> getResult(Object evalResult, Bindings bindings); /** Set parameters in bindings if any */ protected final void prepareBindings(Bindings bindings) { //add parameters if (this.parameters != null) { bindings.put(Script.ARGUMENTS_NAME, this.parameters); } // add special bindings this.prepareSpecialBindings(bindings); } /** Create string script from url */ protected void storeScript(URL url) throws IOException { try (BufferedReader buf = new BufferedReader(new InputStreamReader(url.openStream()))) { StringBuilder builder = new StringBuilder(); String tmp; while ((tmp = buf.readLine()) != null) { builder.append(tmp).append("\n"); } script = builder.toString(); } } /** Create string script from file */ public static String readFile(File file) throws IOException { try (BufferedReader buf = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) { StringBuilder builder = new StringBuilder(); String tmp; while ((tmp = buf.readLine()) != null) { builder.append(tmp).append("\n"); } return builder.toString(); } } public String getEngineName() { return scriptEngineLookup; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((id == null) ? 0 : id.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof Script)) return false; Script<E> other = (Script<E>) obj; if (this.getId() == null) { if (other.getId() != null) return false; } else if (!this.getId().equals(other.getId())) return false; return true; } /** * Get MD5 hash value of the script without parameters */ public static String digest(String script) { try { return new String(MessageDigest.getInstance(MD5).digest(script.getBytes())); } catch (NoSuchAlgorithmException e) { logger.error("No algorithm found, digest will use the script content", e); return script; } } @Override public String toString() { return getScriptName(); } public String display() { String nl = System.lineSeparator(); return " { " + nl + "Script '" + getScriptName() + '\'' + nl + "\tscriptEngineLookup = '" + scriptEngineLookup + '\'' + nl + "\tscript = " + nl + script + nl + "\tid = " + nl + id + nl + "\tparameters = " + Arrays.toString(parameters) + nl + '}'; } public void overrideDefaultScriptName(String defaultScriptName) { if (getScriptName().equals(getDefaultScriptName())) { scriptName = defaultScriptName; } } }