com.freedomotic.core.TriggerCheck.java Source code

Java tutorial

Introduction

Here is the source code for com.freedomotic.core.TriggerCheck.java

Source

/**
 *
 * 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 java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.freedomotic.api.EventTemplate;
import com.freedomotic.app.Freedomotic;
import com.freedomotic.behaviors.BehaviorLogic;
import com.freedomotic.bus.BusService;
import com.freedomotic.events.MessageEvent;
import com.freedomotic.exceptions.FreedomoticException;
import com.freedomotic.exceptions.RepositoryException;
import com.freedomotic.exceptions.VariableResolutionException;
import com.freedomotic.reactions.Command;
import com.freedomotic.reactions.Reaction;
import com.freedomotic.reactions.ReactionRepository;
import com.freedomotic.reactions.Trigger;
import com.freedomotic.rules.ExpressionFactory;
import com.freedomotic.rules.Payload;
import com.freedomotic.rules.Statement;
import com.freedomotic.things.EnvObjectLogic;
import com.freedomotic.things.ThingRepository;
import com.google.inject.Inject;

/**
 *
 * @author Enrico Nicoletti
 */
public class TriggerCheck {

    private static final Logger LOG = LoggerFactory.getLogger(TriggerCheck.class.getName());
    private static final ExecutorService AUTOMATION_EXECUTOR = Executors.newCachedThreadPool();
    // Dependencies
    private final Autodiscovery autodiscovery;
    private final BusService busService;
    private final ThingRepository thingsRepository;
    private final ReactionRepository reactionRepository;
    private final BehaviorManager behaviorManager;
    private static final Pattern PATTERN = Pattern.compile("\\[(.*?)\\]\\.+[0-9A-Za-z]");

    @Inject
    TriggerCheck(Autodiscovery autodiscovery, ThingRepository thingsRepository, BusService busService,
            BehaviorManager behaviorManager, ReactionRepository reactionRepository) {
        this.autodiscovery = autodiscovery;
        this.thingsRepository = thingsRepository;
        this.busService = busService;
        this.behaviorManager = behaviorManager;
        this.reactionRepository = reactionRepository;
    }

    /**
     * Executes trigger-event comparison in a separated thread
     *
     * @param event
     * @param trigger
     * @return
     */
    public boolean check(final EventTemplate event, final Trigger trigger) {
        if ((event == null) || (trigger == null)) {
            throw new IllegalArgumentException("Event and Trigger cannot be null while performing trigger check");
        }

        try {
            Trigger resolved = resolveTrigger(event, trigger);

            if (trigger.isHardwareLevel() && resolved != null && resolved.isConsistentWith(event)) {
                LOG.debug(
                        "[CONSISTENT] hardware level trigger \"{} {}\"\nconsistent with received event \"{}\" \"{}\"",
                        resolved.getName(), this.getPayload(resolved.getPayload()), event.getEventName(),
                        this.getPayload(event.getPayload()));
                applySensorNotification(resolved, event);
                return true;
            } else {
                if (trigger.canFire() && resolved != null && resolved.isConsistentWith(event)) {
                    LOG.debug("[CONSISTENT] registered trigger \"{} {}\"\nconsistent with received event ''{}'' {}",
                            resolved.getName(), this.getPayload(resolved.getPayload()), event.getEventName(),
                            this.getPayload(event.getPayload()));
                    executeTriggeredAutomations(resolved, event);
                    return true;
                }
            }

            //if we are here the trigger is not consistent
            if (LOG.isDebugEnabled()) {
                LOG.debug(
                        "[NOT CONSISTENT] registered trigger \"{} {}\"\nnot consistent with received event ''{}'' {}",
                        trigger.getName(), trigger.getPayload().toString(), event.getEventName(),
                        event.getPayload().toString());
            }
            return false;
        } catch (Exception e) {
            LOG.error("Error while performing trigger check", e);
            return false;
        }
    }

    //it returns the payload as string, if any.
    private String getPayload(Payload payload) {
        return (payload != null) ? payload.toString() : "";
    }

    /**
     *
     *
     * @param event
     * @param trigger
     * @return
     * @throws VariableResolutionException
     */
    private Trigger resolveTrigger(final EventTemplate event, final Trigger trigger)
            throws VariableResolutionException {
        Resolver resolver = new Resolver();
        resolver.addContext("event.", event.getPayload());
        return resolver.resolve(trigger);
    }

