/** * Copyright (c) 1997, 2015 by ProSyst Software GmbH and others. * 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.automation.core.internal; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.stream.Stream; import org.eclipse.smarthome.automation.Rule; import org.eclipse.smarthome.automation.RuleProvider; import org.eclipse.smarthome.automation.RuleRegistry; import org.eclipse.smarthome.automation.RuleStatus; import org.eclipse.smarthome.automation.RuleStatusInfo; import org.eclipse.smarthome.automation.StatusInfoCallback; import org.eclipse.smarthome.automation.core.internal.composite.CompositeModuleHandlerFactory; import org.eclipse.smarthome.automation.core.internal.template.RuleTemplateRegistry; import org.eclipse.smarthome.automation.events.RuleEventFactory; import org.eclipse.smarthome.automation.handler.ModuleHandlerFactory; import org.eclipse.smarthome.automation.template.RuleTemplate; import org.eclipse.smarthome.automation.template.TemplateRegistry; import org.eclipse.smarthome.automation.type.ModuleTypeRegistry; import org.eclipse.smarthome.core.common.registry.AbstractRegistry; import org.eclipse.smarthome.core.common.registry.Provider; import org.eclipse.smarthome.core.common.registry.RegistryChangeListener; import org.eclipse.smarthome.core.storage.Storage; import org.eclipse.smarthome.core.storage.StorageService; import org.osgi.framework.BundleContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This is the main implementation of the {@link RuleRegistry}, which is registered as a service. * The {@link RuleRegistryImpl} provides basic functionality for managing {@link Rule}s. * It can be used to * <ul> * <li>Add Rules with the {@link #add(Rule)}, {@link #added(Provider, Rule)}, {@link #addProvider(RuleProvider)} * methods.</li> * <li>Get the existing rules with the {@link #get(String)}, {@link #getAll()}, {@link #getByTag(String)}, * {@link #getByTags(String[])} methods.</li> * <li>Update the existing rules with the {@link #update(Rule)}, {@link #updated(Provider, Rule, Rule)} methods.</li> * <li>Remove Rules with the {@link #remove(String)} method.</li> * </ul> * <p> * This class also persists the rules into the {@link StorageService} service and restores * them when the system is restarted. * <p> * The {@link RuleRegistry} manages the state (<b>enabled</b> or <b>disabled</b>) of the Rules: * <ul> * <li>A newly added Rule is always <b>enabled</b>.</li> * <li>To check a Rule's state, use the {@link #isEnabled(String)} method.</li> * <li>To change a Rule's state, use the {@link #setEnabled(String, boolean)} method.</li> * </ul> * <p> * The {@link RuleRegistry} manages the status of the Rules: * <ul> * <li>To check a Rule's status info, use the {@link #getStatusInfo(String)} method.</li> * <li>The status of a newly added Rule, or a Rule enabled with {@link #setEnabled(String, boolean)}, or an updated * Rule, is first set to {@link RuleStatus#UNINITIALIZED}.</li> * <li>After a Rule is added or enabled, or updated, a verification procedure is initiated. If the verification of the * modules IDs, connections between modules and configuration values of the modules is successful, and the module * handlers are correctly set, the status is set to {@link RuleStatus#IDLE}.</li> * <li>If some of the module handlers disappear, the Rule will become {@link RuleStatus#UNINITIALIZED} again.</li> * <li>If one of the Rule's Triggers is triggered, the Rule becomes {@link RuleStatus#RUNNING}. * When the execution is complete, it will become {@link RuleStatus#IDLE} again.</li> * <li>If a Rule is disabled with {@link #setEnabled(String, boolean)}, it's status is set to * {@link RuleStatus#DISABLED}.</li> * </ul> * * @author Yordan Mihaylov - Initial Contribution * @author Ana Dimova - Persistence implementation & updating rules from providers * @author Kai Kreuzer - refactored (managed) provider and registry implementation and other fixes * @author Benedikt Niehues - added events for rules * @author Victor Toni - return only copies of {@link Rule}s */ public class RuleRegistryImpl extends AbstractRegistry<Rule, String, RuleProvider> implements RuleRegistry, StatusInfoCallback, RegistryChangeListener<RuleTemplate> { private static final String DISABLED_RULE_STORAGE = "automation_rules_disabled"; private static final String SOURCE = RuleRegistryImpl.class.getSimpleName(); private static final Logger logger = LoggerFactory.getLogger(RuleRegistryImpl.class.getName()); private RuleEngine ruleEngine = new RuleEngine(); private Storage<Boolean> disabledRulesStorage; private ModuleTypeRegistry moduleTypeRegistry; private RuleTemplateRegistry templateRegistry; /** * {@link Map} of template UIDs to rules where these templates participated. */ private Map<String, Set<String>> mapTemplateToRules = new HashMap<String, Set<String>>(); public RuleRegistryImpl() { super(RuleProvider.class); } /** * Activates this component. Called from DS. * * @param componentContext this component context. */ protected void activate(BundleContext bundleContext, Map<String, Object> properties) throws Exception { ruleEngine.setCompositeModuleHandlerFactory( new CompositeModuleHandlerFactory(bundleContext, moduleTypeRegistry, ruleEngine)); ruleEngine.setStatusInfoCallback(this); modified(properties); super.activate(bundleContext); } protected void modified(Map<String, Object> config) { ruleEngine.scheduleRulesConfigurationUpdated(config); } /** * Deactivates this component. Called from DS. */ @Override protected void deactivate() { super.deactivate(); ruleEngine.dispose(); } /** * Bind the {@link ModuleTypeRegistry} service - called from DS. * * @param moduleTypeRegistry moduleTypeRegistry service. */ protected void setModuleTypeRegistry(ModuleTypeRegistry moduleTypeRegistry) { this.moduleTypeRegistry = moduleTypeRegistry; ruleEngine.setModuleTypeRegistry(moduleTypeRegistry); } /** * Unbind the {@link ModuleTypeRegistry} service - called from DS. * * @param moduleTypeRegistry moduleTypeRegistry service. */ protected void unsetModuleTypeRegistry(ModuleTypeRegistry moduleTypeRegistry) { this.moduleTypeRegistry = null; ruleEngine.setModuleTypeRegistry(null); } /** * Bind the {@link RuleTemplateRegistry} service - called from DS. * * @param templateRegistry templateRegistry service. */ protected void setTemplateRegistry(TemplateRegistry<RuleTemplate> templateRegistry) { if (templateRegistry instanceof RuleTemplateRegistry) { this.templateRegistry = (RuleTemplateRegistry) templateRegistry; templateRegistry.addRegistryChangeListener(this); } } /** * Unbind the {@link RuleTemplateRegistry} service - called from DS. * * @param templateRegistry templateRegistry service. */ protected void unsetTemplateRegistry(TemplateRegistry<RuleTemplate> templateRegistry) { if (templateRegistry instanceof RuleTemplateRegistry) { this.templateRegistry = null; templateRegistry.removeRegistryChangeListener(this); } } /** * Bind the {@link StorageService} - called from DS. * * @param storageService */ protected void setStorageService(StorageService storageService) { this.disabledRulesStorage = storageService.<Boolean> getStorage(DISABLED_RULE_STORAGE, this.getClass().getClassLoader()); // enable the rules that are not persisted as Disabled; for (Rule rule : getAll()) { String uid = rule.getUID(); if (disabledRulesStorage.get(uid) == null) { setEnabled(uid, Boolean.TRUE); } } } /** * Unbind the {@link StorageService} - called from DS. * * @param storageService */ protected void unsetStorageService(StorageService storageService) { this.disabledRulesStorage = null; // disable all rules; for (Rule rule : getAll()) { ruleEngine.setRuleEnabled(rule, Boolean.FALSE); } } protected void addModuleHandlerFactory(ModuleHandlerFactory moduleHandlerFactory) { ruleEngine.addModuleHandlerFactory(moduleHandlerFactory); } protected void removeModuleHandlerFactory(ModuleHandlerFactory moduleHandlerFactory) { ruleEngine.removeModuleHandlerFactory(moduleHandlerFactory); } /** * This method is used to register a {@link Rule} into the {@link RuleEngine}. First the {@link Rule} become * {@link RuleStatus#UNINITIALIZED}. * Then verification procedure will be done and the Rule become {@link RuleStatus#IDLE}. * If the verification fails, the Rule will stay {@link RuleStatus#UNINITIALIZED}. * * @param rule a {@link Rule} instance which have to be added into the {@link RuleEngine}. * @return a copy of the added {@link Rule} * @throws RuntimeException * when passed module has a required configuration property and it is not specified in rule definition * nor * in the module's module type definition. * @throws IllegalArgumentException * when a module id contains dot or when the rule with the same UID already exists. */ @Override public Rule add(Rule rule) { if (rule == null) { throw new IllegalArgumentException("The added rule must not be null!"); } String rUID = rule.getUID(); if (rUID == null) { rUID = ruleEngine.getUniqueId(); super.add(initRuleId(rUID, rule)); } else { super.add(rule); } return get(rUID); } /** * Sets a unique ID on the rule that should be added in the registry. If the rule already has an ID the method will * not be invoked. * * @param rUID the unique Rule ID that should be set to the rule * @param rule candidate for unique ID * @return a rule with UID */ protected Rule initRuleId(String rUID, Rule rule) { Rule ruleWithUID = new Rule(rUID, rule.getTriggers(), rule.getConditions(), rule.getActions(), rule.getConfigurationDescriptions(), rule.getConfiguration(), rule.getTemplateUID(), rule.getVisibility()); ruleWithUID.setName(rule.getName()); ruleWithUID.setTags(rule.getTags()); ruleWithUID.setDescription(rule.getDescription()); return ruleWithUID; } /** * This method is used to add {@link Rule} into the RuleEngine. * When the rule is resolved it becomes into {@link RuleStatus#IDLE} state. * If the verification fails, the rule goes into {@link RuleStatus#UNINITIALIZED} state. * * @param provider a provider of the {@link Rule}. * @param element a {@link Rule} instance which have to be added into the RuleEngine. * @throws RuntimeException * when passed module has a required configuration property and it is not specified in rule definition * nor * in the module's module type definition. * @throws IllegalArgumentException * when a module id contains dot or when the rule with the same UID already exists. */ @Override protected void notifyListenersAboutAddedElement(Rule rule) { super.notifyListenersAboutAddedElement(rule); postRuleAddedEvent(rule); String uid = rule.getUID(); ruleEngine.addRule(rule, (disabledRulesStorage != null && disabledRulesStorage.get(uid) == null)); String templateUID = rule.getTemplateUID(); if (templateUID != null) { synchronized (this) { Set<String> ruleUIDs = mapTemplateToRules.get(templateUID); if (ruleUIDs == null) { ruleUIDs = new HashSet<String>(11); mapTemplateToRules.put(templateUID, ruleUIDs); } ruleUIDs.add(uid); } } } protected void postRuleAddedEvent(Rule rule) { postEvent(RuleEventFactory.createRuleAddedEvent(rule, SOURCE)); } protected void postRuleRemovedEvent(Rule rule) { postEvent(RuleEventFactory.createRuleRemovedEvent(rule, SOURCE)); } protected void postRuleUpdatedEvent(Rule rule, Rule oldRule) { postEvent(RuleEventFactory.createRuleUpdatedEvent(rule, oldRule, SOURCE)); } protected void postRuleStatusInfoEvent(RuleStatusInfo statusInfo, String ruleUID) { postEvent(RuleEventFactory.createRuleStatusInfoEvent(statusInfo, ruleUID, SOURCE)); } @Override protected void onRemoveElement(Rule rule) { String uid = rule.getUID(); ruleEngine.removeRule(uid); String templateUID = rule.getTemplateUID(); if (templateUID != null) { synchronized (this) { Set<String> ruleUIDs = mapTemplateToRules.get(templateUID); if (ruleUIDs != null) { ruleUIDs.remove(uid); } } } } /** * This method is used to update an existing {@link Rule} in the {@link RuleEngine}. * When the {@link Rule} is resolved it becomes into {@link RuleStatus#IDLE} state. * If the verifications are failed, the {@link Rule} goes into {@link RuleStatus#UNINITIALIZED} state. * * @param oldElement a {@link Rule} instance that have to be replaced with value of the parameter * <code>element</code>. * @param rule a {@link Rule} instance which have to be used for update of the {@link Rule} into {@link RuleEngine}. * @return a copy of the old value of the updated rule * @throws RuntimeException * when passed module has a required configuration property and it is not specified in rule definition * nor * in the module's module type definition. * @throws IllegalArgumentException * when module id contains dot. */ @Override protected void notifyListenersAboutUpdatedElement(Rule oldElement, Rule element) { super.notifyListenersAboutUpdatedElement(oldElement, element); postRuleUpdatedEvent(element, oldElement); String uid = element.getUID(); ruleEngine.updateRule(element, (disabledRulesStorage != null && disabledRulesStorage.get(uid) == null)); String templateUID = element.getTemplateUID(); if (templateUID != null) { synchronized (this) { Set<String> ruleUIDs = mapTemplateToRules.get(templateUID); if (ruleUIDs != null) { ruleUIDs.remove(uid); } } } } @Override protected void notifyListenersAboutRemovedElement(Rule element) { super.notifyListenersAboutRemovedElement(element); postRuleRemovedEvent(element); } @Override public Rule get(String key) { for (Collection<Rule> rules : elementMap.values()) { for (Rule rule : rules) { if (rule.getUID().equals(key)) { return RuleUtils.getRuleCopy(rule); } } } return null; } @Override public Stream<Rule> stream() { // create copies for consumers return super.stream().map( r -> RuleUtils.getRuleCopy(r) ); } @Override public Collection<Rule> getByTag(String tag) { Collection<Rule> result = new LinkedList<Rule>(); if (tag != null) { for (Collection<Rule> rules : elementMap.values()) { for (Rule rule : rules) { Set<String> tags = rule.getTags(); if (tags != null && tags.contains(tag)) { result.add(RuleUtils.getRuleCopy(rule)); } } } } else { for (Collection<Rule> rules : elementMap.values()) { for (Rule rule : rules) { result.add(RuleUtils.getRuleCopy(rule)); } } } return result; } @Override public Collection<Rule> getByTags(String... tags) { Set<String> tagSet = tags != null ? new HashSet<String>(Arrays.asList(tags)) : null; Collection<Rule> result = new LinkedList<Rule>(); if (tagSet != null) { for (Collection<Rule> rules : elementMap.values()) { for (Rule rule : rules) { Set<String> rTags = rule.getTags(); if (rTags != null && rTags.containsAll(tagSet)) { result.add(RuleUtils.getRuleCopy(rule)); } } } } else { for (Collection<Rule> rules : elementMap.values()) { for (Rule rule : rules) { result.add(RuleUtils.getRuleCopy(rule)); } } } return result; } @Override public synchronized void setEnabled(String uid, boolean isEnabled) { if (disabledRulesStorage == null) { throw new IllegalStateException("Persisting rule state failed. Storage service is not available!"); } Rule rule = get(uid); if (rule == null) { throw new IllegalArgumentException(String.format("No rule with such id=%s was found!", uid)); } else { ruleEngine.setRuleEnabled(rule, isEnabled); } if (isEnabled) { disabledRulesStorage.remove(uid); } else { disabledRulesStorage.put(uid, isEnabled); } } @Override public RuleStatusInfo getStatusInfo(String ruleUID) { return ruleEngine.getRuleStatusInfo(ruleUID); } @Override public RuleStatus getStatus(String ruleUID) { return ruleEngine.getRuleStatus(ruleUID); } @Override public void statusInfoChanged(String ruleUID, RuleStatusInfo statusInfo) { postRuleStatusInfoEvent(statusInfo, ruleUID); } @Override public Boolean isEnabled(String ruleUID) { if (disabledRulesStorage != null && disabledRulesStorage.get(ruleUID) != null) { return Boolean.FALSE; } return ruleEngine.getRuleStatus(ruleUID) == null ? null : !ruleEngine.getRuleStatus(ruleUID).equals(RuleStatus.DISABLED); } /** * The method checks if the rule has to be resolved by template or not. If the rule does not contain tempateUID it * returns same rule, otherwise it tries to resolve the rule created from template. If the template is available * the method creates a new rule based on triggers, conditions and actions from template. If the template is not * available returns the same rule. * * @param rule a rule defined by template. * @return the resolved rule(containing modules defined by the template) or not resolved rule, if the template is * missing. */ private Rule resolveRuleByTemplate(Rule rule) { String templateUID = rule.getTemplateUID(); if (templateUID != null) { RuleTemplate template = templateRegistry.get(templateUID); if (template == null) { logger.debug("Rule template {} does not exist.", templateUID); return rule; } else { Rule resolvedRule = new Rule(rule.getUID(), RuleUtils.getTriggersCopy(template.getTriggers()), RuleUtils.getConditionsCopy(template.getConditions()), RuleUtils.getActionsCopy(template.getActions()), template.getConfigurationDescriptions(), rule.getConfiguration(), null, rule.getVisibility()); resolvedRule.setName(rule.getName()); resolvedRule.setTags(rule.getTags()); resolvedRule.setDescription(rule.getDescription()); // TODO this provide config resolution twice - It must be done only in RuleEngine. Remove it. ruleEngine.resolveConfiguration(resolvedRule); return resolvedRule; } } return rule; } @Override protected void addProvider(Provider<Rule> provider) { super.addProvider(provider); Collection<Rule> rules = new LinkedList<Rule>(elementMap.get(provider)); for (Rule rule : rules) { updateRuleByTemplate(provider, rule); } } @Override public void added(Provider<Rule> provider, Rule element) { Rule ruleWithUID = element; if (element.getUID() == null) { String rUID = ruleEngine.getUniqueId(); ruleWithUID = initRuleId(rUID, element); } super.added(provider, ruleWithUID); updateRuleByTemplate(provider, ruleWithUID); } private void updateRuleByTemplate(Provider<Rule> provider, Rule rule) { Rule resolvedRule = resolveRuleByTemplate(rule); if (rule != resolvedRule) { if (update(resolvedRule) == null) { super.updated(provider, rule, resolvedRule); } } } @Override public void updated(Provider<Rule> provider, Rule oldElement, Rule element) { Rule ruleWithUID = element; String rUID = oldElement.getUID(); if (element.getUID() == null) { ruleWithUID = initRuleId(rUID, element); } Rule resolvedRule = resolveRuleByTemplate(ruleWithUID); super.updated(provider, oldElement, resolvedRule); } @Override public void added(RuleTemplate element) { String templateUID = element.getUID(); Set<String> rules = new HashSet<String>(); synchronized (this) { Set<String> rulesForResolving = mapTemplateToRules.remove(templateUID); if (rulesForResolving != null) { rules.addAll(rulesForResolving); } } if (rules != null) { for (String rUID : rules) { Rule rule = get(rUID); updateRuleByTemplate(getProvider(rule), rule); } } } private Provider<Rule> getProvider(Rule rule) { for (Entry<Provider<Rule>, Collection<Rule>> entry : elementMap.entrySet()) { if (entry.getValue().contains(rule)) { return entry.getKey(); } } return null; } @Override public void removed(RuleTemplate element) { // Do nothing - resolved rules are independent from templates } @Override public void updated(RuleTemplate oldElement, RuleTemplate element) { // Do nothing - resolved rules are independent from templates } @Override public void runNow(String ruleUID) { ruleEngine.runNow(ruleUID); } @Override public void runNow(String ruleUID, boolean considerConditions, Map<String, Object> context) { ruleEngine.runNow(ruleUID, considerConditions, context); } }