/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.model.rule.runtime.internal.engine;
import static org.eclipse.smarthome.model.rule.runtime.internal.engine.RuleTriggerManager.TriggerTypes.*;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.smarthome.core.items.Item;
import org.eclipse.smarthome.core.thing.ThingStatus;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.State;
import org.eclipse.smarthome.core.types.Type;
import org.eclipse.smarthome.core.types.TypeParser;
import org.eclipse.smarthome.model.rule.rules.ChangedEventTrigger;
import org.eclipse.smarthome.model.rule.rules.CommandEventTrigger;
import org.eclipse.smarthome.model.rule.rules.EventEmittedTrigger;
import org.eclipse.smarthome.model.rule.rules.EventTrigger;
import org.eclipse.smarthome.model.rule.rules.Rule;
import org.eclipse.smarthome.model.rule.rules.RuleModel;
import org.eclipse.smarthome.model.rule.rules.SystemOnShutdownTrigger;
import org.eclipse.smarthome.model.rule.rules.SystemOnStartupTrigger;
import org.eclipse.smarthome.model.rule.rules.ThingStateChangedEventTrigger;
import org.eclipse.smarthome.model.rule.rules.ThingStateUpdateEventTrigger;
import org.eclipse.smarthome.model.rule.rules.TimerTrigger;
import org.eclipse.smarthome.model.rule.rules.UpdateEventTrigger;
import org.quartz.CronScheduleBuilder;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.GroupMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Injector;
/**
* This is a helper class which deals with everything about rule triggers.
* It keeps lists of which rule must be executed for which trigger and takes
* over the evaluation of states and trigger conditions for the rule engine.
*
* @author Kai Kreuzer - Initial contribution and API
*
*/
public class RuleTriggerManager {
private final Logger logger = LoggerFactory.getLogger(RuleTriggerManager.class);
public enum TriggerTypes {
UPDATE, // fires whenever a status update is received for an item
CHANGE, // same as UPDATE, but only fires if the current item state is changed by the update
COMMAND, // fires whenever a command is received for an item
TRIGGER, // fires whenever a trigger is emitted on a channel
STARTUP, // fires when the rule engine bundle starts and once as soon as all required items are available
SHUTDOWN, // fires when the rule engine bundle is stopped
TIMER, // fires at a given time
THINGUPDATE, // fires whenever the thing state is updated.
THINGCHANGE, // fires if the thing state is changed by the update
}
// lookup maps for different triggering conditions
private Map<String, Set<Rule>> updateEventTriggeredRules = Maps.newHashMap();
private Map<String, Set<Rule>> changedEventTriggeredRules = Maps.newHashMap();
private Map<String, Set<Rule>> commandEventTriggeredRules = Maps.newHashMap();
private Map<String, Set<Rule>> thingUpdateEventTriggeredRules = Maps.newHashMap();
private Map<String, Set<Rule>> thingChangedEventTriggeredRules = Maps.newHashMap();
// Maps from channelName -> Rules
private Map<String, Set<Rule>> triggerEventTriggeredRules = Maps.newHashMap();
private Set<Rule> systemStartupTriggeredRules = new CopyOnWriteArraySet<>();
private Set<Rule> systemShutdownTriggeredRules = new CopyOnWriteArraySet<>();
private Set<Rule> timerEventTriggeredRules = new CopyOnWriteArraySet<>();
// the scheduler used for timer events
private Scheduler scheduler;
@Inject
public RuleTriggerManager(Injector injector) {
try {
scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.setJobFactory(injector.getInstance(GuiceAwareJobFactory.class));
// we want to defer timer rule execution until after the startup rules have been executed.
scheduler.standby();
} catch (SchedulerException e) {
logger.error("initializing scheduler throws exception", e);
}
}
/**
* Returns all rules which have a trigger of a given type
*
* @param type the trigger type of the rules to return
* @return rules with triggers of the given type
*/
public Iterable<Rule> getRules(TriggerTypes type) {
Iterable<Rule> result;
switch (type) {
case STARTUP:
result = systemStartupTriggeredRules;
break;
case SHUTDOWN:
result = systemShutdownTriggeredRules;
break;
case TIMER:
result = timerEventTriggeredRules;
break;
case UPDATE:
result = Iterables.concat(updateEventTriggeredRules.values());
break;
case CHANGE:
result = Iterables.concat(changedEventTriggeredRules.values());
break;
case COMMAND:
result = Iterables.concat(commandEventTriggeredRules.values());
break;
case TRIGGER:
result = Iterables.concat(triggerEventTriggeredRules.values());
break;
case THINGUPDATE:
result = Iterables.concat(thingUpdateEventTriggeredRules.values());
break;
case THINGCHANGE:
result = Iterables.concat(thingChangedEventTriggeredRules.values());
break;
default:
result = Sets.newHashSet();
}
List<Rule> filteredList = new ArrayList<>();
for (Rule rule : result) {
// we really only want to return rules that are still loaded
if (rule.eResource() != null && !rule.eIsProxy()) {
filteredList.add(rule);
}
}
return filteredList;
}
/**
* Returns all rules for which the trigger condition is true for the given type, item and state.
*
* @param triggerType
* @param item
* @param state
* @return all rules for which the trigger condition is true
*/
public Iterable<Rule> getRules(TriggerTypes triggerType, Item item, State state) {
return internalGetRules(triggerType, item, null, state);
}
/**
* Returns all rules for which the trigger condition is true for the given type, item and states.
*
* @param triggerType
* @param item
* @param oldState
* @param newState
* @return all rules for which the trigger condition is true
*/
public Iterable<Rule> getRules(TriggerTypes triggerType, Item item, State oldState, State newState) {
return internalGetRules(triggerType, item, oldState, newState);
}
/**
* Returns all rules for which the trigger condition is true for the given type, item and command.
*
* @param triggerType
* @param item
* @param command
* @return all rules for which the trigger condition is true
*/
public Iterable<Rule> getRules(TriggerTypes triggerType, Item item, Command command) {
return internalGetRules(triggerType, item, null, command);
}
/**
* Returns all rules for which the trigger condition is true for the given type and channel.
*
* @param triggerType
* @param channel
* @return all rules for which the trigger condition is true
*/
public Iterable<Rule> getRules(TriggerTypes triggerType, String channel, String event) {
List<Rule> result = Lists.newArrayList();
switch (triggerType) {
case TRIGGER:
Set<Rule> rules = triggerEventTriggeredRules.get(channel);
if (rules == null) {
return Collections.emptyList();
}
for (Rule rule : rules) {
for (EventTrigger t : rule.getEventtrigger()) {
if (t instanceof EventEmittedTrigger) {
EventEmittedTrigger et = (EventEmittedTrigger) t;
if (et.getChannel().equals(channel)
&& (et.getTrigger() == null || et.getTrigger().equals(event))) {
// if the rule does not have a specific event , execute it on any event
result.add(rule);
}
}
}
}
break;
default:
return Collections.emptyList();
}
return result;
}
public Iterable<Rule> getRules(TriggerTypes triggerType, String thingUid, ThingStatus state) {
return internalGetThingRules(triggerType, thingUid, null, state);
}
public Iterable<Rule> getRules(TriggerTypes triggerType, String thingUid, ThingStatus oldState,
ThingStatus newState) {
return internalGetThingRules(triggerType, thingUid, oldState, newState);
}
private Iterable<Rule> getAllRules(TriggerTypes type, String name) {
switch (type) {
case STARTUP:
return systemStartupTriggeredRules;
case SHUTDOWN:
return systemShutdownTriggeredRules;
case UPDATE:
return updateEventTriggeredRules.get(name);
case CHANGE:
return changedEventTriggeredRules.get(name);
case COMMAND:
return commandEventTriggeredRules.get(name);
case THINGUPDATE:
return thingUpdateEventTriggeredRules.get(name);
case THINGCHANGE:
return thingChangedEventTriggeredRules.get(name);
default:
return Sets.newHashSet();
}
}
private Iterable<Rule> internalGetRules(TriggerTypes triggerType, Item item, Type oldType, Type newType) {
List<Rule> result = Lists.newArrayList();
Iterable<Rule> rules = getAllRules(triggerType, item.getName());
if (rules == null) {
rules = Lists.newArrayList();
}
switch (triggerType) {
case STARTUP:
return systemStartupTriggeredRules;
case SHUTDOWN:
return systemShutdownTriggeredRules;
case TIMER:
return timerEventTriggeredRules;
case UPDATE:
if (newType instanceof State) {
State state = (State) newType;
for (Rule rule : rules) {
for (EventTrigger t : rule.getEventtrigger()) {
if (t instanceof UpdateEventTrigger) {
UpdateEventTrigger ut = (UpdateEventTrigger) t;
if (ut.getItem().equals(item.getName())) {
if (ut.getState() != null) {
State triggerState = TypeParser.parseState(item.getAcceptedDataTypes(),
ut.getState());
if (!state.equals(triggerState)) {
continue;
}
}
result.add(rule);
}
}
}
}
}
break;
case CHANGE:
if (newType instanceof State && oldType instanceof State) {
State newState = (State) newType;
State oldState = (State) oldType;
for (Rule rule : rules) {
for (EventTrigger t : rule.getEventtrigger()) {
if (t instanceof ChangedEventTrigger) {
ChangedEventTrigger ct = (ChangedEventTrigger) t;
if (ct.getItem().equals(item.getName())) {
if (ct.getOldState() != null) {
State triggerOldState = TypeParser.parseState(item.getAcceptedDataTypes(),
ct.getOldState());
if (!oldState.equals(triggerOldState)) {
continue;
}
}
if (ct.getNewState() != null) {
State triggerNewState = TypeParser.parseState(item.getAcceptedDataTypes(),
ct.getNewState());
if (!newState.equals(triggerNewState)) {
continue;
}
}
result.add(rule);
}
}
}
}
}
break;
case COMMAND:
if (newType instanceof Command) {
final Command command = (Command) newType;
for (Rule rule : rules) {
for (final EventTrigger t : rule.getEventtrigger()) {
if (t instanceof CommandEventTrigger) {
final CommandEventTrigger ct = (CommandEventTrigger) t;
if (ct.getItem().equals(item.getName())) {
if (ct.getCommand() != null) {
final Command triggerCommand = TypeParser
.parseCommand(item.getAcceptedCommandTypes(), ct.getCommand());
if (!command.equals(triggerCommand)) {
continue;
}
}
result.add(rule);
}
}
}
}
}
break;
default:
break;
}
return result;
}
private Iterable<Rule> internalGetThingRules(TriggerTypes triggerType, String thingUid, ThingStatus oldStatus,
ThingStatus newStatus) {
List<Rule> result = Lists.newArrayList();
Iterable<Rule> rules = getAllRules(triggerType, thingUid);
if (rules == null) {
rules = Lists.newArrayList();
}
switch (triggerType) {
case THINGUPDATE:
for (Rule rule : rules) {
for (EventTrigger t : rule.getEventtrigger()) {
if (t instanceof ThingStateUpdateEventTrigger) {
ThingStateUpdateEventTrigger tt = (ThingStateUpdateEventTrigger) t;
if (tt.getThing().equals(thingUid)) {
String stateString = tt.getState();
if (stateString != null) {
ThingStatus triggerState = ThingStatus.valueOf(stateString);
if (!newStatus.equals(triggerState)) {
continue;
}
}
result.add(rule);
}
}
}
}
break;
case THINGCHANGE:
for (Rule rule : rules) {
for (EventTrigger t : rule.getEventtrigger()) {
if (t instanceof ThingStateChangedEventTrigger) {
ThingStateChangedEventTrigger ct = (ThingStateChangedEventTrigger) t;
if (ct.getThing().equals(thingUid)) {
String oldStatusString = ct.getOldState();
if (oldStatusString != null) {
ThingStatus triggerOldState = ThingStatus.valueOf(oldStatusString);
if (!oldStatus.equals(triggerOldState)) {
continue;
}
}
String newStatusString = ct.getNewState();
if (newStatusString != null) {
ThingStatus triggerNewState = ThingStatus.valueOf(newStatusString);
if (!newStatus.equals(triggerNewState)) {
continue;
}
}
result.add(rule);
}
}
}
}
break;
default:
break;
}
return result;
}
/**
* Removes all rules with a given trigger type from the mapping tables.
*
* @param type the trigger type
*/
public void clear(TriggerTypes type) {
switch (type) {
case STARTUP:
systemStartupTriggeredRules.clear();
break;
case SHUTDOWN:
systemShutdownTriggeredRules.clear();
break;
case UPDATE:
updateEventTriggeredRules.clear();
break;
case CHANGE:
changedEventTriggeredRules.clear();
break;
case COMMAND:
commandEventTriggeredRules.clear();
break;
case TRIGGER:
triggerEventTriggeredRules.clear();
break;
case TIMER:
for (Rule rule : timerEventTriggeredRules) {
removeTimerRule(rule);
}
timerEventTriggeredRules.clear();
break;
case THINGUPDATE:
thingUpdateEventTriggeredRules.clear();
break;
case THINGCHANGE:
thingChangedEventTriggeredRules.clear();
break;
}
}
/**
* Removes all rules from all mapping tables.
*/
public void clearAll() {
clear(STARTUP);
clear(SHUTDOWN);
clear(UPDATE);
clear(CHANGE);
clear(COMMAND);
clear(TIMER);
clear(TRIGGER);
clear(THINGUPDATE);
clear(THINGCHANGE);
}
/**
* Adds a given rule to the mapping tables
*
* @param rule the rule to add
*/
public synchronized void addRule(Rule rule) {
for (EventTrigger t : rule.getEventtrigger()) {
// add the rule to the lookup map for the trigger kind
if (t instanceof SystemOnStartupTrigger) {
systemStartupTriggeredRules.add(rule);
} else if (t instanceof SystemOnShutdownTrigger) {
systemShutdownTriggeredRules.add(rule);
} else if (t instanceof CommandEventTrigger) {
CommandEventTrigger ceTrigger = (CommandEventTrigger) t;
Set<Rule> rules = commandEventTriggeredRules.get(ceTrigger.getItem());
if (rules == null) {
rules = new HashSet<Rule>();
commandEventTriggeredRules.put(ceTrigger.getItem(), rules);
}
rules.add(rule);
} else if (t instanceof UpdateEventTrigger) {
UpdateEventTrigger ueTrigger = (UpdateEventTrigger) t;
Set<Rule> rules = updateEventTriggeredRules.get(ueTrigger.getItem());
if (rules == null) {
rules = new HashSet<Rule>();
updateEventTriggeredRules.put(ueTrigger.getItem(), rules);
}
rules.add(rule);
} else if (t instanceof ChangedEventTrigger) {
ChangedEventTrigger ceTrigger = (ChangedEventTrigger) t;
Set<Rule> rules = changedEventTriggeredRules.get(ceTrigger.getItem());
if (rules == null) {
rules = new HashSet<Rule>();
changedEventTriggeredRules.put(ceTrigger.getItem(), rules);
}
rules.add(rule);
} else if (t instanceof TimerTrigger) {
try {
createTimer(rule, (TimerTrigger) t);
timerEventTriggeredRules.add(rule);
} catch (SchedulerException e) {
logger.error("Cannot create timer for rule '{}': {}", rule.getName(), e.getMessage());
}
} else if (t instanceof EventEmittedTrigger) {
EventEmittedTrigger eeTrigger = (EventEmittedTrigger) t;
Set<Rule> rules = triggerEventTriggeredRules.get(eeTrigger.getChannel());
if (rules == null) {
rules = new HashSet<Rule>();
triggerEventTriggeredRules.put(eeTrigger.getChannel(), rules);
}
rules.add(rule);
} else if (t instanceof ThingStateUpdateEventTrigger) {
ThingStateUpdateEventTrigger tsuTrigger = (ThingStateUpdateEventTrigger) t;
Set<Rule> rules = thingUpdateEventTriggeredRules.get(tsuTrigger);
if (rules == null) {
rules = new HashSet<Rule>();
thingUpdateEventTriggeredRules.put(tsuTrigger.getThing(), rules);
}
rules.add(rule);
} else if (t instanceof ThingStateChangedEventTrigger) {
ThingStateChangedEventTrigger tscTrigger = (ThingStateChangedEventTrigger) t;
Set<Rule> rules = thingChangedEventTriggeredRules.get(tscTrigger.getThing());
if (rules == null) {
rules = new HashSet<Rule>();
thingChangedEventTriggeredRules.put(tscTrigger.getThing(), rules);
}
rules.add(rule);
}
}
}
/**
* Removes a given rule from the mapping tables of a certain trigger type
*
* @param type the trigger type for which the rule should be removed
* @param rule the rule to add
*/
public void removeRule(TriggerTypes type, Rule rule) {
switch (type) {
case STARTUP:
systemStartupTriggeredRules.remove(rule);
break;
case SHUTDOWN:
systemShutdownTriggeredRules.remove(rule);
break;
case UPDATE:
for (Set<Rule> rules : updateEventTriggeredRules.values()) {
rules.remove(rule);
}
break;
case CHANGE:
for (Set<Rule> rules : changedEventTriggeredRules.values()) {
rules.remove(rule);
}
break;
case COMMAND:
for (Set<Rule> rules : commandEventTriggeredRules.values()) {
rules.remove(rule);
}
break;
case TRIGGER:
triggerEventTriggeredRules.remove(rule);
break;
case TIMER:
timerEventTriggeredRules.remove(rule);
removeTimerRule(rule);
break;
case THINGUPDATE:
for (Set<Rule> rules : thingUpdateEventTriggeredRules.values()) {
rules.remove(rule);
}
break;
case THINGCHANGE:
for (Set<Rule> rules : thingChangedEventTriggeredRules.values()) {
rules.remove(rule);
}
break;
}
}
/**
* Adds all rules of a model to the mapping tables
*
* @param model the rule model
*/
public void addRuleModel(RuleModel model) {
for (Rule rule : model.getRules()) {
addRule(rule);
}
}
/**
* Removes all rules of a given model (file) from the mapping tables.
*
* @param ruleModel the rule model
*/
public void removeRuleModel(RuleModel ruleModel) {
removeRules(UPDATE, updateEventTriggeredRules.values(), ruleModel);
removeRules(CHANGE, changedEventTriggeredRules.values(), ruleModel);
removeRules(COMMAND, commandEventTriggeredRules.values(), ruleModel);
removeRules(TRIGGER, triggerEventTriggeredRules.values(), ruleModel);
removeRules(STARTUP, Collections.singletonList(systemStartupTriggeredRules), ruleModel);
removeRules(SHUTDOWN, Collections.singletonList(systemShutdownTriggeredRules), ruleModel);
removeRules(TIMER, Collections.singletonList(timerEventTriggeredRules), ruleModel);
removeRules(THINGUPDATE, thingUpdateEventTriggeredRules.values(), ruleModel);
removeRules(THINGCHANGE, thingChangedEventTriggeredRules.values(), ruleModel);
}
private void removeRules(TriggerTypes type, Collection<? extends Collection<Rule>> ruleSets, RuleModel model) {
for (Collection<Rule> ruleSet : ruleSets) {
Set<Rule> clonedSet = new HashSet<Rule>(ruleSet);
// first remove all rules of the model, if not null (=non-existent)
if (model != null) {
for (Rule newRule : model.getRules()) {
for (Rule oldRule : clonedSet) {
if (newRule.getName().equals(oldRule.getName())) {
ruleSet.remove(oldRule);
if (type == TIMER) {
removeTimerRule(oldRule);
}
}
}
}
}
// now also remove all proxified rules from the set
clonedSet = new HashSet<Rule>(ruleSet);
for (Rule rule : clonedSet) {
if (rule.eResource() == null) {
ruleSet.remove(rule);
if (type == TIMER) {
removeTimerRule(rule);
}
}
}
}
}
private void removeTimerRule(Rule rule) {
try {
removeTimer(rule);
} catch (SchedulerException e) {
logger.error("Cannot remove timer for rule '{}'", rule.getName(), e);
}
}
/**
* Creates and schedules a new quartz-job and trigger with model and rule name as jobData.
*
* @param rule the rule to schedule
* @param trigger the defined trigger
*
* @throws SchedulerException if there is an internal Scheduler error.
*/
private void createTimer(Rule rule, TimerTrigger trigger) throws SchedulerException {
String cronExpression = trigger.getCron();
if (trigger.getTime() != null) {
if (trigger.getTime().equals("noon")) {
cronExpression = "0 0 12 * * ?";
} else if (trigger.getTime().equals("midnight")) {
cronExpression = "0 0 0 * * ?";
} else {
logger.warn("Unrecognized time expression '{}' in rule '{}'", trigger.getTime(), rule.getName());
return;
}
}
String jobIdentity = getJobIdentityString(rule, trigger);
try {
JobDetail job = newJob(ExecuteRuleJob.class)
.usingJobData(ExecuteRuleJob.JOB_DATA_RULEMODEL, rule.eResource().getURI().path())
.usingJobData(ExecuteRuleJob.JOB_DATA_RULENAME, rule.getName()).withIdentity(jobIdentity).build();
Trigger quartzTrigger = newTrigger().withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)).build();
scheduler.scheduleJob(job, Collections.singleton(quartzTrigger), true);
logger.debug("Scheduled rule '{}' with cron expression '{}'", rule.getName(), cronExpression);
} catch (RuntimeException e) {
throw new SchedulerException(e.getMessage());
}
}
/**
* Delete all {@link Job}s of the DEFAULT group whose name starts with <code>rule.getName()</code>.
*
* @throws SchedulerException if there is an internal Scheduler error.
*/
private void removeTimer(Rule rule) throws SchedulerException {
Set<JobKey> jobKeys = scheduler.getJobKeys(GroupMatcher.jobGroupEquals(Scheduler.DEFAULT_GROUP));
for (JobKey jobKey : jobKeys) {
String jobIdentityString = getJobIdentityString(rule, null);
if (jobKey.getName().startsWith(jobIdentityString)) {
boolean success = scheduler.deleteJob(jobKey);
if (!success) {
logger.warn("Failed to delete cron job '{}'", jobKey.getName());
} else {
logger.debug("Removed scheduled cron job '{}'", jobKey.getName());
}
}
}
}
private String getJobIdentityString(Rule rule, TimerTrigger trigger) {
String jobIdentity = EcoreUtil.getURI(rule).trimFragment().appendFragment(rule.getName()).toString();
if (trigger != null) {
if (trigger.getTime() != null) {
jobIdentity += "#" + trigger.getTime();
} else if (trigger.getCron() != null) {
jobIdentity += "#" + trigger.getCron();
}
}
return jobIdentity;
}
public void startTimerRuleExecution() {
try {
scheduler.start();
} catch (SchedulerException e) {
logger.error("Error while starting the scheduler service: {}", e.getMessage());
}
}
}