/**
*
* Copyright (c) 2009-2016 Freedomotic team http://freedomotic.com
*
* This file is part of Freedomotic
*
* 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 2, or (at your option) 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
* Freedomotic; see the file COPYING. If not, see
* <http://www.gnu.org/licenses/>.
*/
package com.freedomotic.core;
import com.freedomotic.exceptions.VariableResolutionException;
import com.freedomotic.model.ds.Config;
import com.freedomotic.reactions.Command;
import com.freedomotic.rules.Payload;
import com.freedomotic.reactions.Reaction;
import com.freedomotic.rules.Statement;
import com.freedomotic.reactions.Trigger;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
/**
* Resolves command values using an event as the context of resolution eg: param
* sensorPlugin = "
*
* @event.sender" becomes sensorPlugin="TemperatureSensorPlugin"
*
* <p>
* This class takes a list of properties in form key=value and propagates this
* list to all commands in a given reaction. After that this list (called
* context) is used to resolve the references to external values in the command.
* For example Commmand: turn on this x10 device x10-object =
* @event.object.name x10-address =
* @event.object.address this are resolved according to the parameters in the
* event that has fired the reaction containing the command 'turn on this x10
* device' in this case the event can be something like 'object receive click on
* the GUI' with paramenter object = Light 1 click = SINGLE_CLICK </p>
*
* @author Enrico Nicoletti
*/
public final class Resolver {
private static final Logger LOG = LoggerFactory.getLogger(Resolver.class.getName());
private List<String> namespaces = new ArrayList<>();
private Payload context;
/**
* Creates an empty resolution context
*/
public Resolver() {
this.context = new Payload();
}
/**
* Creates a resolved clone of the reaction in input. All commands in the
* reaction are resolved according to the context given in the contructor.
*
* @param reaction
* @return a clone of the resolver reaction
*/
public Reaction resolve(Reaction reaction) {
if ((context != null) && (reaction != null)) {
return (new Reaction(reaction.getTrigger(), performSubstitutionInCommands(reaction.getCommands())));
}
return null;
}
/**
* Creates a resolved clone of the command in input according to the current
* context given in input to the constructor.
*
* @param command
* @return
* @throws java.lang.CloneNotSupportedException
* @throws com.freedomotic.exceptions.VariableResolutionException
*/
public Command resolve(Command command)
throws CloneNotSupportedException, VariableResolutionException {
if ((context != null) && (command != null)) {
Command clone = command.clone();
mergeContextParamsIntoCommand(clone);
performSubstitutionInCommand(clone);
this.clear();
return clone;
}
return null;
}
/**
* Creates a resolved clone of the trigger in input according to the current
* context given in input to the constructor.
*
* @param trigger
* @return
* @throws com.freedomotic.exceptions.VariableResolutionException
*/
public Trigger resolve(Trigger trigger) throws VariableResolutionException {
if ((context != null) && (trigger != null)) {
Trigger clone = trigger.clone();
mergeContextParamsIntoTrigger(clone);
performSubstitutionInTrigger(clone);
this.clear();
return clone;
}
return null;
}
/*
* makes all commands in a reaction inherits the event parameters. after
* that resolves the properties of the command eg: if a command properties
* is: device = @event.device it becomes: device = P01 translating
* @event.device to the name of the device which has generated the event
*/
private List<Command> performSubstitutionInCommands(List<Command> commands) {
//clone reaction to not affect the original commands with temporary values
//construct a cloned reaction
List<Command> tmp = new ArrayList<>();
try {
for (Command originalCommand : commands) {
//resolving values using context data
Command clonedCmd = resolve(originalCommand);
tmp.add(clonedCmd);
}
} catch (Exception e) {
LOG.error("Error while substituting variables", e);
}
return tmp;
}
/**
* search in a command attribute for a pattern
*
* @event.VARIABLE_NAME and replace it with the real value from event
* Payload p
*
* @param command
*/
private void performSubstitutionInCommand(Command command) throws VariableResolutionException {
for (Map.Entry aProperty : command.getProperties().entrySet()) {
String key = (String) aProperty.getKey();
String propertyValue = (String) aProperty.getValue();
for (final String namespace : namespaces) {
String regex = "@" + namespace + "[.A-Za-z0-9_-]*\\b(#)?";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(propertyValue);
while (matcher.find()) {
String occurrence = matcher.group();
//re-read the property each loop because is possible the previous loop has already replaced something
propertyValue = (String) aProperty.getValue();
String referenceToResolve = occurrence;
if (occurrence.endsWith("#")) {
//cutting out the optional last '#'
referenceToResolve = referenceToResolve.substring(0, referenceToResolve.length() - 1);
}
//cutting out the first char '@'
referenceToResolve
= referenceToResolve.substring(1,
referenceToResolve.length());
String replacer = command.getProperty(referenceToResolve);
if (((replacer != null) && !replacer.isEmpty())) {
String propertyValueResolved = propertyValue.replaceFirst(occurrence, replacer);
aProperty.setValue(propertyValueResolved);
} else {
throw new VariableResolutionException("Variable '" + referenceToResolve
+ "' cannot be resolved in command '" + command.getName() + "'.\n"
+ "Availabe tokens are: " + context.toString());
}
}
}
//all references are replaced with real values in the current property, now perform scripting
String possibleScript = (String) aProperty.getValue();
boolean success = false;
if (possibleScript.startsWith("=")) {
//this is a javascript
try {
ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine js = mgr.getEngineByName("JavaScript");
String script = possibleScript.substring(1); //removing equal sign on the head
if (js == null) {
LOG.error("Cannot instantiate a JavaScript engine");
}
try {
js.eval(script);
} catch (ScriptException scriptException) {
LOG.error(scriptException.getMessage());
}
if (js.get(key) == null) {
LOG.error(
"Script evaluation has returned a null value, maybe the key ''{}'' is not evaluated properly.",
key);
}
aProperty.setValue(js.get(key).toString());
success = true;
} catch (Exception ex) {
success = false;
LOG.error("Error while evaluating script in command", ex);
}
}
if (!success) {
aProperty.setValue(possibleScript);
}
}
}
/**
* Searches in a trigger attribute for a pattern
*
* @event.VARIABLE_NAME and replace it with the real value from event
* Payload p
*
* @param trigger
*/
private void performSubstitutionInTrigger(Trigger trigger) throws VariableResolutionException {
Iterator it = trigger.getPayload().iterator();
while (it.hasNext()) {
Statement statement = (Statement) it.next();
String key = statement.getAttribute();
String propertyValue = statement.getValue();
for (final String PREFIX : namespaces) {
Pattern pattern = Pattern.compile("@" + PREFIX + "[.A-Za-z0-9_-]*\\b(#)?"); //find any @token
Matcher matcher = pattern.matcher(propertyValue);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(result, "");
String tokenKey = matcher.group();
if (tokenKey.endsWith("#")) {
tokenKey = tokenKey.substring(0, tokenKey.length() - 1); //cutting out the optional last '#'
}
tokenKey
= tokenKey.substring(1,
tokenKey.length()); //cutting out the first char '@'
String tokenValue = trigger.getPayload().getStatementValue(tokenKey);
if (tokenValue == null) {
throw new VariableResolutionException("Variable '" + tokenValue + "' cannot be resolved in trigger '"
+ trigger.getName() + "'.\n" + "Availabe tokens are: "
+ context.toString());
}
//replace an @token.property with its real value
result.append(tokenValue);
}
matcher.appendTail(result);
statement.setValue(result.toString());
}
//all references are replaced with real values in the current statement, now perform scripting
String possibleScript = statement.getValue().trim();
boolean success = false;
if (possibleScript.startsWith("=")) {
//this is a javascript
try {
ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine js = mgr.getEngineByName("JavaScript");
//removing equal sign on the head
String script = possibleScript.substring(1);
if (js == null) {
LOG.error("Cannot instatiate a JavaScript engine");
}
try {
js.eval(script);
} catch (ScriptException scriptException) {
LOG.error(scriptException.getMessage());
}
if (js.get(key) == null) {
LOG.error(
"Script evaluation in trigger \"{}\" has returned a null value, maybe the key \"{}\" is not evaluated properly.",
new Object[]{trigger.getName(), key});
}
statement.setValue(js.get(key).toString());
success = true;
} catch (Exception ex) {
success = false;
LOG.error(ex.getMessage());
}
}
if (!success) {
//fall back to the value before scripting evaluation
statement.setValue(possibleScript);
}
}
}
/**
*
*
* @param c
*/
private void mergeContextParamsIntoCommand(Command c) {
//adding parameters to command parameters with a prefix
Iterator<Statement> it = context.iterator();
while (it.hasNext()) {
Statement statement = it.next();
c.setProperty(statement.getAttribute(), statement.getValue());
}
}
/**
*
*
* @param t
*/
private void mergeContextParamsIntoTrigger(Trigger t) {
//adding parameters to command parameters with a prefix
t.getPayload().merge(context);
}
/**
*
* @param prefix
* @param aContext
*/
public void addContext(final String prefix, final Config aContext) {
if (context == null) {
context = new Payload();
}
//registering the new prefix
if (!namespaces.contains(prefix)) {
namespaces.add(prefix);
}
Set entries = aContext.getProperties().entrySet();
Iterator it = entries.iterator();
while (it.hasNext()) {
String key;
Map.Entry entry = (Map.Entry) it.next();
//removing the prefix of the properties if already exists
//to avoid dublicate prefixes like @event.event.object.name
if (entry.getKey().toString().startsWith(prefix)) {
key = entry.getKey().toString().substring(prefix.length());
} else {
key = entry.getKey().toString();
}
context.addStatement(prefix + key,
entry.getValue().toString());
}
}
/**
*
* @param prefix
* @param aContext
*/
public void addContext(final String prefix, final Map<String, String> aContext) {
if (context == null) {
context = new Payload();
}
//registering the new prefix
if (!namespaces.contains(prefix)) {
namespaces.add(prefix);
}
Iterator it = aContext.entrySet().iterator();
while (it.hasNext()) {
String key;
Entry entry = (Entry) it.next();
//removing the prefix of the properties if already exists
//to avoid duplicate prefixes like @event.event.object.name
if (entry.getKey().toString().startsWith(prefix)) {
key = entry.getKey().toString().substring(prefix.length());
} else {
key = entry.getKey().toString();
}
context.addStatement(prefix + key, entry.getValue().toString());
}
}
/**
*
* @param prefix
* @param aContext
*/
public void addContext(final String prefix, final Payload aContext) {
if (context == null) {
context = new Payload();
}
//registering the new prefix
if (!namespaces.contains(prefix)) {
namespaces.add(prefix);
}
// get an hold on the statements list mutex to avoid others to use it
synchronized (aContext.getStatements()) {
Iterator it = aContext.iterator();
while (it.hasNext()) {
String key;
Statement statement = (Statement) it.next();
//removing the prefix of the properties if already exists
//to avoid dublicate prefixes like @event.event.object.name
if (statement.getAttribute().startsWith(prefix)) {
key = statement.getAttribute().substring(prefix.length());
} else {
key = statement.getAttribute();
}
context.addStatement(prefix + key, statement.getValue());
}
}
}
/**
*
*
*/
private void clear() {
namespaces.clear();
context.clear();
}
}