    /**
     *
     *
     * @param resolved
     * @param event
     */
    private void applySensorNotification(Trigger resolved, final EventTemplate event) {
        String protocol = null;
        String address = null;
        EnvObjectLogic affectedObject = null;

        if (!(resolved.getPayload().getStatements("event.protocol").isEmpty()
                && resolved.getPayload().getStatements("event.address").isEmpty())) {
            //join device: add the object on the map if not already there
            //join device requires to know 'object.class' and 'object.name' properties
            protocol = resolved.getPayload().getStatements("event.protocol").get(0).getValue();
            address = resolved.getPayload().getStatements("event.address").get(0).getValue();
        }

        if ((protocol != null) && (address != null)) {
            affectedObject = this.getAffectedObject(event, protocol, address);
        }

        //uses trigger->behavior mapping to apply the trigger to this object
        if (affectedObject != null && affectedObject.executeTrigger(resolved)) {
            long elapsedTime = System.currentTimeMillis() - event.getCreation();
            LOG.info("Sensor notification \"{}\" applied to thing \"{}\" in {} ms.", resolved.getName(),
                    affectedObject.getPojo().getName(), elapsedTime);
        } else {
            LOG.warn("Hardware trigger \"{}\" is not associated to any thing.", resolved.getName());
        }
        resolved.getPayload().clear();
        event.getPayload().clear();
    }

    private EnvObjectLogic getAffectedObject(EventTemplate event, String protocol, String address) {
        String clazz = event.getProperty("object.class");
        String name = event.getProperty("object.name");
        String autodiscoveryAllowClones = event.getProperty("autodiscovery.allow-clones");

        EnvObjectLogic affectedObject = thingsRepository.findByAddress(protocol, address);

        if (affectedObject == null) { //there isn't an object with this protocol and address
            LOG.warn("Found a candidate for things autodiscovery: thing \"{}\" of type \"{}\"", name, clazz);
            if ((clazz != null) && !clazz.isEmpty()) {
                boolean allowClones = ("false".equalsIgnoreCase(autodiscoveryAllowClones)) ? false : true;
                try {
                    affectedObject = autodiscovery.join(clazz, name, protocol, address, allowClones);
                } catch (RepositoryException ex) {
                    LOG.error(Freedomotic.getStackTraceInfo(ex));
                }
            }
        }
        return affectedObject;
    }

    /**
     *
     *
     * @param trigger
     * @param event
     */
    private void executeTriggeredAutomations(final Trigger trigger, final EventTemplate event) {
        AUTOMATION_EXECUTOR.execute(() -> {
            //Searching for reactions using this trigger
            Iterator<Reaction> reactIterator = new CopyOnWriteArrayList<Reaction>(reactionRepository.findAll())
                    .listIterator();

            while (reactIterator.hasNext()) {
                Reaction reaction = reactIterator.next();
                Trigger reactionTrigger = reaction.getTrigger();
                //found a related reaction. This must be executed
                if (trigger.equals(reactionTrigger) && !reaction.getCommands().isEmpty()) {
                    if (!checkAdditionalConditions(reaction)) {
                        LOG.info("Additional conditions test failed in reaction \"{}\"",
                                reaction.getShortDescription());
                        return;
                    }
                    reactionTrigger.setExecuted();

                    LOG.debug("Try to execute reaction \"{}\"", reaction.toString());

                    //executes the commands in sequence (only the first sequence is used) 
                    //if more then one sequence is needed it can be done with two reactions with the same trigger
                    Resolver commandResolver = new Resolver();
                    event.getPayload().addStatement("description", trigger.getDescription()); //embedd the trigger description to the event payload
                    commandResolver.addContext("event.", event.getPayload());

                    if (!this.processReactionCommands(event, reaction, commandResolver))
                        return;

                    String info = "Executing automation \"" + reaction.toString() + "\" takes "
                            + (System.currentTimeMillis() - event.getCreation()) + "ms.";
                    LOG.info(info);

                    MessageEvent message = new MessageEvent(null, info);
                    message.setType("callout"); //display as callout on frontends
                    busService.send(message);
                }

                else {
                    LOG.info("No valid reaction {} bound to trigger \"{}\"", reaction.getShortDescription(),
                            trigger.getName());
                }
            }

            trigger.getPayload().clear();
            event.getPayload().clear();
        });
    }

    private boolean processReactionCommands(final EventTemplate event, Reaction reaction,
            Resolver commandResolver) {
        try {
            for (final Command command : reaction.getCommands()) {
                if (!this.processCommand(event, command, commandResolver))
                    return false;
            }
        } catch (Exception e) {
            LOG.error("Exception while merging event parameters into reaction.", e);
            return false;
        }

        return true;
    }

