/**
* 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.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.eclipse.smarthome.automation.Action;
import org.eclipse.smarthome.automation.Condition;
import org.eclipse.smarthome.automation.Module;
import org.eclipse.smarthome.automation.Rule;
import org.eclipse.smarthome.automation.RuleRegistry;
import org.eclipse.smarthome.automation.RuleStatus;
import org.eclipse.smarthome.automation.RuleStatusDetail;
import org.eclipse.smarthome.automation.RuleStatusInfo;
import org.eclipse.smarthome.automation.StatusInfoCallback;
import org.eclipse.smarthome.automation.Trigger;
import org.eclipse.smarthome.automation.core.internal.RuleEngineCallbackImpl.TriggerData;
import org.eclipse.smarthome.automation.core.internal.composite.CompositeModuleHandlerFactory;
import org.eclipse.smarthome.automation.core.util.ConnectionValidator;
import org.eclipse.smarthome.automation.handler.ActionHandler;
import org.eclipse.smarthome.automation.handler.ConditionHandler;
import org.eclipse.smarthome.automation.handler.ModuleHandler;
import org.eclipse.smarthome.automation.handler.ModuleHandlerFactory;
import org.eclipse.smarthome.automation.handler.RuleEngineCallback;
import org.eclipse.smarthome.automation.handler.TriggerHandler;
import org.eclipse.smarthome.automation.type.ActionType;
import org.eclipse.smarthome.automation.type.CompositeActionType;
import org.eclipse.smarthome.automation.type.CompositeConditionType;
import org.eclipse.smarthome.automation.type.CompositeTriggerType;
import org.eclipse.smarthome.automation.type.ConditionType;
import org.eclipse.smarthome.automation.type.Input;
import org.eclipse.smarthome.automation.type.ModuleType;
import org.eclipse.smarthome.automation.type.ModuleTypeRegistry;
import org.eclipse.smarthome.automation.type.Output;
import org.eclipse.smarthome.automation.type.TriggerType;
import org.eclipse.smarthome.config.core.ConfigDescriptionParameter;
import org.eclipse.smarthome.config.core.ConfigDescriptionParameter.Type;
import org.eclipse.smarthome.config.core.ConfigUtil;
import org.eclipse.smarthome.config.core.Configuration;
import org.eclipse.smarthome.core.common.registry.RegistryChangeListener;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* This class is used to initialized and execute {@link Rule}s added in rule engine. Each Rule has associated
* {@link RuleStatusInfo} object which shows status and status details of of the Rule. The states are self excluded and
* they are:
* <LI>disabled - the rule is temporary not available. This status is set by the user.
* <LI>not initialized -
* the rule is enabled, but it still is not working because some of the module handlers are not available or its module
* types or template is not resolved. The initialization problem is described by the status details
* <LI>idle - the rule
* is enabled and initialized and it is waiting for triggering events.
* <LI>running - the rule is enabled and initialized
* and it is executing at the moment. When the execution is finished, it goes to the idle state.
*
* @author Yordan Mihaylov - Initial Contribution
* @author Kai Kreuzer - refactored (managed) provider, registry implementation and customized modules
* @author Benedikt Niehues - change behavior for unregistering ModuleHandler
*
*/
@SuppressWarnings("rawtypes")
public class RuleEngine implements RegistryChangeListener<ModuleType> {
/**
* Constant defining separator between module uid and output name.
*/
public static final char OUTPUT_SEPARATOR = '.';
/**
* Prefix of {@link Rule}'s UID created by the rule engine.
*/
public static final String ID_PREFIX = "rule_"; //$NON-NLS-1$
/**
* Default value of delay between rule's re-initialization tries.
*/
public static final long DEFAULT_REINITIALIZATION_DELAY = 500;
/**
* Delay between rule's re-initialization tries.
*/
public static final String CONFIG_PROPERTY_REINITIALIZATION_DELAY = "rule.reinitialization.delay";
/**
* Delay between rule's re-initialization tries.
*/
private long scheduleReinitializationDelay;
/**
* {@link Map} of rule's id to corresponding {@link RuleEngineCallback}s. For each {@link Rule} there is one and
* only one rule callback.
*/
private Map<String, RuleEngineCallbackImpl> reCallbacks = new HashMap<String, RuleEngineCallbackImpl>();
/**
* {@link Map} of module type UIDs to rules where these module types participated.
*/
private Map<String, Set<String>> mapModuleTypeToRules = new HashMap<String, Set<String>>();
/**
* {@link Map} of created rules. It contains all rules added to rule engine independent if they are initialized or
* not. The relation is rule's id to {@link Rule} object.
*/
private Map<String, RuntimeRule> rules;
/**
* {@link Map} system module type to corresponding module handler factories.
*/
private Map<String, ModuleHandlerFactory> moduleHandlerFactories;
private Set<ModuleHandlerFactory> allModuleHandlerFactories = new CopyOnWriteArraySet<>();
/**
* Locker which does not permit rule initialization when the rule engine is stopping.
*/
private boolean isDisposed = false;
/**
* {@link Map} of {@link Rule}'s id to current {@link RuleStatus} object.
*/
private Map<String, RuleStatusInfo> statusMap = new HashMap<String, RuleStatusInfo>();
protected Logger logger = LoggerFactory.getLogger(RuleEngine.class.getName());
private StatusInfoCallback statusInfoCallback;
private Map<String, Map<String, Object>> contextMap;
private ModuleTypeRegistry mtRegistry;
private CompositeModuleHandlerFactory compositeFactory;
private int ruleMaxID = 0;
private Map<String, Future> scheduleTasks = new HashMap<String, Future>(31);
private ScheduledExecutorService executor;
private Gson gson;
/**
* Constructor of {@link RuleEngine}. It initializes the logger and starts tracker for {@link ModuleHandlerFactory}
* services.
*
* @param bc {@link BundleContext} used for tracker registration and rule engine logger creation.
* @throws InvalidSyntaxException
*/
public RuleEngine() {
this.rules = new HashMap<String, RuntimeRule>(20);
this.contextMap = new HashMap<String, Map<String, Object>>();
this.moduleHandlerFactories = new HashMap<String, ModuleHandlerFactory>(20);
}
protected void setModuleTypeRegistry(ModuleTypeRegistry moduleTypeRegistry) {
if (moduleTypeRegistry == null) {
mtRegistry.removeRegistryChangeListener(this);
mtRegistry = null;
} else {
mtRegistry = moduleTypeRegistry;
mtRegistry.addRegistryChangeListener(this);
}
ConnectionValidator.setRegistry(mtRegistry);
}
protected void setCompositeModuleHandlerFactory(CompositeModuleHandlerFactory compositeFactory) {
this.compositeFactory = compositeFactory;
}
@Override
public void added(ModuleType moduleType) {
String moduleTypeName = moduleType.getUID();
for (ModuleHandlerFactory moduleHandlerFactory : allModuleHandlerFactories) {
Collection<String> moduleTypes = moduleHandlerFactory.getTypes();
if (moduleTypes.contains(moduleTypeName)) {
synchronized (this) {
this.moduleHandlerFactories.put(moduleTypeName, moduleHandlerFactory);
}
break;
}
}
Set<String> rules = null;
synchronized (this) {
Set<String> rulesPerModule = mapModuleTypeToRules.get(moduleTypeName);
if (rulesPerModule != null) {
rules = new HashSet<String>();
rules.addAll(rulesPerModule);
}
}
if (rules != null) {
for (String rUID : rules) {
RuleStatus ruleStatus = getRuleStatus(rUID);
if (ruleStatus == RuleStatus.UNINITIALIZED) {
scheduleRuleInitialization(rUID);
}
}
}
}
@Override
public void removed(ModuleType moduleType) {
// removing module types does not effect the rule
}
@Override
public void updated(ModuleType oldElement, ModuleType moduleType) {
if (moduleType.equals(oldElement)) {
return;
}
String moduleTypeName = moduleType.getUID();
Set<String> rules = null;
synchronized (this) {
Set<String> rulesPerModule = mapModuleTypeToRules.get(moduleTypeName);
if (rulesPerModule != null) {
rules = new HashSet<String>();
rules.addAll(rulesPerModule);
}
}
if (rules != null) {
for (String rUID : rules) {
if (getRuleStatus(rUID).equals(RuleStatus.IDLE) || getRuleStatus(rUID).equals(RuleStatus.RUNNING)) {
setRuleStatusInfo(rUID, new RuleStatusInfo(RuleStatus.UNINITIALIZED), true);
unregister(getRuntimeRule(rUID));
}
if (!getRuleStatus(rUID).equals(RuleStatus.DISABLED)) {
scheduleRuleInitialization(rUID);
}
}
}
}
protected void addModuleHandlerFactory(ModuleHandlerFactory moduleHandlerFactory) {
logger.debug("ModuleHandlerFactory added.");
allModuleHandlerFactories.add(moduleHandlerFactory);
Collection<String> moduleTypes = moduleHandlerFactory.getTypes();
addNewModuleTypes(moduleHandlerFactory, moduleTypes);
}
protected void removeModuleHandlerFactory(ModuleHandlerFactory moduleHandlerFactory) {
if (moduleHandlerFactory instanceof CompositeModuleHandlerFactory) {
compositeFactory.deactivate();
compositeFactory = null;
}
allModuleHandlerFactories.remove(moduleHandlerFactory);
Collection<String> moduleTypes = moduleHandlerFactory.getTypes();
removeMissingModuleTypes(moduleTypes);
updateModuleHandlerFactoryMap(moduleTypes);
}
private synchronized void updateModuleHandlerFactoryMap(Collection<String> removedTypes) {
for (Iterator<String> it = removedTypes.iterator(); it.hasNext();) {
String moduleTypeName = it.next();
moduleHandlerFactories.remove(moduleTypeName);
}
}
/**
* This method add a new rule into rule engine. Scope identity of the Rule is the identity of the caller.
*
* @param rule a rule which has to be added.
* @param isEnabled specifies the rule to be added as disabled or not.
*/
protected void addRule(Rule rule, boolean isEnabled) {
String rUID = rule.getUID();
RuntimeRule runtimeRule = new RuntimeRule(rule);
synchronized (this) {
rules.put(rUID, runtimeRule);
if (isEnabled) {
setRuleStatusInfo(rUID, new RuleStatusInfo(RuleStatus.UNINITIALIZED), false);
setRule(runtimeRule);
} else {
setRuleStatusInfo(rUID, new RuleStatusInfo(RuleStatus.DISABLED), true);
}
}
}
/**
* Validates IDs of modules. The module id must not contain dot.
*
* @param modules list of trigger, condition and action modules
* @throws IllegalArgumentException when a module id contains dot.
*/
private void validateModuleIDs(List<Module> modules) {
for (Module m : modules) {
String mId = m.getId();
if (mId == null || !mId.matches("[A-Za-z0-9_-]*")) {
throw new IllegalArgumentException("Invalid module uid: " + (mId != null ? mId : "null")
+ ". It must not be null or not fit to the pattern: [A-Za-z0-9_-]*");
}
}
}
/**
* This method is used to update existing rule. It creates an internal {@link RuntimeRule} object which is deep copy
* of passed {@link Rule} object. If the rule exist in the rule engine it will be replaced by the new one.
*
* @param rule a rule which has to be updated.
* @param enabled specifies the rule to be updated as disabled or not.
*/
protected void updateRule(Rule rule, boolean isEnabled) {
String rUID = rule.getUID();
if (getRuntimeRule(rUID) == null) {
logger.debug("There is no rule with UID '{}' which could be updated", rUID);
return;
}
RuntimeRule runtimeRule = new RuntimeRule(rule);
synchronized (this) {
RuntimeRule oldRule = rules.get(rUID);
unregister(oldRule);
rules.put(rUID, runtimeRule);
if (isEnabled) {
setRuleStatusInfo(rUID, new RuleStatusInfo(RuleStatus.UNINITIALIZED), false);
setRule(runtimeRule);
} else {
setRuleStatusInfo(rUID, new RuleStatusInfo(RuleStatus.DISABLED), true);
}
}
logger.debug("Rule with UID '{}' is updated.", rUID);
}
/**
* This method tries to initialize the rule. It uses available {@link ModuleHandlerFactory}s to create
* {@link ModuleHandler}s for all {@link Module}s of the {@link Rule} and to link them. When all the modules have
* associated module handlers then the {@link Rule} is initialized and it is ready to working. It goes into idle
* state. Otherwise the Rule stays into not initialized and continue to wait missing handlers, module types or
* templates.
*
* @param rUID a UID of rule which tries to be initialized.
*/
private void setRule(RuntimeRule runtimeRule) {
if (isDisposed) {
return;
}
String rUID = runtimeRule.getUID();
setRuleStatusInfo(rUID, new RuleStatusInfo(RuleStatus.INITIALIZING), true);
if (runtimeRule.getTemplateUID() != null) {
setRuleStatusInfo(rUID,
new RuleStatusInfo(RuleStatus.UNINITIALIZED, RuleStatusDetail.TEMPLATE_MISSING_ERROR), true);
return; // Template is not available (when a template is resolved it removes tempalteUID configuration
// property). The rule must stay NOT_INITIALISED.
}
List<Module> modules = runtimeRule.getModules(null);
if (modules != null) {
for (Module m : modules) {
updateMapModuleTypeToRule(rUID, m.getTypeUID());
}
}
String errMsgs;
try {
validateModuleIDs(modules);
resolveConfiguration(runtimeRule);
autoMapConnections(runtimeRule);
ConnectionValidator.validateConnections(runtimeRule);
} catch (RuntimeException e) {
errMsgs = "\n Validation of rule " + rUID + " has failed! " + e.getLocalizedMessage();
// change state to NOTINITIALIZED
setRuleStatusInfo(rUID,
new RuleStatusInfo(RuleStatus.UNINITIALIZED, RuleStatusDetail.CONFIGURATION_ERROR, errMsgs.trim()),
true);
return;
}
errMsgs = setModuleHandlers(rUID, modules);
if (errMsgs == null) {
register(runtimeRule);
// change state to IDLE
setRuleStatusInfo(rUID, new RuleStatusInfo(RuleStatus.IDLE), true);
Future f = scheduleTasks.remove(rUID);
if (f != null) {
if (!f.isDone()) {
f.cancel(true);
}
}
if (scheduleTasks.isEmpty()) {
if (executor != null) {
executor.shutdown();
executor = null;
}
}
} else {
// change state to NOTINITIALIZED
setRuleStatusInfo(rUID,
new RuleStatusInfo(RuleStatus.UNINITIALIZED, RuleStatusDetail.HANDLER_INITIALIZING_ERROR, errMsgs),
true);
unregister(runtimeRule);
}
}
/**
* This method is used to update {@link RuleStatusInfo} of the rule. It also notifies the registry about the change.
*
* @param rUID UID of the rule which has changed status info.
* @param status new rule status info
*/
private void setRuleStatusInfo(String rUID, RuleStatusInfo status, boolean isSendEvent) {
synchronized (this) {
statusMap.put(rUID, status);
}
if (isSendEvent) {
notifyStatusInfoCallback(rUID, status);
}
}
private void notifyStatusInfoCallback(String rUID, RuleStatusInfo statusInfo) {
StatusInfoCallback statusInfoCallback = this.statusInfoCallback;
if (statusInfoCallback != null) {
try {
statusInfoCallback.statusInfoChanged(rUID, statusInfo);
} catch (Exception exc) {
logger.error("Exception while notifying StatusInfoCallback '{}' for rule '{}'", statusInfoCallback,
rUID, exc);
}
}
}
/**
* This method links modules to corresponding module handlers.
*
* @param rUID id of rule containing these modules
* @param modules list of modules
* @return null when all modules are connected or list of RuleErrors for missing handlers.
*/
private <T extends Module> String setModuleHandlers(String rUID, List<T> modules) {
StringBuffer sb = null;
if (modules != null) {
for (T m : modules) {
try {
ModuleHandler moduleHandler = getModuleHandler(m, rUID);
if (moduleHandler != null) {
if (m instanceof RuntimeAction) {
((RuntimeAction) m).setModuleHandler((ActionHandler) moduleHandler);
} else if (m instanceof RuntimeCondition) {
((RuntimeCondition) m).setModuleHandler((ConditionHandler) moduleHandler);
} else if (m instanceof RuntimeTrigger) {
((RuntimeTrigger) m).setModuleHandler((TriggerHandler) moduleHandler);
}
} else {
if (sb == null) {
sb = new StringBuffer();
}
String message = "Missing handler '" + m.getTypeUID() + "' for module '" + m.getId() + "'";
sb.append(message).append("\n");
logger.trace(message);
}
} catch (Throwable t) {
if (sb == null) {
sb = new StringBuffer();
}
String message = "Getting handler '" + m.getTypeUID() + "' for module '" + m.getId() + "' failed: "
+ t.getMessage();
sb.append(message).append("\n");
logger.trace(message);
}
}
}
return sb != null ? sb.toString() : null;
}
/**
* Gets {@link RuleEngineCallback} for passed {@link Rule}. If it does not exists, a callback object is created
*
* @param rule rule object for which the callback is looking for.
* @return a {@link RuleEngineCallback} corresponding to the passed {@link Rule} object.
*/
private synchronized RuleEngineCallbackImpl getRuleEngineCallback(RuntimeRule rule) {
RuleEngineCallbackImpl result = reCallbacks.get(rule.getUID());
if (result == null) {
result = new RuleEngineCallbackImpl(this, rule);
reCallbacks.put(rule.getUID(), result);
}
return result;
}
/**
* Unlink module handlers from their modules. The method is called when the rule containing these modules goes into
* not initialized state .
*
* @param modules list of module which are disconnected.
*/
private <T extends Module> void removeModuleHandlers(List<T> modules, String ruleUID) {
if (modules != null) {
for (T m : modules) {
ModuleHandler handler = null;
if (m instanceof RuntimeAction) {
handler = ((RuntimeAction) m).getModuleHandler();
} else if (m instanceof RuntimeCondition) {
handler = ((RuntimeCondition) m).getModuleHandler();
} else if (m instanceof RuntimeTrigger) {
handler = ((RuntimeTrigger) m).getModuleHandler();
}
if (handler != null) {
ModuleHandlerFactory factory = getModuleHandlerFactory(m.getTypeUID());
if (factory != null) {
factory.ungetHandler(m, ruleUID, handler);
}
if (m instanceof RuntimeAction) {
((RuntimeAction) m).setModuleHandler(null);
} else if (m instanceof RuntimeCondition) {
((RuntimeCondition) m).setModuleHandler(null);
} else if (m instanceof RuntimeTrigger) {
((RuntimeTrigger) m).setModuleHandler(null);
}
}
}
}
}
/**
* This method register the Rule to start working. This is the final step of initialization process where triggers
* received {@link RuleEngineCallback}s object and starts to notify the rule engine when they are triggered. After
* activating all triggers the rule goes into IDLE state
*
* @param rule an initialized rule which has to starts tracking the triggers.
*/
private void register(RuntimeRule rule) {
RuleEngineCallback reCallback = getRuleEngineCallback(rule);
for (Iterator<Trigger> it = rule.getTriggers().iterator(); it.hasNext();) {
RuntimeTrigger t = (RuntimeTrigger) it.next();
TriggerHandler triggerHandler = t.getModuleHandler();
triggerHandler.setRuleEngineCallback(reCallback);
}
}
/**
* This method unregister rule form rule engine and the rule stops working. This is happen when the {@link Rule} is
* removed or some of module handlers are disappeared. In the second case the rule stays available but its state is
* moved to not initialized.
*
* @param r the unregistered rule
*/
private void unregister(RuntimeRule r) {
if (r != null) {
synchronized (this) {
RuleEngineCallbackImpl reCallback = reCallbacks.remove(r.getUID());
if (reCallback != null) {
reCallback.dispose();
}
}
removeModuleHandlers(r.getTriggers(), r.getUID());
removeModuleHandlers(r.getActions(), r.getUID());
removeModuleHandlers(r.getConditions(), r.getUID());
}
}
/**
* Gets handler of passed module.
*
* @param m a {@link Module} which is looking for handler
* @return handler for this module or null when it is not available.
*/
private ModuleHandler getModuleHandler(Module m, String ruleUID) {
String moduleTypeId = m.getTypeUID();
ModuleHandlerFactory mhf = getModuleHandlerFactory(moduleTypeId);
if (mhf == null || mtRegistry.get(moduleTypeId) == null) {
return null;
}
return mhf.getHandler(m, ruleUID);
}
public ModuleHandlerFactory getModuleHandlerFactory(String moduleTypeId) {
ModuleHandlerFactory mhf = null;
synchronized (this) {
mhf = moduleHandlerFactories.get(moduleTypeId);
}
if (mhf == null) {
ModuleType mt = mtRegistry.get(moduleTypeId);
if (mt instanceof CompositeTriggerType || //
mt instanceof CompositeConditionType || //
mt instanceof CompositeActionType) {
mhf = compositeFactory;
}
}
return mhf;
}
public synchronized void updateMapModuleTypeToRule(String rUID, String moduleTypeId) {
Set<String> rules = mapModuleTypeToRules.get(moduleTypeId);
if (rules == null) {
rules = new HashSet<String>(11);
}
rules.add(rUID);
mapModuleTypeToRules.put(moduleTypeId, rules);
}
/**
* This method removes Rule from rule engine. It is called by the {@link RuleRegistry}
*
* @param rUID id of removed {@link Rule}
* @return true when a rule is deleted, false when there is no rule with such id.
*/
protected synchronized boolean removeRule(String rUID) {
RuntimeRule r = rules.remove(rUID);
if (r != null) {
removeRuleEntry(r);
return true;
}
return false;
}
/**
* Utility method cleaning status and handler type Maps of removing {@link Rule}.
*
* @param r removed {@link Rule}
* @return removed rule
*/
private RuntimeRule removeRuleEntry(RuntimeRule r) {
unregister(r);
synchronized (this) {
for (Iterator<Map.Entry<String, Set<String>>> it = mapModuleTypeToRules.entrySet().iterator(); it
.hasNext();) {
Map.Entry<String, Set<String>> e = it.next();
Set<String> rules = e.getValue();
if (rules != null && rules.contains(r.getUID())) {
rules.remove(r.getUID());
if (rules.size() < 1) {
it.remove();
}
}
}
statusMap.remove(r.getUID());
}
return r;
}
/**
* Gets {@link RuntimeRule} corresponding to the passed id. This method is used internally and it does not create a
* copy of the rule.
*
* @param rUID unieque id of the {@link Rule}
* @return internal {@link RuntimeRule} object
*/
protected synchronized RuntimeRule getRuntimeRule(String rUID) {
return rules.get(rUID);
}
/**
* Gets all rules available in the rule engine.
*
* @return collection of all added rules.
*/
protected synchronized Collection<RuntimeRule> getRuntimeRules() {
return Collections.unmodifiableCollection(rules.values());
}
/**
* This method can switch enabled/ disabled state of the {@link Rule}
*
* @param rUID unique id of the rule
* @param isEnabled true to enable the rule, false to disable it
*/
protected void setRuleEnabled(Rule rule, boolean isEnabled) {
String rUID = rule.getUID();
RuleStatus status = getRuleStatus(rUID);
String enabled = isEnabled ? "enabled" : "disabled";
RuntimeRule runtimeRule = getRuntimeRule(rUID);
if (runtimeRule == null) {
logger.debug("There is no rule with UID '{}' which could be {}", rUID, enabled);
return;
}
if (isEnabled) {
if (status == RuleStatus.DISABLED) {
setRule(runtimeRule);
} else {
logger.debug("The rule rId = " + rUID + " is already enabled.");
}
} else {
unregister(runtimeRule);
setRuleStatusInfo(rUID, new RuleStatusInfo(RuleStatus.DISABLED), true);
}
}
private void addNewModuleTypes(ModuleHandlerFactory mhf, Collection<String> moduleTypes) {
Set<String> notInitailizedRules = null;
for (Iterator<String> it = moduleTypes.iterator(); it.hasNext();) {
String moduleTypeName = it.next();
Set<String> rules = null;
synchronized (this) {
moduleHandlerFactories.put(moduleTypeName, mhf);
Set<String> rulesPerModule = mapModuleTypeToRules.get(moduleTypeName);
if (rulesPerModule != null) {
rules = new HashSet<String>();
rules.addAll(rulesPerModule);
}
}
if (rules != null) {
for (String rUID : rules) {
RuleStatus ruleStatus = getRuleStatus(rUID);
if (ruleStatus == RuleStatus.UNINITIALIZED) {
notInitailizedRules = notInitailizedRules != null ? notInitailizedRules
: new HashSet<String>(20);
notInitailizedRules.add(rUID);
}
}
}
}
if (notInitailizedRules != null) {
for (final String rUID : notInitailizedRules) {
scheduleRuleInitialization(rUID);
}
}
}
protected void scheduleRuleInitialization(final String rUID) {
Future f = scheduleTasks.get(rUID);
if (f == null) {
ScheduledExecutorService ex = getScheduledExecutor();
f = ex.schedule(new Runnable() {
@Override
public void run() {
setRule(getRuntimeRule(rUID));
}
}, scheduleReinitializationDelay, TimeUnit.MILLISECONDS);
scheduleTasks.put(rUID, f);
}
}
private void removeMissingModuleTypes(Collection<String> moduleTypes) {
Map<String, List<String>> mapMissingHandlers = null;
for (Iterator<String> it = moduleTypes.iterator(); it.hasNext();) {
String moduleTypeName = it.next();
Set<String> rules = null;
synchronized (this) {
rules = mapModuleTypeToRules.get(moduleTypeName);
}
if (rules != null) {
for (String rUID : rules) {
RuleStatus ruleStatus = getRuleStatus(rUID);
switch (ruleStatus) {
case RUNNING:
case IDLE:
mapMissingHandlers = mapMissingHandlers != null ? mapMissingHandlers
: new HashMap<String, List<String>>(20);
List<String> list = mapMissingHandlers.get(rUID);
if (list == null) {
list = new ArrayList<String>(5);
}
list.add(moduleTypeName);
mapMissingHandlers.put(rUID, list);
break;
default:
break;
}
}
}
} // for
if (mapMissingHandlers != null) {
for (Entry<String, List<String>> e : mapMissingHandlers.entrySet()) {
String rUID = e.getKey();
List<String> missingTypes = e.getValue();
StringBuffer sb = new StringBuffer();
sb.append("Missing handlers: ");
for (String typeUID : missingTypes) {
sb.append(typeUID).append(", ");
}
setRuleStatusInfo(rUID, new RuleStatusInfo(RuleStatus.UNINITIALIZED,
RuleStatusDetail.HANDLER_MISSING_ERROR, sb.substring(0, sb.length() - 2)), true);
unregister(getRuntimeRule(rUID));
}
}
}
/**
* This method runs a {@link Rule}. It is called by the {@link RuleEngineCallback}'s thread when a new
* {@link TriggerData} is available. This method switches
*
* @param rule the {@link Rule} which has to evaluate new {@link TriggerData}.
* @param td {@link TriggerData} object containing new values for {@link Trigger}'s {@link Output}s
*/
protected void runRule(RuntimeRule rule, RuleEngineCallbackImpl.TriggerData td) {
String rUID = rule.getUID();
if (reCallbacks.get(rUID) == null) {
// the rule was unregistered
return;
}
synchronized (this) {
final RuleStatus ruleStatus = getRuleStatus(rUID);
if (ruleStatus != RuleStatus.IDLE) {
logger.error("Failed to execute rule ‘{}' with status '{}'", rUID, ruleStatus.name());
return;
}
// change state to RUNNING
setRuleStatusInfo(rUID, new RuleStatusInfo(RuleStatus.RUNNING), true);
}
try {
clearContext(rule);
setTriggerOutputs(rUID, td);
boolean isSatisfied = calculateConditions(rule);
if (isSatisfied) {
executeActions(rule, true);
logger.debug("The rule '{}' is executed.", rUID);
} else {
logger.debug("The rule '{}' is NOT executed, since it has unsatisfied conditions.", rUID);
}
} catch (Throwable t) {
logger.error("Failed to execute rule '{}': {}", rUID, t.getMessage());
logger.debug("", t);
}
// change state to IDLE only if the rule has not been DISABLED.
synchronized (this) {
if (getRuleStatus(rUID) == RuleStatus.RUNNING) {
setRuleStatusInfo(rUID, new RuleStatusInfo(RuleStatus.IDLE), true);
}
}
}
protected void runNow(String ruleUID, boolean considerConditions, Map<String, Object> context) {
RuntimeRule rule = getRuntimeRule(ruleUID);
if (rule == null) {
logger.warn("Failed to execute rule '{}': Invalid Rule UID", ruleUID);
return;
}
synchronized (this) {
final RuleStatus ruleStatus = getRuleStatus(ruleUID);
if (ruleStatus != RuleStatus.IDLE) {
logger.error("Failed to execute rule ‘{}' with status '{}'", ruleUID, ruleStatus.name());
return;
}
// change state to RUNNING
setRuleStatusInfo(ruleUID, new RuleStatusInfo(RuleStatus.RUNNING), true);
}
try {
clearContext(rule);
if (context != null && !context.isEmpty()) {
getContext(ruleUID).putAll(context);
}
if (considerConditions) {
if (calculateConditions(rule)) {
executeActions(rule, false);
}
} else {
executeActions(rule, false);
}
logger.debug("The rule '{}' is executed.", ruleUID);
} catch (Throwable t) {
logger.error("Fail to execute rule '{}': {}", new Object[] { ruleUID, t.getMessage() }, t);
}
// change state to IDLE only if the rule has not been DISABLED.
synchronized (this) {
if (getRuleStatus(ruleUID) == RuleStatus.RUNNING) {
setRuleStatusInfo(ruleUID, new RuleStatusInfo(RuleStatus.IDLE), true);
}
}
}
protected void runNow(String ruleUID) {
runNow(ruleUID, false, null);
}
protected void clearContext(RuntimeRule rule) {
Map<String, Object> context = contextMap.get(rule.getUID());
if (context != null) {
context.clear();
}
}
/**
* The method updates {@link Output} of the {@link Trigger} with a new triggered data.
*
* @param td new Triggered data.
*/
private void setTriggerOutputs(String ruleUID, TriggerData td) {
Trigger t = td.getTrigger();
updateContext(ruleUID, t.getId(), td.getOutputs());
}
/**
* Updates current context of rule engine.
*
* @param moduleUID uid of updated module.
*
* @param outputs new output values.
*/
private void updateContext(String ruleUID, String moduleUID, Map<String, ?> outputs) {
Map<String, Object> context = getContext(ruleUID);
if (outputs != null) {
for (Map.Entry<String, ?> entry : outputs.entrySet()) {
String key = moduleUID + OUTPUT_SEPARATOR + entry.getKey();
context.put(key, entry.getValue());
}
}
}
/**
* @return copy of current context in rule engine
*/
private Map<String, Object> getContext(String ruleUID) {
return getContext(ruleUID, null);
}
private Map<String, Object> getContext(String ruleUID, Set<Connection> connections) {
Map<String, Object> context = contextMap.get(ruleUID);
if (context == null) {
context = new HashMap<String, Object>();
contextMap.put(ruleUID, context);
}
if (connections != null) {
StringBuffer sb = new StringBuffer();
for (Connection c : connections) {
String outputModuleId = c.getOuputModuleId();
if (outputModuleId != null) {
sb.append(outputModuleId).append(OUTPUT_SEPARATOR).append(c.getOutputName());
context.put(c.getInputName(), context.get(sb.toString()));
sb.setLength(0);
} else {
// get reference from context
String ref = c.getOutputName();
final Object value = ReferenceResolverUtil.resolveReference(ref, context);
if (value != null) {
context.put(c.getInputName(), value);
}
}
}
}
return context;
}
/**
* This method checks if all rule's condition are satisfied or not.
*
* @param rule the checked rule
* @return true when all conditions of the rule are satisfied, false otherwise.
*/
private boolean calculateConditions(Rule rule) {
List<Condition> conditions = ((RuntimeRule) rule).getConditions();
if (conditions.size() == 0) {
return true;
}
RuleStatus ruleStatus = null;
for (Iterator<Condition> it = conditions.iterator(); it.hasNext();) {
ruleStatus = getRuleStatus(rule.getUID());
if (ruleStatus != RuleStatus.RUNNING) {
return false;
}
RuntimeCondition c = (RuntimeCondition) it.next();
ConditionHandler tHandler = c.getModuleHandler();
Map<String, Object> context = getContext(rule.getUID(), c.getConnections());
if (!tHandler.isSatisfied(Collections.unmodifiableMap(context))) {
logger.debug("The condition '{}' of rule '{}' is unsatisfied.",
new Object[] { c.getId(), rule.getUID() });
return false;
}
}
return true;
}
/**
* This method evaluates actions of the {@link Rule} and set their {@link Output}s when they exists.
*
* @param rule executed rule.
*/
private void executeActions(Rule rule, boolean stopOnFirstFail) {
List<Action> actions = ((RuntimeRule) rule).getActions();
if (actions == null || actions.size() == 0) {
return;
}
RuleStatus ruleStatus = null;
RuntimeAction action = null;
for (Iterator<Action> it = actions.iterator(); it.hasNext();) {
ruleStatus = getRuleStatus(rule.getUID());
if (ruleStatus != RuleStatus.RUNNING) {
return;
}
action = (RuntimeAction) it.next();
ActionHandler aHandler = action.getModuleHandler();
String rUID = rule.getUID();
Map<String, Object> context = getContext(rUID, action.getConnections());
try {
Map<String, ?> outputs = aHandler.execute(Collections.unmodifiableMap(context));
if (outputs != null) {
context = getContext(rUID);
updateContext(rUID, action.getId(), outputs);
}
} catch (Throwable t) {
String errMessage = "Fail to execute action: " + action != null ? action.getId() : "<unknown>";
if (stopOnFirstFail) {
RuntimeException re = new RuntimeException(errMessage, t);
throw re;
} else {
logger.warn(errMessage, t);
}
}
}
}
/**
* The method clean used resource by rule engine when it is stopped.
*/
public synchronized void dispose() {
if (!isDisposed) {
isDisposed = true;
for (Iterator<RuntimeRule> it = rules.values().iterator(); it.hasNext();) {
RuntimeRule r = it.next();
removeRuleEntry(r);
it.remove();
}
if (compositeFactory != null) {
compositeFactory.dispose();
compositeFactory = null;
}
}
for (Future f : scheduleTasks.values()) {
f.cancel(true);
}
if (scheduleTasks.isEmpty()) {
if (executor != null) {
executor.shutdown();
executor = null;
}
}
scheduleTasks = null;
if (contextMap != null) {
contextMap.clear();
contextMap = null;
}
statusInfoCallback = null;
}
/**
* This method gets rule's status object.
*
* @param rUID rule uid
* @return status of the rule or null when such rule does not exists.
*/
protected RuleStatus getRuleStatus(String rUID) {
RuleStatusInfo info = getRuleStatusInfo(rUID);
RuleStatus status = null;
if (info != null) {
status = info.getStatus();
}
return status;
}
/**
* This method gets rule's status info object.
*
* @param rUID rule uid
* @return status of the rule or null when such rule does not exists.
*/
protected synchronized RuleStatusInfo getRuleStatusInfo(String rUID) {
return statusMap.get(rUID);
}
protected synchronized String getUniqueId() {
int result = 0;
if (rules != null) {
Set<String> col = rules.keySet();
if (col != null) {
for (Iterator<String> it = col.iterator(); it.hasNext();) {
String rUID = it.next();
if (rUID != null && rUID.startsWith(ID_PREFIX)) {
String sNum = rUID.substring(ID_PREFIX.length());
int i;
try {
i = Integer.parseInt(sNum);
result = i > result ? i : result; // find bigger key
} catch (NumberFormatException e) {
// skip this key
}
}
}
}
}
if (result > ruleMaxID) {
ruleMaxID = result + 1;
} else {
++ruleMaxID;
}
return ID_PREFIX + ruleMaxID;
}
protected void setStatusInfoCallback(StatusInfoCallback statusInfoCallback) {
this.statusInfoCallback = statusInfoCallback;
}
private ScheduledExecutorService getScheduledExecutor() {
if (executor == null || executor.isShutdown()) {
executor = Executors.newSingleThreadScheduledExecutor();
}
return executor;
}
protected void scheduleRulesConfigurationUpdated(Map<String, Object> config) {
if (config != null) {
Object value = config.get(CONFIG_PROPERTY_REINITIALIZATION_DELAY);
if (value != null) {
if (value instanceof Number) {
scheduleReinitializationDelay = ((Number) value).longValue();
} else {
logger.error("Invalid configuration value: " + value + "It MUST be Number.");
}
} else {
scheduleReinitializationDelay = DEFAULT_REINITIALIZATION_DELAY;
}
} else {
scheduleReinitializationDelay = DEFAULT_REINITIALIZATION_DELAY;
}
}
/**
* The auto mapping tries to link not connected module inputs to output of other modules. The auto mapping will link
* input to output only when following criteria are done: 1) input must not be connected. The auto mapping will not
* overwrite explicit connections done by the user. 2) input tags must be subset of the output tags. 3) condition
* inputs can be connected only to triggers' outputs 4) action outputs can be connected to both conditions and
* actions
* outputs 5) There is only one output, based on previous criteria, where the input can connect to. If more then one
* candidate outputs exists for connection, this is a conflict and the auto mapping leaves the input unconnected.
* Auto
* mapping is always applied when the rule is added or updated. It changes initial value of inputs of conditions and
* actions participating in the rule. If an "auto map" connection has to be removed, the tags of corresponding
* input/output have to be changed.
*
* @param r updated rule
*/
private void autoMapConnections(RuntimeRule r) {
Map<Set<String>, OutputRef> triggerOutputTags = new HashMap<Set<String>, OutputRef>(11);
for (Trigger t : r.getTriggers()) {
TriggerType tt = (TriggerType) mtRegistry.get(t.getTypeUID());
if (tt != null) {
initTagsMap(t.getId(), tt.getOutputs(), triggerOutputTags);
}
}
Map<Set<String>, OutputRef> actionOutputTags = new HashMap<Set<String>, OutputRef>(11);
for (Action a : r.getActions()) {
ActionType at = (ActionType) mtRegistry.get(a.getTypeUID());
if (at != null) {
initTagsMap(a.getId(), at.getOutputs(), actionOutputTags);
}
}
// auto mapping of conditions
if (!triggerOutputTags.isEmpty()) {
for (Condition c : r.getConditions()) {
boolean isConnectionChanged = false;
ConditionType ct = (ConditionType) mtRegistry.get(c.getTypeUID());
if (ct != null) {
Set<Connection> connections = ((RuntimeCondition) c).getConnections();
for (Input input : ct.getInputs()) {
if (isConnected(input, connections)) {
continue; // the input is already connected. Skip it.
}
if (addAutoMapConnections(input, triggerOutputTags, connections)) {
isConnectionChanged = true;
}
}
if (isConnectionChanged) {
// update condition inputs
connections = ((RuntimeCondition) c).getConnections();
Map<String, String> connectionMap = getConnectionMap(connections);
c.setInputs(connectionMap);
}
}
}
}
// auto mapping of actions
if (!triggerOutputTags.isEmpty() || !actionOutputTags.isEmpty()) {
for (Action a : r.getActions()) {
boolean isConnectionChanged = false;
ActionType at = (ActionType) mtRegistry.get(a.getTypeUID());
if (at != null) {
Set<Connection> connections = ((RuntimeAction) a).getConnections();
for (Input input : at.getInputs()) {
if (isConnected(input, connections)) {
continue; // the input is already connected. Skip it.
}
if (addAutoMapConnections(input, triggerOutputTags, connections)) {
isConnectionChanged = true;
}
if (addAutoMapConnections(input, actionOutputTags, connections)) {
isConnectionChanged = true;
}
}
if (isConnectionChanged) {
// update condition inputs
connections = ((RuntimeAction) a).getConnections();
Map<String, String> connectionMap = getConnectionMap(connections);
a.setInputs(connectionMap);
}
}
}
}
}
/**
* Try to connect a free input to available outputs.
*
* @param input a free input which has to be connected
* @param outputTagMap a map of set of tags to outptu references
* @param currentConnections current connections of this module
* @return true when only one output which meets auto mapping ctiteria is found. False otherwise.
*/
private boolean addAutoMapConnections(Input input, Map<Set<String>, OutputRef> outputTagMap,
Set<Connection> currentConnections) {
boolean result = false;
Set<String> inputTags = input.getTags();
OutputRef outputRef = null;
boolean conflict = false;
if (inputTags.size() > 0) {
for (Set<String> outTags : outputTagMap.keySet()) {
if (outTags.containsAll(inputTags)) { // input tags must be subset of the output ones
if (outputRef == null) {
outputRef = outputTagMap.get(outTags);
} else {
conflict = true; // already exist candidate for autoMap
break;
}
}
}
if (!conflict && outputRef != null) {
if (currentConnections == null) {
currentConnections = new HashSet<Connection>(11);
}
currentConnections
.add(new Connection(input.getName(), outputRef.getModuleId(), outputRef.getOutputName()));
result = true;
}
}
return result;
}
private void initTagsMap(String moduleId, List<Output> outputs, Map<Set<String>, OutputRef> tagMap) {
for (Output output : outputs) {
Set<String> tags = output.getTags();
if (tags.size() > 0) {
if (tagMap.get(tags) != null) {
// this set of output tags already exists. (conflict)
tagMap.remove(tags);
} else {
tagMap.put(tags, new OutputRef(moduleId, output.getName()));
}
}
}
}
private boolean isConnected(Input input, Set<Connection> connections) {
if (connections != null) {
for (Connection connection : connections) {
if (connection.getInputName().equals(input.getName())) {
return true;
}
}
}
return false;
}
private Map<String, String> getConnectionMap(Set<Connection> connections) {
Map<String, String> connectionMap = new HashMap<String, String>(11);
for (Connection connection : connections) {
connectionMap.put(connection.getInputName(),
connection.getOuputModuleId() + "." + connection.getOutputName());
}
return connectionMap;
}
class OutputRef {
private String moduleId;
private String outputName;
public OutputRef(String moduleId, String outputName) {
this.moduleId = moduleId;
this.outputName = outputName;
}
public String getModuleId() {
return moduleId;
}
public String getOutputName() {
return outputName;
}
}
protected void resolveConfiguration(Rule rule) {
List<ConfigDescriptionParameter> configDescriptions = rule.getConfigurationDescriptions();
Map<String, Object> configuration = rule.getConfiguration().getProperties();
if (configuration != null) {
handleModuleConfigReferences(rule.getTriggers(), configuration);
handleModuleConfigReferences(rule.getConditions(), configuration);
handleModuleConfigReferences(rule.getActions(), configuration);
}
normalizeRuleConfigurations(rule);
validateConfiguration(rule.getUID(), configDescriptions, new HashMap<String, Object>(configuration));
}
private void validateConfiguration(String uid, List<ConfigDescriptionParameter> configDescriptions,
Map<String, Object> configurations) {
if (configurations == null || configurations.isEmpty()) {
if (isOptionalConfig(configDescriptions)) {
return;
} else {
for (ConfigDescriptionParameter configParameter : configDescriptions) {
if (configParameter.isRequired()) {
logger.error("Missing required configuration property '{}' for rule with UID '{}'!",
configParameter.getName(), uid);
}
}
throw new IllegalArgumentException("Missing required configuration properties!");
}
} else {
for (ConfigDescriptionParameter configParameter : configDescriptions) {
String configParameterName = configParameter.getName();
processValue(configurations.remove(configParameterName), configParameter);
}
for (String name : configurations.keySet()) {
logger.error("Extra configuration property '{}' for rule with UID '{}'!", name, uid);
}
if (!configurations.isEmpty()) {
throw new IllegalArgumentException("Extra configuration properties!");
}
}
}
private boolean isOptionalConfig(List<ConfigDescriptionParameter> configDescriptions) {
if (configDescriptions != null && !configDescriptions.isEmpty()) {
boolean required = false;
Iterator<ConfigDescriptionParameter> i = configDescriptions.iterator();
while (i.hasNext()) {
ConfigDescriptionParameter param = i.next();
required = required || param.isRequired();
}
return !required;
}
return true;
}
private void processValue(Object configValue, ConfigDescriptionParameter configParameter) {
if (configValue != null) {
checkType(configValue, configParameter);
return;
}
if (configParameter.isRequired()) {
throw new IllegalArgumentException(
"Required configuration property missing: \"" + configParameter.getName() + "\"!");
}
}
private void checkType(Object configValue, ConfigDescriptionParameter configParameter) {
Type type = configParameter.getType();
if (configParameter.isMultiple()) {
if (configValue instanceof List) {
List lConfigValues = (List) configValue;
for (Object value : lConfigValues) {
if (!checkType(type, value)) {
throw new IllegalArgumentException("Unexpected value for configuration property \""
+ configParameter.getName() + "\". Expected type: " + type);
}
}
}
throw new IllegalArgumentException(
"Unexpected value for configuration property \"" + configParameter.getName()
+ "\". Expected is Array with type for elements : " + type.toString() + "!");
} else {
if (!checkType(type, configValue)) {
throw new IllegalArgumentException("Unexpected value for configuration property \""
+ configParameter.getName() + "\". Expected is " + type.toString() + "!");
}
}
}
private boolean checkType(Type type, Object configValue) {
switch (type) {
case TEXT:
return configValue instanceof String;
case BOOLEAN:
return configValue instanceof Boolean;
case INTEGER:
return configValue instanceof BigDecimal || configValue instanceof Integer
|| configValue instanceof Double && ((Double) configValue).intValue() == (Double) configValue;
case DECIMAL:
return configValue instanceof BigDecimal || configValue instanceof Double;
}
return false;
}
private void handleModuleConfigReferences(List<? extends Module> modules, Map<String, ?> ruleConfiguration) {
if (modules != null) {
for (Module module : modules) {
ReferenceResolverUtil.updateModuleConfiguration(module, ruleConfiguration);
}
}
}
private void normalizeRuleConfigurations(Rule rule) {
List<ConfigDescriptionParameter> configDescriptions = rule.getConfigurationDescriptions();
Map<String, ConfigDescriptionParameter> mapConfigDescriptions;
if (configDescriptions != null) {
mapConfigDescriptions = getConfigDescriptionMap(configDescriptions);
normalizeConfiguration(rule.getConfiguration(), mapConfigDescriptions);
}
normalizeModuleConfigurations(rule.getTriggers());
normalizeModuleConfigurations(rule.getConditions());
normalizeModuleConfigurations(rule.getActions());
}
private <T extends Module> void normalizeModuleConfigurations(List<T> modules) {
for (Module module : modules) {
Configuration config = module.getConfiguration();
if (config != null) {
String type = module.getTypeUID();
ModuleType mt = mtRegistry.get(type);
if (mt != null) {
List<ConfigDescriptionParameter> configDescriptions = mt.getConfigurationDescriptions();
if (configDescriptions != null) {
Map<String, ConfigDescriptionParameter> mapConfigDescriptions = getConfigDescriptionMap(
configDescriptions);
normalizeConfiguration(config, mapConfigDescriptions);
}
}
}
}
}
private Map<String, ConfigDescriptionParameter> getConfigDescriptionMap(
List<ConfigDescriptionParameter> configDesc) {
Map<String, ConfigDescriptionParameter> mapConfigDescs = null;
if (configDesc != null) {
for (ConfigDescriptionParameter configDescriptionParameter : configDesc) {
if (mapConfigDescs == null) {
mapConfigDescs = new HashMap<String, ConfigDescriptionParameter>();
}
mapConfigDescs.put(configDescriptionParameter.getName(), configDescriptionParameter);
}
}
return mapConfigDescs;
}
private void normalizeConfiguration(Configuration config, Map<String, ConfigDescriptionParameter> mapCD) {
if (config != null && mapCD != null) {
for (String propName : mapCD.keySet()) {
ConfigDescriptionParameter cd = mapCD.get(propName);
if (cd != null) {
Object tmp = config.get(propName);
Object defaultValue = cd.getDefault();
if (tmp == null && defaultValue != null) {
config.put(propName, defaultValue);
}
if (cd.isMultiple()) {
tmp = config.get(propName);
if (tmp != null && tmp instanceof String) {
String sValue = (String) tmp;
if (gson == null) {
gson = new Gson();
}
try {
Object value = gson.fromJson(sValue, List.class);
config.put(propName, value);
} catch (JsonSyntaxException e) {
logger.error("Can't parse {} to list value.", sValue, e);
}
continue;
}
}
}
Object value = ConfigUtil.normalizeType(config.get(propName), cd);
if (value != null) {
config.put(propName, value);
}
}
}
}
}