/**
* Copyright (c) 2011-2014, OpenIoT
*
* This file is part of OpenIoT.
*
* OpenIoT is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* OpenIoT 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with OpenIoT. If not, see <http://www.gnu.org/licenses/>.
*
* Contact: OpenIoT mailto: info@openiot.eu
* @author Timotee Maret
* @author Sofiane Sarni
*/
package org.openiot.gsn.processor;
import groovy.lang.*;
import org.openiot.gsn.beans.DataField;
import org.openiot.gsn.beans.StreamElement;
import org.openiot.gsn.vsensor.AbstractVirtualSensor;
import org.apache.log4j.Logger;
import java.io.Serializable;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TreeMap;
/**
* This Processor (processing class) executes a scriptlet upon reception of a new StreamElement and can be used to
* implement arbitrary complex processing class by specifying its logic directly in the virtual sensor description file.
* This is especially useful for setting up flexible, complex and DBMS independent calibration functions.
* The current implementation supports the Groovy scripting language @see http://groovy.codehaus.org .
* <p/>
* Data Binding
* ------------
* The current implementation automatically binds the data between the StreamElement and the variables of the scriptlet.
* The binding is based on the mapping between the StreamElement data field names, and the scriptlet variable names.
* <p/>
* Before executing the scriptlet, all the fields from the StreamElement received are binded to the scriptlet.
* The scriptlet is then executed and could use both variables hard-coded or dynamically binded from the StreamElement.
* Once the scriptlet execution is done, a new StreamElement matching the output sctructure defined in the virtual
* sensor description file is created. The data of this StreamElement are binded from all the variables binded to the
* scriptlet. If a field name exists in the output sctructure and no variable match it in the scriptlet, then its value
* is set to null.
* <p/>
* State
* -----
* <p/>
* In order to save the state of a variable for the next evaluation, the following code is automatically added to your
* script:
* <p/>
* def isdef(var) {
* (binding.getVariables().containsKey(var))
* }
* <p/>
* It can be used for initializing or updating a variable like below:
* <p/>
* statefulCounter = isdef('statefulCounter') ? statefulCounter + 1 : 0;
* <p/>
* <p/>
* PREDEFINED VARIABLES
* --------------------
* <p/>
* The following variable is accessible directly in the scriptlet:
* <p/>
* 1. groovy.lang.Binding binding This contains the variables binded to your scriptlet.
* <p/>
* PROCESSING CLASS INIT-PARAMETERS
* --------------------------------
* <ul>
* <li>
* scriptlet, String, mandatory if scriptlet-periodic is not specified, optional otherwise<br/>
* Contains the content of your script executed upon reception of a new StreamElement.
* </li>
* <li>
* persistant, boolean, optional<br/>
* Sets wether or not a StreamElement is created and stored at the end of the scriplet execution.
* </li>
* <li>
* scriplet-periodic, String, mandatory if scriptlet is not specified, optional otherwise<br/>
* Contains the content of your script which is executed periodically at 'period' ms interval.
* </li>
* <li>
* period, long, mandatoryif scriptlet-periodic is specified<br/>
* Define the period (in ms) between two execution of the scriptlet-periodic script.
* </li>
* </ul>
* PERIODICAL EXECUTION
* --------------------
* <p>
* If some part of your script has to be executed at a periodical interval (and not upon reception of a new StreamElement),
* you must set this part of code in the 'scriptlet-periodic' parameter and set the interval in the 'period' parameter.
* Note that the 'script' and 'scriplet-periodic' scripts are never executed concurrently and that the state is shared
* among the two.
* Note that the StreamElement generated by the execution of the 'scriptlet-periodic' is NOT stored in the db, even if
* the 'persistant' parameter is enabled.
* </p>
* PREDEFINED SERVICES
* -------------------
* <p/>
* The following classes providing services are, by default, statically imported to your scriptlet and thus, all their
* static methods can be used directly in your scriptlet.
* <p/>
* 1. The {@link org.openiot.gsn.utils.services.EmailService} class provides access the Email notification services.
* 2. The {@link org.openiot.gsn.utils.services.TwitterService} class provides access to Twitter notifications services.
* <p/>
* LIMITATIONS
* -----------
* <p/>
* 1. The variables names binded into the script are uppercase.
*/
public class ScriptletProcessor extends AbstractVirtualSensor {
private static final transient Logger logger = Logger.getLogger(ScriptletProcessor.class);
private static final String PARAM_SCRIPTLET = "scriptlet";
private static final String PARAM_SCRIPTLETPERIODIC = "scriplet-periodic";
private static final String PARAM_PERIOD = "period";
private static final String PARAM_PERSITANT = "persistant";
private Timer timer = null;
/**
* This field holds the scriplet (state and logic) executed upon reception of a new {@link org.openiot.gsn.beans.StreamElement}.
*/
protected Script scriptlet = null;
/**
* This field holds the scriplet (state and logic) executed periodically.
*/
protected Script scriptletPeriodic = null;
/**
* This field holds the context (variable state) which is shared among
* the {@link org.openiot.gsn.processor.ScriptletProcessor#scriptlet}
* and {@link org.openiot.gsn.processor.ScriptletProcessor#scriptletPeriodic} fields.
*/
protected final Binding context = new Binding();
protected DataField[] outputStructure = null;
private long period = -1;
private boolean persistant = true;
private TimerTask periodicalTask = null;
@Override
public boolean initialize() {
return initialize(
getVirtualSensorConfiguration().getOutputStructure(),
getVirtualSensorConfiguration().getMainClassInitialParams()
);
}
@Override
public void dispose() {
if (periodicalTask != null)
periodicalTask.cancel();
}
@Override
public void dataAvailable(String inputStreamName, StreamElement se) {
evaluate(scriptlet, se, persistant);
}
protected boolean initialize(DataField[] outputStructure, TreeMap<String, String> parameters) {
if (outputStructure == null) {
logger.warn("Failed to initialize the processing class because the outputStructure is null.");
return false;
} else
this.outputStructure = outputStructure;
// Mandatory Parameters
String p = parameters.get(PARAM_PERIOD);
if (p != null) {
try {
period = Long.parseLong(p);
}
catch (Exception e) {
// ...
}
}
String ps1 = parameters.get(PARAM_SCRIPTLET);
if (ps1 != null) {
scriptlet = initScriptlet(ps1);
if (scriptlet == null)
return false;
}
String ps2 = parameters.get(PARAM_SCRIPTLETPERIODIC);
if (ps2 != null) {
scriptletPeriodic = initScriptlet(ps2);
if (scriptletPeriodic == null)
return false;
}
// At least one of the following is mandatory: {scriptlet, scriptlet-periodic}.
if (scriptlet == null && scriptletPeriodic == null) {
logger.warn("The Initial Parameter >" + PARAM_SCRIPTLET + "< or >" + PARAM_SCRIPTLETPERIODIC + "< MUST be provided in the configuration file for the processing class.");
return false;
}
if ((scriptletPeriodic != null && period < 0) || (scriptletPeriodic == null && period >= 0)) {
logger.warn("The Initial Parameters >" + PARAM_SCRIPTLETPERIODIC + "< and >" + PARAM_PERIOD + "< MUST be provided together in the configuration file for the processing class.");
return false;
}
// Optional Parameters
if (parameters.containsKey(PARAM_PERSITANT)) {
try {
persistant = Boolean.parseBoolean(parameters.get(PARAM_PERSITANT));
}
catch (Exception e) {
logger.debug(e.getMessage(), e);
}
}
// Add the periodical task to the timer if needed.
if (scriptletPeriodic != null && period >= 0) {
periodicalTask = new TimerTask() {
public void run() {
evaluate(scriptletPeriodic, null, false);
}
};
getTimer().schedule(periodicalTask, 0, period);
}
return true;
}
protected Script initScriptlet(String ps) {
StringBuilder scriptlet = new StringBuilder();
scriptlet.append("// start auto generated part --\n");
// Add the static import (for predefined services)
scriptlet.append("import static ").append(org.openiot.gsn.utils.services.EmailService.class.getCanonicalName()).append(".*;\n");
scriptlet.append("import static ").append(org.openiot.gsn.utils.services.TwitterService.class.getCanonicalName()).append(".*;\n");
// Add the syntactic sugars
scriptlet.append("def isdef(var){(binding.getVariables().containsKey(var))}\n");
scriptlet.append("// end auto generated part --\n");
// Append the scriplet from the parameter
scriptlet.append(ps);
//
GroovyShell shell = new GroovyShell();
Script script = null;
try {
script = shell.parse(scriptlet.toString());
logger.debug("Compiled script: \n" + scriptlet.toString());
}
catch (Exception e) {
logger.error("Failed to compile the scriptlet " + e.getMessage());
return null;
}
return script;
}
protected StreamElement formatOutputStreamElement(Binding binding) {
Serializable[] data = new Serializable[outputStructure.length];
for (int i = 0; i < outputStructure.length; i++) {
DataField df = outputStructure[i];
Object o = null;
try {
o = binding.getVariable(df.getName().toUpperCase());
}
catch (MissingPropertyException e) {
// ...
}
data[i] = (Serializable) o;
}
StreamElement seo = new StreamElement(outputStructure, data);
try {
Long timed = (Long) binding.getVariable("TIMED");
seo.setTimeStamp(timed);
}
catch (MissingPropertyException e) {
// ...
}
return seo;
}
protected Binding updateContext(StreamElement se) {
if (se != null) {
for (String fieldName : se.getFieldNames()) {
context.setVariable(fieldName.toUpperCase(), se.getData(fieldName));
}
context.setVariable("TIMED", se.getTimeStamp());
}
return context;
}
protected void evaluate(Script script, StreamElement se, boolean persist) {
StreamElement seo = null;
synchronized (context) {
updateContext(se);
script.setBinding(context);
script.run();
if (persist) {
seo = formatOutputStreamElement(context);
}
}
if (seo != null) {
dataProduced(seo);
}
}
private synchronized Timer getTimer() {
if (timer == null)
timer = new Timer(false);
return timer;
}
}