/**
*
* 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.things;
import com.freedomotic.behaviors.BehaviorLogic;
import com.freedomotic.app.Freedomotic;
import com.freedomotic.bus.BusService;
import com.freedomotic.core.Resolver;
import com.freedomotic.core.SynchAction;
import com.freedomotic.core.SynchThingRequest;
import com.freedomotic.environment.EnvironmentLogic;
import com.freedomotic.environment.EnvironmentRepository;
import com.freedomotic.environment.ZoneLogic;
import com.freedomotic.events.ObjectHasChangedBehavior;
import com.freedomotic.exceptions.VariableResolutionException;
import com.freedomotic.model.ds.Config;
import com.freedomotic.model.geometry.FreedomPolygon;
import com.freedomotic.model.geometry.FreedomShape;
import com.freedomotic.model.object.EnvObject;
import com.freedomotic.model.object.Representation;
import com.freedomotic.reactions.Command;
import com.freedomotic.reactions.CommandRepository;
import com.freedomotic.reactions.Reaction;
import com.freedomotic.reactions.ReactionRepository;
import com.freedomotic.rules.Statement;
import com.freedomotic.reactions.Trigger;
import com.freedomotic.reactions.TriggerRepository;
import com.freedomotic.util.TopologyUtils;
import com.google.inject.Inject;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.shiro.authz.annotation.RequiresPermissions;
/**
*
* @author Enrico Nicoletti
*/
public class EnvObjectLogic {
private static final Logger LOG = LoggerFactory.getLogger(EnvObjectLogic.class.getName());
private EnvObject pojo;
private boolean changed;
private Map<String, Command> commandsMapping; //mapping between action name -> hardware command instance
private Map<String, BehaviorLogic> behaviors = new HashMap<>();
private EnvironmentLogic environment;
@Inject
protected EnvironmentRepository environmentRepository;
@Inject
protected TriggerRepository triggerRepository;
@Inject
protected CommandRepository commandRepository;
@Inject
protected ReactionRepository reactionRepository;
@Inject
private BusService busService;
/**
* Instantiation disabled from outside its package. Use
* {@code EnvObjectFactory} to generate instances of {@code EnvObjectLogic}
*/
protected EnvObjectLogic() {
super();
}
/**
* Gets the hardware command mapped to the action in input for example:
* Action -> Hardware Command Turn on -> Turn on light with X10 Actuator
* Turn off -> Turn off light with X10 Actuator
*
* @param action
* @return a Command or null if action doesn't exist or the mapping is not
* valid
*/
@RequiresPermissions("objects:read")
public final Command getHardwareCommand(String action) {
if ((action != null) && (!action.trim().isEmpty())) {
Command commandToSearch = commandsMapping.get(action.trim().toLowerCase());
if (commandToSearch != null) {
return commandToSearch;
} else {
LOG.error("Doesn''t exists a valid hardware command associated to action \"{}\" of thing \"{}"
+ "\". \n"
+ "These are the available mappings between action -> command for thing \"{}\": {}",
new Object[]{action, pojo.getName(), pojo.getName(), commandsMapping.toString()});
return null;
}
} else {
LOG.error("The action \"{}\" is not valid in thing \"{}\"",
new Object[]{action, pojo.getName()});
return null;
}
}
/**
* Create an HashMap with all object properties useful in an event
*
* @return a set of key/values of object properties
*/
@RequiresPermissions("objects:read")
public Map<String, String> getExposedProperties() {
HashMap<String, String> result = pojo.getExposedProperties();
return result;
}
/**
*
* @return
*/
@RequiresPermissions("objects:read")
public Map<String, String> getExposedBehaviors() {
Map<String, String> result = new HashMap<>();
for (BehaviorLogic behavior : getBehaviors()) {
result.put("object.behavior." + behavior.getName(),
behavior.getValueAsString());
}
return result;
}
/**
*
* @param newName
*/
@RequiresPermissions("objects:update")
public final void rename(String newName) {
String oldName = this.getPojo().getName();
newName = newName.trim();
LOG.warn("Renaming thing \"{}\" in \"{}\"", new Object[]{oldName, newName});
//change the object name
this.getPojo().setName(newName);
//change trigger references to this thing
for (Trigger t : triggerRepository.findAll()) {
renameValuesInTrigger(t, oldName, newName);
}
//change commands references to this thing
for (Command c : commandRepository.findUserCommands()) {
renameValuesInCommand(c, oldName, newName);
}
//rebuild reactions description
for (Reaction r : reactionRepository.findAll()) {
r.setChanged();
}
}
/**
*
* @param action
* @param command
*/
@RequiresPermissions("objects:update")
public void setAction(String action, Command command) {
if ((action != null) && !action.isEmpty() && (command != null)) {
commandsMapping.put(action.trim(),
command);
pojo.getActions().setProperty(action.trim(),
command.getName());
}
}
/**
*
* @param trigger
* @param behaviorName
*/
@RequiresPermissions("objects:update")
public void addTriggerMapping(Trigger trigger, String behaviorName) {
//checking input parameters
if ((behaviorName == null) || behaviorName.isEmpty() || (trigger == null)) {
throw new IllegalArgumentException("Behavior name and trigger cannot be null");
}
//parameters in input are ok, continue...
Iterator<Entry<String, String>> it = pojo.getTriggers().entrySet().iterator();
//remove old references if any
while (it.hasNext()) {
Entry<String, String> e = it.next();
if (e.getValue().equals(behaviorName)) {
it.remove(); //remove the old value that had to be updated
}
}
pojo.getTriggers().setProperty(trigger.getName(), behaviorName);
LOG.info("Trigger mapping in thing \"{}\": behavior \"{}\" is now associated to trigger named \"{}\"",
new Object[]{this.getPojo().getName(), behaviorName, trigger.getName()});
}
/**
*
* @param t
* @return
*/
@RequiresPermissions("objects:read")
public String getBehaviorNameMappedToTrigger(String t) {
return getPojo().getTriggers().getProperty(t);
}
/**
* Notify that this Thing was created, deleted or updated. To just notify an
* update is better to use the {@link setChanged(true)} method.
*
* @param action
*/
public void setChanged(SynchAction action) {
switch (action) {
case CREATED:
SynchThingRequest creationEvent = new SynchThingRequest(SynchAction.CREATED, getPojo());
busService.send(creationEvent);
break;
case DELETED:
SynchThingRequest deletionEvent = new SynchThingRequest(SynchAction.DELETED, getPojo());
busService.send(deletionEvent);
break;
case UPDATED:
// do nothing, the update is forced later
break;
default:
throw new AssertionError(action.name());
}
setChanged(true); //force the update in any case
}
/**
*
* @param value
*/
@RequiresPermissions("objects:update")
public synchronized void setChanged(boolean value) {
if (value) {
this.changed = true;
ObjectHasChangedBehavior objectEvent = new ObjectHasChangedBehavior(this, this);
//send multicast because an event must be received by all triggers registred on the destination channel
if (LOG.isDebugEnabled()) {
LOG.debug("Thing \"{}\" changes something in its status (eg: a behavior value)",
this.getPojo().getName());
}
busService.send(objectEvent);
} else {
changed = false;
}
}
/**
* When defining an object logic the registration of its behaviors is needed
* otherwise they are not used.
*
* @param b
*/
@RequiresPermissions("objects:update")
public final void registerBehavior(BehaviorLogic b) {
if (behaviors.get(b.getName()) != null) {
behaviors.remove(b.getName());
LOG.warn("Re-registering existing behavior \"{}\" in thing \"{}\"", b.getName(), this.getPojo().getName());
//throw new IllegalArgumentException("Impossible to register behavior " + b.getName() + " in object "
// + this.getPojo().getName() + " because it is already registed");
}
behaviors.put(b.getName(), b);
}
/**
* Finds a behavior using its name (case sensitive)
*
* @param name
* @return the reference to the behavior or null if it doesn't exists
*/
@RequiresPermissions("objects:read")
public final BehaviorLogic getBehavior(String name) {
BehaviorLogic behaviorLogic = behaviors.get(name);
// Manage the case the behavior is not found
if (behaviorLogic == null) {
// Create a list of available behaviors
StringBuilder buff = new StringBuilder();
for (BehaviorLogic behavior : behaviors.values()) {
buff.append(behavior.getName()).append(" ");
}
// Print an user friendly message
LOG.error("Cannot find a behavior named \"{}\" for thing named \"{}\". "
+ "Available behaviors for this thing are: \"{}\"",
new Object[]{name, getPojo().getName(), buff.toString()});
}
return behaviorLogic;
}
/**
* Caches developers level commands and creates user level commands as
* specified in the createCommands() method of its subclasses
*/
@RequiresPermissions("objects:read")
public void init() {
//validation
if (pojo == null) {
throw new IllegalStateException("An object must have a valid pojo before initialization");
}
pojo.initTags();
createCommands();
createTriggers();
commandsMapping = new HashMap<>();
cacheDeveloperLevelCommand();
// assign object to an environment
this.setEnvironment(environmentRepository.findOne(pojo.getEnvironmentID()));
}
@Deprecated
@RequiresPermissions("objects:read")
private boolean isChanged() {
return changed;
}
/**
*
* @return
*/
@RequiresPermissions("objects:read")
public EnvironmentLogic getEnvironment() {
return this.environment;
}
/**
*
* @return
*/
@RequiresPermissions("objects:read")
public EnvObject getPojo() {
// if (pojo.getUUID() == null || auth.isPermitted("objects:read:" + pojo.getUUID().substring(0, 5))
// ) {
return pojo;
// }
// return null;
}
/**
*
*/
@RequiresPermissions("objects:delete")
public final void destroy() {
pojo = null;
commandsMapping.clear();
commandsMapping = null;
behaviors.clear();
behaviors = null;
}
/**
*
* @param obj
* @return
*/
@Override
@RequiresPermissions("objects:read")
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final EnvObjectLogic other = (EnvObjectLogic) obj;
if ((this.pojo != other.pojo) && ((this.pojo == null) || !this.pojo.equals(other.pojo))) {
return false;
}
return true;
}
/**
*
* @return
*/
@Override
@RequiresPermissions("objects:read")
public int hashCode() {
int hash = 7;
hash = (53 * hash) + ((this.pojo != null) ? this.pojo.hashCode() : 0);
return hash;
}
/**
*
* @return
*/
@RequiresPermissions("objects:read")
public Iterable<BehaviorLogic> getBehaviors() {
return behaviors.values();
}
/**
*
*/
@RequiresPermissions("objects:create")
public final void setRandomLocation() {
int randomX = 0 + (int) (Math.random() * environmentRepository.findAll().get(0).getPojo().getWidth());
int randomY = 0 + (int) (Math.random() * environmentRepository.findAll().get(0).getPojo().getHeight());
setLocation(randomX, randomY);
}
/**
*
* @param x
* @param y
*/
@RequiresPermissions("objects:update")
public void setLocation(int x, int y) {
for (Representation rep : getPojo().getRepresentations()) {
rep.setOffset(x, y);
}
updateTopology();
//commit the changes to this object
setChanged(true);
}
/**
* Sets the object location without invoking an object change notification
* An user should never use this method. It's needed by the framework and
* reserver for it's exclusive use.
*/
public void synchLocation(int x, int y) {
for (Representation rep : getPojo().getRepresentations()) {
rep.setOffset(x, y);
}
updateTopology();
}
@RequiresPermissions({"objects:read", "zones.update"})
private void updateTopology() {
FreedomShape shape = getPojo().getRepresentations().get(0).getShape();
int xoffset = getPojo().getCurrentRepresentation().getOffset().getX();
int yoffset = getPojo().getCurrentRepresentation().getOffset().getY();
//now apply offset to the shape
FreedomPolygon translatedObject = TopologyUtils.translate((FreedomPolygon) shape, xoffset, yoffset);
//REGRESSION
for (EnvironmentLogic locEnv : environmentRepository.findAll()) {
for (ZoneLogic zone : locEnv.getZones()) {
if (this.getEnvironment() == locEnv && TopologyUtils.intersects(translatedObject, zone.getPojo().getShape())) {
//add to the zones this object belongs
zone.getPojo().getObjects().add(this.getPojo());
if (LOG.isDebugEnabled()) {
LOG.debug("Thing \"{}\" is in zone \"{}\"", new Object[]{getPojo().getName(), zone.getPojo().getName()});
}
} else {
//remove from the zone
zone.getPojo().getObjects().remove(this.getPojo());
}
}
}
}
/**
* Changes a behavior value accordingly to the value property in the trigger
* in input without firing a command on hardware. It updates only the
* internal model
*
* @param trigger an hardware level trigger
* @return true if the values is applied successfully, false otherwise
*/
public final boolean executeTrigger(Trigger trigger) {
// Get the behavior name connected to the trigger in input
String behaviorName = getBehaviorNameMappedToTrigger(trigger.getName());
// If missing because it's not an hardware trigger check if it is specified in the trigger itself
if (behaviorName == null) {
//check if the behavior name is written in the trigger
behaviorName = trigger.getPayload().getStatements("behavior.name").isEmpty()
? "" : trigger.getPayload().getStatements("behavior.name").get(0).getValue();
if (behaviorName.isEmpty()) {
return false;
}
}
Statement valueStatement = trigger.getPayload().getStatements("behaviorValue").get(0);
if (valueStatement == null) {
LOG.warn(
"No value in hardware trigger \"{}\" to apply to behavior \"{}\" of thing \"{}\"",
new Object[]{trigger.getName(), behaviorName, getPojo().getName()});
return false;
}
LOG.info(
"Sensors notification \"{}\" is going to change \"{}\" behavior \"{}\" to \"{}\"",
new Object[]{trigger.getName(), getPojo().getName(), behaviorName, valueStatement.getValue()});
Config params = new Config();
params.setProperty("value", valueStatement.getValue());
// Validating the target behavior
BehaviorLogic behavior = getBehavior(behaviorName);
if (behavior != null) {
behavior.filterParams(params, false); //false means not fire commands, only change behavior value
} else {
LOG.error("Cannot apply trigger \"{}\" to thing \"{}\"", new Object[]{trigger.getName(), getPojo().getName()});
return false;
}
return true;
}
/**
* Executes the hardware command related to the action passed as paramenter
* using an user command.
*
* @param action the name of the action to executeCommand as defined in the
* object XML
* @param params parameters of the event that have started the reaction
* execution
* @return true if the command is succesfully executed by the actuator and
* false otherways
*/
@RequiresPermissions("objects:read")
protected final boolean executeCommand(final String action, final Config params) {
LOG.debug("Executing action \"{}\" of thing \"{}\"", new Object[]{action, getPojo().getName()});
if ("virtual".equalsIgnoreCase(getPojo().getActAs())) {
//it's a virtual object like a button, not needed real execution of a command
LOG.info(
"The thing \"{}\" act as virtual device so its hardware commands are not executed.",
getPojo().getName());
return true;
}
final Command command = getHardwareCommand(action.trim());
if (command == null) {
LOG.warn(
"The hardware level command for action \"{}\" in thing \"{}\" doesn''t exists or is not set",
new Object[]{action, pojo.getName()});
return false; //command not executed
}
//resolves developer level command parameters like myObjectName = "@event.object.name" -> myObjectName = "Light 1"
//in this case the parameter in the userLevelCommand are used as basis for the resolution process (the context)
//along with the parameters getted from the relative behavior (if exists)
if (LOG.isDebugEnabled()) {
LOG.debug("Environment object \"{}\" tries to \"{}\" itself using hardware command \"{}\"",
new Object[]{pojo.getName(), action, command.getName()});
}
Resolver resolver = new Resolver();
//adding a resolution context for object that owns this hardware level command. 'owner.' is the prefix of this context
resolver.addContext("", params);
resolver.addContext("owner.", getExposedProperties());
resolver.addContext("owner.", getExposedBehaviors());
try {
final Command resolvedCommand = resolver.resolve(command); //eg: turn on an X10 device
Command result;
//mark the command as not executed if it is supposed to not return
//an execution state value
if (Boolean.valueOf(command.getProperty("send-and-forget"))) {
LOG.info("Command \"{}\" is \"send-and-forget\". No execution result will be catched from plugin''s reply", resolvedCommand.getName());
resolvedCommand.setReplyTimeout(-1); //disable reply request
Freedomotic.sendCommand(resolvedCommand);
return false;
} else {
//10 seconds is the default timeout if not already set
if (resolvedCommand.getReplyTimeout() < 1) {
resolvedCommand.setReplyTimeout(10000); //enable reply request
}
result = Freedomotic.sendCommand(resolvedCommand); //blocking wait until timeout
}
if (result == null) {
LOG.warn("Received null reply after sending hardware command \"{}\"", resolvedCommand.getName());
} else if (result.isExecuted()) {
return true; //succesfully executed
}
} catch (CloneNotSupportedException ex) {
LOG.error(ex.getMessage());
} catch (VariableResolutionException ex) {
LOG.error(ex.getMessage());
}
return false; //command not executed
}
protected void createCommands() {
//default empty implementation
}
protected void createTriggers() {
//default empty implementation
}
/**
*
* @param pojo
*/
@RequiresPermissions("objects:update")
protected void setPojo(EnvObject pojo) {
this.pojo = pojo;
}
@RequiresPermissions({"objects:update", "triggers:update"})
private void renameValuesInTrigger(Trigger t, String oldName, String newName) {
if (!t.isHardwareLevel()) {
if (t.getName().contains(oldName)) {
t.setName(t.getName().replace(oldName, newName));
LOG.warn("Trigger name renamed to \"{}\"", t.getName());
}
Iterator<Statement> it = t.getPayload().iterator();
while (it.hasNext()) {
Statement statement = it.next();
if (statement.getValue().contains(oldName)) {
statement.setValue(statement.getValue().replace(oldName, newName));
LOG.warn("Trigger value in payload renamed to \"{}\"", statement.getValue());
}
}
}
}
@RequiresPermissions({"objects:read", "commands:update"})
private void renameValuesInCommand(Command c, String oldName, String newName) {
if (c.getName().contains(oldName)) {
c.setName(c.getName().replace(oldName, newName));
LOG.warn("Command name renamed to \"{}\"", c.getName());
}
if (c.getProperty("object") != null) {
if (c.getProperty("object").contains(oldName)) {
c.setProperty("object",
c.getProperty("object").replace(oldName, newName));
LOG.warn("Property \"object\" in command renamed to \"{}\"", c.getProperty("object"));
}
}
}
private void cacheDeveloperLevelCommand() {
if (commandsMapping == null) {
commandsMapping = new HashMap<>();
}
for (String action : pojo.getActions().stringPropertyNames()) {
String commandName = pojo.getActions().getProperty(action);
Command command;
List<Command> list = commandRepository.findByName(commandName);
if (!list.isEmpty()) {
command = list.get(0);
} else {
throw new RuntimeException("No commands found with name \"" + commandName + "\"");
}
if (command != null) {
LOG.debug(
"Caching the command \"{}\" as related to action \"{}\" ",
new Object[]{command.getName(), action});
setAction(action, command);
} else {
LOG.warn(
"Doesn''t exist a command called \"{}\". It's not possible to bound this command to action \"{}\" of \"{}\"",
new Object[]{commandName, action, this.getPojo().getName()});
}
}
}
/**
*
* @param selEnv
*/
@RequiresPermissions("objects:update")
public void setEnvironment(EnvironmentLogic selEnv) {
if (selEnv == null) {
LOG.warn("Trying to assign a null environment to thing \"" + this.getPojo().getName()
+ "\". It will be relocated to the fallback environment");
selEnv = environmentRepository.findAll().get(0);
if (selEnv == null) {
throw new IllegalArgumentException("Fallback environment is null for thing \"" + getPojo().getName() + "\"");
}
}
this.environment = selEnv;
getPojo().setEnvironmentID(selEnv.getPojo().getUUID());
// update topology information
updateTopology();
}
/**
*
* @param tagList
*/
@RequiresPermissions("objects:update")
public void addTags(String tagList) {
String[] tags = tagList.toLowerCase().split(",");
getPojo().getTagsList().addAll(Arrays.asList(tags));
}
}