/***********************************************************************************
*
* Copyright (c) 2014 Kamil Baczkowicz
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* Contributors:
*
* Kamil Baczkowicz - initial API and implementation and/or initial documentation
*
*/
package pl.baczkowicz.spy.scripts;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import javax.script.Bindings;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pl.baczkowicz.spy.common.generated.ScriptDetails;
import pl.baczkowicz.spy.eventbus.IKBus;
import pl.baczkowicz.spy.exceptions.CriticalException;
import pl.baczkowicz.spy.exceptions.SpyException;
import pl.baczkowicz.spy.files.FileUtils;
import pl.baczkowicz.spy.messages.IBaseMessage;
/**
* This class manages script creation and execution.
*/
public abstract class BaseScriptManager
{
/** Name of the variable in JS for received messages. */
public static final String RECEIVED_MESSAGE_PARAMETER = "receivedMessage";
/** Name of the variable in JS for published/searched message. */
public static final String MESSAGE_PARAMETER = "message";
public static final String SCRIPT_EXTENSION = ".js";
public static final String BEFORE_METHOD = "before";
public static final String AFTER_METHOD = "after";
public static final String ON_MESSAGE_METHOD = "onMessage";
/** Diagnostic logger. */
private final static Logger logger = LoggerFactory.getLogger(BaseScriptManager.class);
/** Mapping between unique script names and scripts. */
private Map<String, Script> scripts = new HashMap<String, Script>();
/** Used for notifying events related to script execution. */
protected IKBus eventBus;
/** Executor for tasks. */
protected Executor executor;
private Map<String, Object> customParameters;
/**
* Creates the script manager.
*
* @param eventBus The event bus to be used
* @param executor The executor to be used
* @param connection The connection for which to run the scripts
*/
public BaseScriptManager(final IKBus eventBus, final Executor executor)
{
this.eventBus = eventBus;
this.executor = executor;
}
/**
* Gets the file (script) name for the given file object.
*
* @param file The file from which to get the filename
*
* @return The name of the script file
*/
public static String getScriptName(final File file)
{
return file.getName().replace(SCRIPT_EXTENSION, "");
}
/**
* Gets the file (script) name for the given file object, including the subdirectory it's in.
*
* @param file The file from which to get the filename
*
* @return The name of the script file, including the subdirectory
*/
public static String getScriptNameWithSubdirectory(final File file, final String rootDirectory)
{
final String filePathSeparator = System.getProperty("file.separator");
final String valueToReplace = rootDirectory.endsWith(filePathSeparator) ? rootDirectory : rootDirectory + filePathSeparator;
return file.getAbsolutePath().replace(valueToReplace, "").replace(file.getName(), getScriptName(file));
}
/**
* Creates and records a script with the given details.
*
* @param scriptDetails The script details
*
* @return Created script
*/
public Script addScript(final ScriptDetails scriptDetails)
{
final File scriptFile = new File(scriptDetails.getFile());
final Script script = new Script();
createFileBasedScript(script, scriptFile, scriptDetails);
logger.info("Adding script {} at {}", script.getName(), scriptFile.getAbsolutePath());
scripts.put(scriptFile.getAbsolutePath(), script);
return script;
}
public Script addScript(final String scriptLocation)
{
return addScript(new ScriptDetails(false, false, scriptLocation));
}
/**
* Creates and records a script with the given details.
*
* @param scriptName The script name
* @param content Script's content
*
* @return Created script
*/
public Script addInlineScript(final String scriptName, final String content)
{
final Script script = new Script();
script.setScriptContent(content);
script.setScriptDetails(new ScriptDetails(true, false, null));
createScript(script, scriptName);
logger.debug("Adding in-line script {}", scriptName);
scripts.put(scriptName, script);
return script;
}
/**
* Creates and records scripts with the given details.
*
* @param scriptDetails The script details
*/
public List<Script> addScripts(final List<ScriptDetails> scriptDetails)
{
final List<Script> addedScripts = new ArrayList<>();
for (final ScriptDetails script : scriptDetails)
{
addedScripts.add(addScript(script));
}
return addedScripts;
}
/**
* Adds scripts from the given directory.
*
* @param directory The directory to search for scripts
*/
public void addScripts(final String directory)
{
final List<File> files = new ArrayList<File>();
if (directory != null && !directory.isEmpty())
{
files.addAll(FileUtils.getFileNamesForDirectory(directory, SCRIPT_EXTENSION));
}
else
{
logger.error("Given directory is empty");
}
populateScriptsFromFileList(files);
}
/**
* Populates scripts from a list of files. This doesn't override existing files.
*
* @param files List of script files
*
* @return The list of created (newly added) script objects
*/
public List<Script> populateScriptsFromFileList(final List<File> files)
{
final List<Script> addedScripts = new ArrayList<>();
for (final File scriptFile : files)
{
Script script = scripts.get(Script.getScriptIdFromFile(scriptFile));
if (script == null)
{
script = new Script();
createFileBasedScript(script, scriptFile, new ScriptDetails(true, false, scriptFile.getName()));
addedScripts.add(script);
addScript(script);
}
}
return addedScripts;
}
/**
* Populates scripts from a list of script details.
*
* @param scriptDetails List of script details
*
* @return The list of created script objects
*/
public List<Script> populateScripts(final List<ScriptDetails> scriptDetails)
{
final List<Script> addedScripts = new ArrayList<>();
for (final ScriptDetails details : scriptDetails)
{
final File scriptFile = new File(details.getFile());
if (!scriptFile.exists())
{
logger.warn("Script {} does not exist!", details.getFile());
}
else
{
logger.info("Adding script {}", details.getFile());
Script script = scripts.get(Script.getScriptIdFromFile(scriptFile));
if (script == null)
{
script = new Script();
createFileBasedScript(script, scriptFile, details);
addedScripts.add(script);
addScript(script);
}
}
}
return addedScripts;
}
/**
* Populates the script object with the necessary values and references.
*
* @param script The script object to be populated
* @param scriptFile The script's file name
* @param connection The connection for which this script will be running
* @param scriptDetails Script details
*/
public void createFileBasedScript(final Script script, final File scriptFile, final ScriptDetails scriptDetails)
{
final String scriptName = BaseScriptManager.getScriptName(scriptFile);
createScript(script, scriptName);
script.setScriptFile(scriptFile);
script.setScriptDetails(scriptDetails);
}
public void setVariable(final Script script, final String name, final Object value)
{
script.getScriptEngine().put(name, value);
}
/**
* Populates the script object with the necessary values and references.
*
* @param script The script object to be populated
* @param scriptName The name of the script
*/
public void createScript(final Script script, final String scriptName)
{
try
{
final ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName("nashorn");
if (scriptEngine != null)
{
script.setName(scriptName);
script.setStatusAndNotify(ScriptRunningState.NOT_STARTED);
script.setScriptEngine(scriptEngine);
populateEngineVariables(script);
// final MqttScriptIO scriptIO = new MqttScriptIO(connection, eventManager, script, executor);
// //script.setScriptIO(scriptIO);
//
// final Map<String, Object> scriptVariables = new HashMap<String, Object>();
//
// // This should be considered deprecated
// scriptVariables.put("mqttspy", scriptIO);
// // This should be used for general script-related actions
// scriptVariables.put("spy", scriptIO);
// // Going forward, this should only have mqtt-specific elements, e.g. pub/sub
// scriptVariables.put("mqtt", scriptIO);
//
// scriptVariables.put("logger", LoggerFactory.getLogger(ScriptRunner.class));
//
// final IMqttMessageLogIO mqttMessageLog = new MqttMessageLogIO();
// // Add it to the script IO so that it gets stopped when requested
// script.addTask(mqttMessageLog);
// scriptVariables.put("messageLog", mqttMessageLog);
//
// putJavaVariablesIntoEngine(scriptEngine, scriptVariables);
}
else
{
throw new CriticalException("Cannot instantiate the nashorn javascript engine - most likely you don't have Java 8 installed. "
+ "Please either disable scripts in your configuration file or install the appropriate JRE/JDK.");
}
}
catch (SpyException e)
{
throw new CriticalException("Cannot initialise the script objects");
}
}
abstract public void populateEngineVariables(final Script script) throws SpyException;
/**
* Puts a the given map of variables into the engine.
*
* @param engine The engine to be populated with variables
* @param variables The variables to be populated
*/
public static void putJavaVariablesIntoEngine(final ScriptEngine engine, final Map<String, Object> variables)
{
final Bindings bindings = new SimpleBindings();
// final Bindings bindings = engine.createBindings();
for (String key : variables.keySet())
{
bindings.put(key, variables.get(key));
}
engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
}
/**
* Runs the given script in a synchronous or asynchronous way.
*
* @param script The script to run
* @param asynchronous Whether to run the script asynchronously or not
*/
public void runScript(final Script script, final boolean asynchronous)
{
runScript(script, asynchronous, null);
}
/**
* Runs the given script in a synchronous or asynchronous way.
*
* @param script The script to run
* @param asynchronous Whether to run the script asynchronously or not
* @param args Arguments/parameters passed onto the script
*/
public void runScript(final Script script, final boolean asynchronous, final Map<String, Object> args)
{
// Only start if not running already
if (!ScriptRunningState.RUNNING.equals(script.getStatus()))
{
script.createScriptRunner(eventBus, executor);
script.setAsynchronous(asynchronous);
final Map<String, Object> scriptArgs = new HashMap<>();
if (args != null)
{
scriptArgs.putAll(args);
}
if (customParameters != null)
{
scriptArgs.putAll(customParameters);
}
setVariable(script, "args", scriptArgs);
if (asynchronous)
{
new Thread(script.getScriptRunner()).start();
}
else
{
script.getScriptRunner().run();
}
}
}
public Script addAndRunScript(final String scriptLocation, final boolean async, final Map<String, Object> args)
{
final Script script = addScript(scriptLocation);
runScript(script, async, args);
return script;
}
/**
* Runs the given script and passes the given message as a parameter. Defaults to the 'receivedMessage' parameter and synchronous execution.
*
* @param script The script to run
* @param message The message to be passed onto the script
*/
public void runScriptFileWithReceivedMessage(final String scriptFile, final IBaseMessage receivedMessage)
{
final Script script = getScriptObjectFromName(scriptFile);
if (script != null)
{
runScriptFileParameter(script, BaseScriptManager.RECEIVED_MESSAGE_PARAMETER, receivedMessage, false);
}
else
{
logger.warn("No script file found at {}. Please check if this location is correct.", scriptFile);
}
}
/**
* Runs the given script and passes the given message as a parameter. Defaults to the 'receivedMessage' parameter and synchronous execution.
*
* @param script The script to run
* @param message The message to be passed onto the script
*/
public boolean runScriptWithReceivedMessage(final Script script, final IBaseMessage receivedMessage)
{
setVariable(script, BaseScriptManager.RECEIVED_MESSAGE_PARAMETER, receivedMessage);
try
{
invokeFunction(script, ON_MESSAGE_METHOD);
return true;
}
catch (NoSuchMethodException | ScriptException e)
{
return false;
}
}
/**
* Runs the given script and passes the given message as a parameter. Defaults to the 'message' parameter and asynchronous execution.
*
* @param script The script to run
* @param message The message to be passed onto the script
*/
public void runScriptFileWithMessage(final Script script, final IBaseMessage message)
{
runScriptFileParameter(script, BaseScriptManager.MESSAGE_PARAMETER, message, true);
}
/**
* Runs the given script and passes the given object as a parameter.
*
* @param script The script to run
* @param parameterName The name of the message parameter
* @param message The message to be passed onto the script
* @param asynchronous Whether the call should be asynchronous
*/
public void runScriptFileParameter(final Script script, final String parameterName, final Object parameter, final boolean asynchronous)
{
setVariable(script, parameterName, parameter);
runScript(script, asynchronous);
}
public boolean invokeBefore(final Script script)
{
try
{
invokeFunction(script, BaseScriptManager.BEFORE_METHOD);
}
catch (NoSuchMethodException e)
{
logger.info("No setup function present");
}
catch (ScriptException e)
{
logger.error("Function execution failure", e);
return false;
}
return true;
}
public boolean invokeAfter(final Script script)
{
try
{
invokeFunction(script, BaseScriptManager.AFTER_METHOD);
}
catch (NoSuchMethodException e)
{
logger.info("No after function present");
}
catch (ScriptException e)
{
logger.error("Function execution failure", e);
return false;
}
return true;
}
public Object invokeFunction(final Script script, final String function, final Object... args) throws NoSuchMethodException, ScriptException
{
final Invocable invocable = (Invocable) script.getScriptEngine();
Object result = null;
try
{
result = invocable.invokeFunction(function, args);
}
catch (NoSuchMethodException e)
{
throw e;
}
// Catch all - in case the script throws an undefined exception
catch (Exception e)
{
throw new ScriptException(e);
}
return result;
}
public void stopScript(final Script script)
{
logger.debug("Stopping script " + script.getName());
if (script.getScriptRunner() != null)
{
final Thread scriptThread = script.getScriptRunner().getThread();
if (scriptThread != null)
{
scriptThread.interrupt();
}
}
}
public void stopScripts()
{
// Stop all scripts
for (final Script script : getScripts())
{
// Only stop file-based scripts
if (script.getScriptFile() != null)
{
stopScript(script);
}
}
}
/**
* Gets script object for the given file.
*
* @param scriptFile The file for which to get the script object
*
* @return Script object or null if not found
*/
public Script getScriptObjectFromName(final String scriptFile)
{
return scripts.get(scriptFile);
}
/**
* Checks if any of the scripts is running.
*
* @return True if any of the scripts is running
*/
public boolean areScriptsRunning()
{
for (final Script script : scripts.values())
{
if (ScriptRunningState.RUNNING.equals(script.getStatus()))
{
logger.debug("Script {} is still running", script.getName());
return true;
}
}
return false;
}
/**
* Gets the name to script object mapping.
*
* @return Script name to object mapping
*/
public Map<String, Script> getScriptsMap()
{
return scripts;
}
/**
* Gets the collection of scripts.
*
* @return All scripts
*/
public Collection<Script> getScripts()
{
return scripts.values();
}
public void addScript(final Script script)
{
scripts.put(script.getScriptId(), script);
}
public boolean containsScript(final Script script)
{
return scripts.containsKey(script.getScriptId());
}
public void addCustomParameters(final Map<String, Object> parameters)
{
this.customParameters = parameters;
}
}