/** * 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; } }