    private boolean processCommand(final EventTemplate event, final Command command, Resolver commandResolver)
            throws VariableResolutionException, CloneNotSupportedException {

        if (command == null) {
            return true;
        }

        if (command.getReceiver().equalsIgnoreCase(BehaviorManager.getMessagingChannel())) {
            //this command is for an object so it needs only to know only about event parameters
            Command resolvedCommand = commandResolver.resolve(command);
            //doing so we bypass messaging system gaining better performances
            behaviorManager.parseCommand(resolvedCommand);
            return true;
        }

        //if the event has a target object we include also object info
        List<EnvObjectLogic> targetObjects = thingsRepository.findByName(event.getProperty("object.name"));

        if (!targetObjects.isEmpty()) {
            EnvObjectLogic targetObject = targetObjects.get(0);
            commandResolver.addContext("current.", targetObject.getExposedProperties());
            commandResolver.addContext("current.", targetObject.getExposedBehaviors());
        }

        final Command resolvedCommand = commandResolver.resolve(command);
        //it's not a user level command for objects (eg: turn it on), it is for another kind of actuator
        Command reply = busService.send(resolvedCommand); //blocking wait until executed

        if (reply == null) {
            command.setExecuted(false);
            LOG.warn("Unreceived reply within given time ({} ms) for command \"{}\"", command.getReplyTimeout(),
                    command.getName());
            notifyMessage("Unreceived reply within given time for command \"" + command.getName() + "\"");
        } else {
            if (reply.isExecuted()) {
                //the reply is executed so mark the origial command as executed as well
                command.setExecuted(true);
                LOG.debug("Executed successfully \"{}\"", command.getName());
            } else {
                command.setExecuted(false);
                LOG.warn("Unable to execute command \"{}\". Skipping the others", command.getName());
                notifyMessage("Unable to execute command \"" + command.getName() + "\"");
                // skip the other commands
                return false;
            }
        }

        return true;
    }

    /**
     * Resolves the additional conditions of the reaction in input. Now it just
     * takes the statement attribute and value and check if they are equal to
     * the target behavior name and value respectively. This should be improved
     * to allow also REGEX and other statement resolution.
     */
    private boolean checkAdditionalConditions(Reaction rea) {
        boolean result = true;
        for (Condition condition : rea.getConditions()) {
            EnvObjectLogic object = thingsRepository.findByName(condition.getTarget()).get(0);
            Statement statement = condition.getStatement();
            if (object != null) {
                String attributeValue = object.getBehavior(statement.getAttribute()).getValueAsString();
                String operand = statement.getOperand();
                String value = statement.getValue();
                boolean isAndCondition = "AND".equalsIgnoreCase(statement.getLogical());

                if (StringUtils.isBlank(value))
                    value = "";

                //check if value is an external object behavior eg. <value>[object name].temperature</value>
                //or a target object behavior <value>[].temperature</value> 
                try {
                    value = this.getCurrentValueFromBehavior(value, object);
                } catch (FreedomoticException e) {
                    LOG.warn(e.getMessage());
                    return false;
                }

                // if attributeValue and value are float and operand not "EQUALS" we must convert them to integer
                if ((isDecimalNumber(attributeValue) || isDecimalNumber(value))
                        && !("EQUALS".equalsIgnoreCase(operand))) {
                    attributeValue = this.increaseStringValue(attributeValue);
                    value = this.increaseStringValue(value);
                }

                boolean eval = (boolean) new ExpressionFactory<>().createExpression(attributeValue, operand, value)
                        .evaluate();
                result = executeBooleanExpression(isAndCondition, result, eval);
            }
        } // all is ok
        return result;
    }

    private String increaseStringValue(String value) {
        return String.valueOf((int) Float.parseFloat(value) * 10);
    }

    private String getCurrentValueFromBehavior(String currValue, EnvObjectLogic object)
            throws FreedomoticException {
        String currentValue = currValue;
        Matcher m = PATTERN.matcher(currentValue);
        if (m.find()) {
            BehaviorLogic behavior = null;
            String valueBehavior = currentValue.substring(currentValue.indexOf('.') + 1);
            // in this case we consider the target object behavior 
            if (currentValue.startsWith("[]")) {
                behavior = object.getBehavior(valueBehavior);
            } else {
                String thingName = currentValue.substring(currentValue.indexOf('[') + 1, currentValue.indexOf(']'));
                List<EnvObjectLogic> newObject = thingsRepository.findByName(thingName);
                if (newObject.isEmpty() || newObject.get(0).getBehavior(valueBehavior) == null) {
                    throw new FreedomoticException("Cannot test condition on unexistent thing: " + thingName);
                } else {
                    behavior = newObject.get(0).getBehavior(valueBehavior);
                }
            }

            if (behavior != null) {
                currentValue = behavior.getValueAsString();
            }
        }
        return currentValue;
    }

    private boolean executeBooleanExpression(boolean isAndCondition, boolean result, boolean eval) {
        if (isAndCondition) {
            return result && eval;
        } else {
            return result || eval;
        }
    }

    /**
     * Checks if a string represents a decimal number.
     *
     * @param number string to check
     * @return true if it's a decimal number, false otherwise
     */
    private boolean isDecimalNumber(String number) {
        String decimalPattern = "([0-9]*)\\.([0-9]*)";
        return Pattern.matches(decimalPattern, number);
    }

    /**
     * Notifies a message as callout on frontends.
     *
     * @param message message to notify
     */
    private void notifyMessage(String message) {
        MessageEvent event = new MessageEvent(this, message);
        event.setType("callout"); //display as callout on frontends
        busService.send(event);
    }
}