/*
* eGov suite of products aim to improve the internal efficiency,transparency,
* accountability and the service delivery of the government organizations.
*
* Copyright (C) <2015> eGovernments Foundation
*
* The updated version of eGov suite of products as by eGovernments Foundation
* is available at http://www.egovernments.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/ or
* http://www.gnu.org/licenses/gpl.html .
*
* In addition to the terms of the GPL license to be adhered to in using this
* program, the following additional terms are to be complied with:
*
* 1) All versions of this program, verbatim or modified must carry this
* Legal Notice.
*
* 2) Any misrepresentation of the origin of the material is prohibited. It
* is required that all modified versions of this material be marked in
* reasonable ways as different from the original version.
*
* 3) This license does not grant any rights to any user of the program
* with regards to rights under trademark law for use of the trade names
* or trademarks of eGovernments Foundation.
*
* In case of any queries, you can reach eGovernments Foundation at contact@egovernments.org.
*/
package org.egov.infra.script.service;
import org.egov.infra.cache.impl.LRUCache;
import org.egov.infra.exception.ApplicationRuntimeException;
import org.egov.infra.script.entity.Script;
import org.egov.infra.script.repository.ScriptRepository;
import org.egov.infra.utils.DateUtils;
import org.egov.infra.validation.exception.ValidationError;
import org.egov.infra.validation.exception.ValidationException;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* Service for executing scripts. Caches the script engine and frequently used
* scripts to improve performance. Also provides methods that can be used from
* within scripts to invoke other scripts or functions from already loaded
* scripts. <br>
* <br>
* Code snippet: <br>
*
* <pre>
* <code>scriptService.executeScript("script name", ScriptService.createContext("var1",
* value1, "var2", value2));
* </code>
* </pre>
*/
@Transactional(readOnly=true)
@Service
public class ScriptService {
private static LRUCache<String, Script> scriptCache;
private static final Logger LOG = LoggerFactory.getLogger(ScriptService.class);
@Autowired
private ScriptRepository scriptRepository;
@Autowired
private ScriptEngineProvider scriptEngineProvider;
public Script getByName(String name) {
return scriptRepository.findByName(name);
}
public Script findByNameAndPeriod( String name, Date period) {
return scriptRepository.findByNameAndPeriod(name, period);
}
public ScriptService() {
if (scriptCache == null)
scriptCache = new LRUCache<String, Script>(10, 50);
}
/**
* Takes an even number of arguments, and creates a
* <code>ScriptContext</code> object, with ith argument as the key and
* (i+1)th argument as the value. This is why this method expects an even
* number of arguments.
*
* @param args
* Arguments from which the context will be created
* @return The script context in which a script can be executed. This can be
* passed as second argument to the method
* {@link ScriptService#executeScript(String, ScriptContext)}
*/
public static ScriptContext createContext(final Object... args) {
final SimpleScriptContext context = new SimpleScriptContext();
if (args.length % 2 != 0)
throw new ApplicationRuntimeException("Number of arguments must be even");
for (int i = 0; i < args.length; i += 2)
context.setAttribute((String) args[i], args[i + 1], ScriptContext.ENGINE_SCOPE);
return context;
}
/**
* Loads all functions from given script.
*
* @param scriptName
* Script which is to be loaded
*/
public void loadFunctionsFromScript(final String scriptName) {
final Script script = getScript(scriptName, DateUtils.today());
final ScriptEngine engine = scriptEngineProvider.getScriptEngine(script.getType());
executeScript(script, engine, engine.getContext());
}
/**
* Sets standard context attributes that can be used inside the script.
* These are: <br>
* <code>scriptService</code> - Instance of script service - can be used to
* invoke another script, load functions from a script, or execute an
* already loaded function.<br>
* <code>scriptEngine</code> - The script engine - to be passed as first
* argument to the
* {@link ScriptService#executeFunction(ScriptEngine, String, Object...)}
* <br>
* <code>scriptContext</code> - The script context - to be passed as second
* argument to the
* {@link ScriptService#executeScript(String, ScriptContext)} API<br>
*
* @param engine
* The script engine
* @param context
* The script context
*/
private void setupContextAttributes(final ScriptEngine engine, final ScriptContext context) {
context.setAttribute("scriptService", this, ScriptContext.ENGINE_SCOPE);
context.setAttribute("scriptEngine", engine, ScriptContext.ENGINE_SCOPE);
context.setAttribute("scriptContext", context, ScriptContext.ENGINE_SCOPE);
}
/**
* Executes the given script using given script engine and context
*
* @param script
* Script to be executed
* @param engine
* Engine to be used for executing the script
* @param context
* Context in which the script is to be executed
* @return The result from the script, or the value of the script variable
* "result"
* @throws ValidationException
* if the script returns a list of ValidationErrors
* @throws EGOVRuntimException
* if script execution throws an exception
*/
private Object executeScript(final Script script, final ScriptEngine engine, final ScriptContext context) {
if (context == null) {
// Context must be passed. We don't want to use the default engine
// context as it will keep on growing over a period of time
final String errMsg = "ScriptContext not passed to executeScript method!";
LOG.error(errMsg);
throw new ApplicationRuntimeException(errMsg);
}
Object evalResult = null;
// Set up standard context attributes
setupContextAttributes(engine, context);
try {
final CompiledScript compiledScript = script.getCompiledScript();
if (engine instanceof Compilable && compiledScript != null)
// Script engine supports compiled scripts.
// Execute the compiled script using given context.
evalResult = compiledScript.eval(context);
else
// Script engine doesn't support compiled scripts.
// Set the context on engine and execute the script.
evalResult = engine.eval(script.getScript(), context);
handleErrorsIfAny(context.getAttribute("validationErrors"));
// No errors. Get and return the result.
final Object result = context.getAttribute("result");
return evalResult == null ? result : evalResult;
} catch (final ScriptException e) {
LOG.error("script error for " + script.getType() + ":" + script.getName() + ":" + script.getScript(), e);
throw new ApplicationRuntimeException("script.error", e);
} catch (final ValidationException e) {
if(e.getErrors()!=null && !e.getErrors().isEmpty())
LOG.error(e.getErrors().get(0).getMessage());
throw e;
}
catch (final Exception e) {
LOG.error("Exception for " + script.getType() + ":" + script.getName() + ":" + script.getScript(), e);
throw new ApplicationRuntimeException("script.error", e);
}
}
/**
* Executes given function using given script engine
*
* @param engine
* Script engine to be used for executing the function
* @param functionName
* Name of function to be executed
* @return Return value from the function
*/
public Object executeFunctionNoArgs(final ScriptEngine engine, final String functionName) {
return executeFunction(engine, functionName);
}
/**
* Executes given function using given script engine
*
* @param engine
* Script engine to be used for executing the function
* @param functionName
* Name of function to be executed
* @param args
* Arguments to be passed to the function
* @return Return value from the function
*/
public Object executeFunction(final ScriptEngine engine, final String functionName, final Object... args) {
Object evalResult = null;
if (engine instanceof Invocable)
try {
evalResult = ((Invocable) engine).invokeFunction(functionName, args);
if (evalResult == null)
evalResult = engine.get("result");
} catch (final Exception e) {
final String errMsg = "Exception while invoking function [" + functionName + "]";
LOG.error(errMsg, e);
throw new ApplicationRuntimeException(errMsg, e);
}
else {
final String errMsg = "Script engine [" + engine + "] does not support method execution!";
LOG.error(errMsg);
throw new ApplicationRuntimeException(errMsg);
}
return evalResult;
}
/**
* Compiles given script if it is not already compiled, and if the
* corresponding script engine supports compilation
*
* @param script
* Script to be compiled
* @return The compiled script object. Returns null if the script engine
* doesn't support compilation.
*/
private CompiledScript compileScriptIfRequired(final Script script) {
CompiledScript compiledScript = script.getCompiledScript();
if (compiledScript != null)
return compiledScript;
final ScriptEngine engine = scriptEngineProvider.getScriptEngine(script.getType());
if (engine instanceof Compilable)
try {
// Script engine supports compilation
compiledScript = ((Compilable) engine).compile(script.getScript());
script.setCompiledScript(compiledScript);
// TODO: Add compiledScript column to SCRIPT table and persist
// the modified object. Problem: JythonCompiledScript is not
// serializable!
// scriptService.persist(script);
} catch (final ScriptException e) {
final String errMsg = "Could not compile script " + script.getType() + ":" + script.getName() + ":"
+ script.getScript();
LOG.error(errMsg, e);
throw new ApplicationRuntimeException(errMsg, e);
}
return compiledScript;
}
/**
* @param scriptName
* Script name
* @param asOnDate
* The date against which validity of the script is to be
* checked. If null, validity as of current date will be checked.
* @return Script object for given name
* @throws ApplicationRuntimeException
* if the script is not configured in the system
*/
private Script getScript(final String scriptName, Date asOnDate) {
Date currentDate = new Date();
if (asOnDate != null)
currentDate = asOnDate;
Script script = scriptCache.get(scriptName);
if (script != null && new DateTime(script.getPeriod().getEndDate()).isAfter(new DateTime(asOnDate.getTime())))
// Script found in cache and is still valid
return script;
// Script not available in cache. Try to fetch from database.
script = scriptRepository.findByNameAndPeriod(scriptName, currentDate);
if (script == null)
throw new ApplicationRuntimeException("Script [" + scriptName + "] not found!");
// Compile the script if required and possible
compileScriptIfRequired(script);
// Add the script to cache
scriptCache.put(scriptName, script);
return script;
}
/**
* Executes the given script with context as the ScriptContext. The values
* in the context are available as variables in the script. The names of
* these variables will be the attributes in the context.
*
* @param script
* Script to be executed
* @param context
* The script context - attributes in this context can be used as
* variables inside the script
* @return The result from the script, or the value of the script variable
* "result"
* @throws ValidationException
* if the script returns a list of ValidationErrors
*/
public Object executeScript(final Script script, final ScriptContext context) {
final ScriptEngine engine = scriptEngineProvider.getScriptEngine(script.getType());
return executeScript(script, engine, context);
}
/**
* Executes the given script with context as the ScriptContext. The values
* in the context are available as variables in the script. The names of
* these variables will be the attributes in the context
*
* @param scriptName
* Name of script to be executed
* @param context
* @return The result from the script, or the value of the script variable
* "result"
* @throws ValidationException
* , if the script returns a list of ValidationErrors
*/
public Object executeScript(final String scriptName, final ScriptContext context) {
return executeScript(getScript(scriptName, DateUtils.today()), context);
}
/**
* Executes the given script with context as the ScriptContext as on given
* date. This means that the script that was valid as on given date will be
* executed (not the currently valid one). The values in the context are
* available as variables in the script. The names of these variables will
* be the attributes in the context.
*
* @param scriptName
* Name of script to be executed
* @param context
* @param asOnDate
* The date as on which the script is to be executed
* @return The result from the script, or the value of the script variable
* "result"
* @throws ValidationException
* , if the script returns a list of ValidationErrors
*/
public Object executeScript(final String scriptName, final ScriptContext context, final Date asOnDate) {
return executeScript(getScript(scriptName, asOnDate), context);
}
/**
* Checks if the script has returned any validation errors. If yes, throws a
* new <code>ValidationException</code> containing the errors.
*
* @param errors
* The value of "validationErrors" attributes from script context
*/
private void handleErrorsIfAny(final Object errors) {
if (errors != null) {
List<ValidationError> validationErrors = null;
if (errors instanceof List)
validationErrors = (List<ValidationError>) errors;
else if (errors instanceof Map)
validationErrors = toErrors((Map) errors);
else
validationErrors = Arrays.asList(new ValidationError(errors.toString(), errors.toString()));
throw new ValidationException(validationErrors);
}
}
/**
* Converts a map of validation errors to a list of ValidationError objects
* assuming that key = error key and value = error message
*
* @param errors
* Map of validation errors
* @return List of ValidationError objects
*/
private List<ValidationError> toErrors(final Map errors) {
List<ValidationError> validationErrors;
validationErrors = new LinkedList<ValidationError>();
final Set<Entry> errorEntries = errors.entrySet();
for (final Entry entry : errorEntries)
validationErrors.add(new ValidationError(entry.getKey().toString(), entry.getValue().toString()));
return validationErrors;
}
/**
* Clears the script cache
*/
public void clearScriptCache() {
scriptCache.clear();
}
}