
org.openremote.manager.rules.RulesetDeployment Maven / Gradle / Ivy
/*
* Copyright 2018, OpenRemote Inc.
*
* See the CONTRIBUTORS.txt file in the distribution for a
* full listing of individual contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package org.openremote.manager.rules;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.jeasy.rules.api.Action;
import org.jeasy.rules.api.Condition;
import org.jeasy.rules.api.Rule;
import org.jeasy.rules.api.Rules;
import org.jeasy.rules.core.RuleBuilder;
import org.kohsuke.groovy.sandbox.GroovyValueFilter;
import org.kohsuke.groovy.sandbox.SandboxTransformer;
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
import org.openremote.container.timer.TimerService;
import org.openremote.manager.asset.AssetStorageService;
import org.openremote.model.calendar.CalendarEvent;
import org.openremote.model.rules.*;
import org.openremote.model.rules.flow.NodeCollection;
import org.openremote.model.syslog.SyslogCategory;
import org.openremote.model.util.Pair;
import org.openremote.model.util.TextUtil;
import org.openremote.model.util.ValueUtil;
import javax.script.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.openremote.model.rules.RulesetStatus.*;
public class RulesetDeployment {
// TODO Finish groovy sandbox
static class GroovyDenyAllFilter extends GroovyValueFilter {
@Override
public Object filterReceiver(Object receiver) {
throw new SecurityException("Not allowed: " + receiver);
}
}
public static final int DEFAULT_RULE_PRIORITY = 1000;
// Share one JS script engine manager, it's thread-safe
static final protected ScriptEngineManager scriptEngineManager;
static final protected GroovyShell groovyShell;
static {
scriptEngineManager = new ScriptEngineManager();
/* TODO Sharing a static GroovyShell doesn't work, redeploying a ruleset which defines classes (e.g. Flight) is broken:
java.lang.RuntimeException: Error evaluating condition of rule '-Update flight facts when estimated landing time of flight asset is updated':
No signature of method: org.openremote.manager.setup.database.Script1$_run_closure2$_closure14$_closure17.doCall() is applicable for argument types: (org.openremote.manager.setup.database.Flight) values: [...]
Possible solutions: doCall(org.openremote.manager.setup.database.Flight), findAll(), findAll(), isCase(java.lang.Object), isCase(java.lang.Object)
The following classes appear as argument class and as parameter class, but are defined by different class loader:
org.openremote.manager.setup.database.Flight (defined by 'groovy.lang.GroovyClassLoader$InnerLoader@2cc34cd5' and 'groovy.lang.GroovyClassLoader$InnerLoader@1af957bc')
If one of the method suggestions matches the method you wanted to call,
then check your class loader setup.
*/
groovyShell = new GroovyShell(
new CompilerConfiguration().addCompilationCustomizers(new SandboxTransformer())
);
}
protected static final Pair ALWAYS_ACTIVE = new Pair<>(0L, Long.MAX_VALUE);
protected static final Pair EXPIRED = new Pair<>(0L, 0L);
final protected Ruleset ruleset;
final protected Rules rules = new Rules();
final protected AssetStorageService assetStorageService;
final protected TimerService timerService;
final protected ExecutorService executorService;
final protected ScheduledExecutorService scheduledExecutorService;
final protected Assets assetsFacade;
final protected Users usersFacade;
final protected Notifications notificationsFacade;
final protected Webhooks webhooksFacade;
final protected Alarms alarmsFacade;
final protected HistoricDatapoints historicDatapointsFacade;
final protected PredictedDatapoints predictedDatapointsFacade;
final protected List> scheduledRuleActions = Collections.synchronizedList(new ArrayList<>());
final protected RulesEngine> rulesEngine;
protected final Logger LOG;
protected boolean running;
protected RulesetStatus status = RulesetStatus.READY;
protected Throwable error;
protected JsonRulesBuilder jsonRulesBuilder;
protected FlowRulesBuilder flowRulesBuilder;
protected CalendarEvent validity;
protected Pair nextValidity;
public RulesetDeployment(Ruleset ruleset, RulesEngine> rulesEngine, TimerService timerService,
AssetStorageService assetStorageService, ExecutorService executorService, ScheduledExecutorService scheduledExecutorService,
Assets assetsFacade, Users usersFacade, Notifications notificationsFacade, Webhooks webhooksFacade,
Alarms alarmsFacade, HistoricDatapoints historicDatapointsFacade, PredictedDatapoints predictedDatapointsFacade) {
this.ruleset = ruleset;
this.rulesEngine = rulesEngine;
this.timerService = timerService;
this.assetStorageService = assetStorageService;
this.executorService = executorService;
this.scheduledExecutorService = scheduledExecutorService;
this.assetsFacade = assetsFacade;
this.usersFacade = usersFacade;
this.notificationsFacade = notificationsFacade;
this.webhooksFacade = webhooksFacade;
this.alarmsFacade = alarmsFacade;
this.historicDatapointsFacade = historicDatapointsFacade;
this.predictedDatapointsFacade = predictedDatapointsFacade;
String ruleCategory = ruleset.getClass().getSimpleName() + "-" + ruleset.getId();
LOG = SyslogCategory.getLogger(SyslogCategory.RULES, RulesEngine.class.getName() + "." + ruleCategory);
}
protected void init() throws IllegalStateException {
if (ruleset.getMeta().containsKey(Ruleset.VALIDITY)) {
validity = ruleset.getValidity();
if (validity == null) {
LOG.log(Level.WARNING, "Ruleset '" + ruleset.getName() + "' has invalid validity value: " + ruleset.getMeta().get(Ruleset.VALIDITY));
status = VALIDITY_PERIOD_ERROR;
return;
}
}
if (TextUtil.isNullOrEmpty(ruleset.getRules())) {
LOG.finest("Ruleset is empty so no rules to deploy: " + ruleset.getName());
status = EMPTY;
return;
}
if (!ruleset.isEnabled()) {
LOG.finest("Ruleset is disabled: " + ruleset.getName());
status = DISABLED;
}
if (!compile()) {
LOG.log(Level.SEVERE, "Ruleset compilation error: " + ruleset.getName(), getError());
status = COMPILATION_ERROR;
}
}
public long getId() {
return ruleset.getId();
}
public String getName() {
return ruleset.getName();
}
public long getVersion() {
return ruleset.getVersion();
}
public Ruleset getRuleset() {
return ruleset;
}
public Rules getRules() {
return rules;
}
protected void updateValidity() {
Pair fromTo = validity.getNextOrActiveFromTo(new Date(timerService.getCurrentTimeMillis()));
if (fromTo == null) {
nextValidity = EXPIRED;
LOG.log(Level.INFO, "Ruleset deployment '" + getName() + "' has expired");
} else {
nextValidity = fromTo;
LOG.log(Level.INFO, "Ruleset deployment '" + getName() + "' paused until: " + new Date(fromTo.key));
}
}
/**
* Returns the current or next time window in which this rule is active
* @return null if deployment has expired
*/
public Pair getNextOrActiveFromTo() {
if (validity == null) {
return ALWAYS_ACTIVE;
}
if (nextValidity == EXPIRED) {
return nextValidity;
}
if (nextValidity == null || nextValidity.value <= timerService.getCurrentTimeMillis()) {
updateValidity();
}
return nextValidity;
}
public boolean compile() {
LOG.info("Compiling ruleset deployment: " + ruleset);
if (error != null) {
return false;
}
switch (ruleset.getLang()) {
case JAVASCRIPT:
return compileRulesJavascript(ruleset, assetsFacade, usersFacade, notificationsFacade, historicDatapointsFacade, predictedDatapointsFacade);
case GROOVY:
return compileRulesGroovy(ruleset, assetsFacade, usersFacade, notificationsFacade, historicDatapointsFacade, predictedDatapointsFacade);
case JSON:
return compileRulesJson(ruleset);
case FLOW:
return compileRulesFlow(ruleset, assetsFacade, usersFacade, notificationsFacade, historicDatapointsFacade, predictedDatapointsFacade);
}
return false;
}
public boolean canStart() {
return status != COMPILATION_ERROR && status != DISABLED && status != RulesetStatus.EXPIRED;
}
/**
* Called when a ruleset is started (allows for initialisation tasks)
*/
public boolean start(RulesFacts facts) {
if (!canStart()) {
return false;
}
if (jsonRulesBuilder != null) {
jsonRulesBuilder.start(facts);
}
running = true;
return true;
}
/**
* Called when this deployment is stopped, could be the ruleset is being updated, removed or an error has occurred
* during execution
*/
public boolean stop(RulesFacts facts) {
if (!running) {
return false;
}
running = false;
synchronized (scheduledRuleActions) {
scheduledRuleActions.removeIf(scheduledFuture -> {
scheduledFuture.cancel(true);
return true;
});
}
if (jsonRulesBuilder != null) {
jsonRulesBuilder.stop(facts);
}
return true;
}
public void onAssetStatesChanged(RulesFacts facts, RulesEngine.AssetStateChangeEvent event) {
if (jsonRulesBuilder != null) {
jsonRulesBuilder.onAssetStatesChanged(facts, event);
}
}
protected void scheduleRuleAction(Runnable action, long delayMillis) {
ScheduledFuture> future = scheduledExecutorService.schedule(() -> {
scheduledRuleActions.removeIf(Future::isDone);
action.run();
}, delayMillis, TimeUnit.MILLISECONDS);
scheduledRuleActions.add(future);
}
protected boolean compileRulesJson(Ruleset ruleset) {
try {
jsonRulesBuilder = new JsonRulesBuilder(LOG, ruleset, rulesEngine, timerService, assetStorageService, assetsFacade, usersFacade, notificationsFacade, webhooksFacade, alarmsFacade, historicDatapointsFacade, predictedDatapointsFacade, this::scheduleRuleAction);
for (Rule rule : jsonRulesBuilder.build()) {
LOG.finest("Registering JSON rule: " + rule.getName());
rules.register(rule);
}
return true;
} catch (Exception e) {
setError(e);
return false;
}
}
protected boolean compileRulesJavascript(Ruleset ruleset, Assets assetsFacade, Users usersFacade, Notifications notificationsFacade, HistoricDatapoints historicDatapointsFacade, PredictedDatapoints predictedDatapointsFacade) {
// TODO https://github.com/pfisterer/scripting-sandbox/blob/master/src/main/java/de/farberg/scripting/sandbox/ScriptingSandbox.java
ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("nashorn");
ScriptContext newContext = new SimpleScriptContext();
newContext.setBindings(scriptEngine.createBindings(), ScriptContext.ENGINE_SCOPE);
Bindings engineScope = newContext.getBindings(ScriptContext.ENGINE_SCOPE);
engineScope.put("LOG", LOG);
engineScope.put("assets", assetsFacade);
engineScope.put("users", usersFacade);
engineScope.put("notifications", notificationsFacade);
engineScope.put("historicDatapoints", historicDatapointsFacade);
engineScope.put("predictedDatapoints", predictedDatapointsFacade);
String script = ruleset.getRules();
// Default header/imports for all rules scripts
script = "load(\"nashorn:mozilla_compat.js\");\n" + // This provides importPackage
"\n" +
"importPackage(\n" +
" \"java.util.stream\",\n" +
" \"org.openremote.model.asset\",\n" +
" \"org.openremote.model.attribute\",\n" +
" \"org.openremote.model.value\",\n" +
" \"org.openremote.model.rules\",\n" +
" \"org.openremote.model.query\"\n" +
");\n" +
"var Match = Java.type(\"org.openremote.model.query.AssetQuery$Match\");\n" +
"var Operator = Java.type(\"org.openremote.model.query.AssetQuery$Operator\");\n" +
"var NumberType = Java.type(\"org.openremote.model.query.AssetQuery$NumberType\");\n" +
"var StringPredicate = Java.type(\"org.openremote.model.query.filter.StringPredicate\");\n" +
"var BooleanPredicate = Java.type(\"org.openremote.model.query.filter.BooleanPredicate\");\n" +
"var StringArrayPredicate = Java.type(\"org.openremote.model.query.filter.StringArrayPredicate\");\n" +
"var DateTimePredicate = Java.type(\"org.openremote.model.query.filter.DateTimePredicate\");\n" +
"var NumberPredicate = Java.type(\"org.openremote.model.query.filter.NumberPredicate\");\n" +
"var ParentPredicate = Java.type(\"org.openremote.model.query.filter.ParentPredicate\");\n" +
"var PathPredicate = Java.type(\"org.openremote.model.query.filter.PathPredicate\");\n" +
"var RealmPredicate = Java.type(\"org.openremote.model.query.filter.RealmPredicate\");\n" +
"var AttributePredicate = Java.type(\"org.openremote.model.query.filter.AttributePredicate\");\n" +
"var AttributeExecuteStatus = Java.type(\"org.openremote.model.attribute.AttributeExecuteStatus\");\n" +
"var EXACT = Match.EXACT;\n" +
"var BEGIN = Match.BEGIN;\n" +
"var END = Match.END;\n" +
"var CONTAINS = Match.CONTAINS;\n" +
"var EQUALS = Operator.EQUALS;\n" +
"var GREATER_THAN = Operator.GREATER_THAN;\n" +
"var GREATER_EQUALS = Operator.GREATER_EQUALS;\n" +
"var LESS_THAN = Operator.LESS_THAN;\n" +
"var LESS_EQUALS = Operator.LESS_EQUALS;\n" +
"var BETWEEN = Operator.BETWEEN;\n" +
"var REQUEST_START = AttributeExecuteStatus.REQUEST_START;\n" +
"var REQUEST_REPEATING = AttributeExecuteStatus.REQUEST_REPEATING;\n" +
"var REQUEST_CANCEL = AttributeExecuteStatus.REQUEST_CANCEL;\n" +
"var READY = AttributeExecuteStatus.READY;\n" +
"var COMPLETED = AttributeExecuteStatus.COMPLETED;\n" +
"var RUNNING = AttributeExecuteStatus.RUNNING;\n" +
"var CANCELLED = AttributeExecuteStatus.CANCELLED;\n" +
"var ERROR = AttributeExecuteStatus.ERROR;\n" +
"var DISABLED = AttributeExecuteStatus.DISABLED;\n" +
"\n"
+ script;
try {
scriptEngine.eval(script, engineScope);
compileRulesJavascript((ScriptObjectMirror) engineScope.get("rules"));
return true;
} catch (Exception e) {
setError(e);
engineScope.clear();
return false;
}
}
/**
* Marshal the JavaScript rules array into {@link Rule} instances.
*/
protected void compileRulesJavascript(ScriptObjectMirror scriptRules) {
if (scriptRules == null || !scriptRules.isArray()) {
throw new IllegalArgumentException("No 'rules' array defined in ruleset");
}
Collection
© 2015 - 2025 Weber Informatics LLC | Privacy Policy