mirror of
https://github.com/danieldemus/openhab-core.git
synced 2025-01-25 11:45:49 +01:00
[rules] Add support for pre-compilation of conditions and actions (#4289)
* ScriptConditionHandler/ScriptActionHandler: Add support for pre-compilation of scripts Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
This commit is contained in:
parent
ea7d61b199
commit
918b4faa3b
@ -18,10 +18,14 @@ import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.script.Compilable;
|
||||
import javax.script.CompiledScript;
|
||||
import javax.script.ScriptContext;
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.automation.Module;
|
||||
import org.openhab.core.automation.handler.BaseModuleHandler;
|
||||
import org.openhab.core.automation.module.script.ScriptEngineContainer;
|
||||
@ -35,6 +39,7 @@ import org.slf4j.LoggerFactory;
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution
|
||||
* @author Simon Merschjohann - Initial contribution
|
||||
* @author Florian Hotze - Add support for script pre-compilation
|
||||
*
|
||||
* @param <T> the type of module the concrete handler can handle
|
||||
*/
|
||||
@ -54,6 +59,7 @@ public abstract class AbstractScriptModuleHandler<T extends Module> extends Base
|
||||
private final String engineIdentifier;
|
||||
|
||||
private Optional<ScriptEngine> scriptEngine = Optional.empty();
|
||||
private Optional<CompiledScript> compiledScript = Optional.empty();
|
||||
private final String type;
|
||||
protected final String script;
|
||||
|
||||
@ -80,6 +86,34 @@ public abstract class AbstractScriptModuleHandler<T extends Module> extends Base
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the {@link ScriptEngine} and compiles the script if the {@link ScriptEngine} implements
|
||||
* {@link Compilable}.
|
||||
*/
|
||||
protected void compileScript() throws ScriptException {
|
||||
if (compiledScript.isPresent()) {
|
||||
return;
|
||||
}
|
||||
if (!scriptEngineManager.isSupported(this.type)) {
|
||||
logger.debug(
|
||||
"ScriptEngine for language '{}' could not be found, skipping compilation of script for identifier: {}",
|
||||
type, engineIdentifier);
|
||||
return;
|
||||
}
|
||||
Optional<ScriptEngine> engine = getScriptEngine();
|
||||
if (engine.isPresent()) {
|
||||
ScriptEngine scriptEngine = engine.get();
|
||||
if (scriptEngine instanceof Compilable) {
|
||||
logger.debug("Pre-compiling script of rule with UID '{}'", ruleUID);
|
||||
compiledScript = Optional.ofNullable(((Compilable) scriptEngine).compile(script));
|
||||
} else {
|
||||
logger.error(
|
||||
"Script engine of rule with UID '{}' does not implement Compilable but claims to support pre-compilation",
|
||||
module.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
scriptEngineManager.removeEngine(engineIdentifier);
|
||||
@ -169,4 +203,26 @@ public abstract class AbstractScriptModuleHandler<T extends Module> extends Base
|
||||
executionContext.removeAttribute(key, ScriptContext.ENGINE_SCOPE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the passed script with the ScriptEngine.
|
||||
*
|
||||
* @param engine the script engine that is used
|
||||
* @param script the script to evaluate
|
||||
* @return the value returned from the execution of the script
|
||||
*/
|
||||
protected @Nullable Object eval(ScriptEngine engine, String script) {
|
||||
try {
|
||||
if (compiledScript.isPresent()) {
|
||||
logger.debug("Executing pre-compiled script of rule with UID '{}'", ruleUID);
|
||||
return compiledScript.get().eval(engine.getContext());
|
||||
}
|
||||
logger.debug("Executing script of rule with UID '{}'", ruleUID);
|
||||
return engine.eval(script);
|
||||
} catch (ScriptException e) {
|
||||
logger.error("Script execution of rule with UID '{}' failed: {}", ruleUID, e.getMessage(),
|
||||
logger.isDebugEnabled() ? e : null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory;
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution
|
||||
* @author Simon Merschjohann - Initial contribution
|
||||
* @author Florian Hotze - Add support for script pre-compilation
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ScriptActionHandler extends AbstractScriptModuleHandler<Action> implements ActionHandler {
|
||||
@ -61,6 +62,11 @@ public class ScriptActionHandler extends AbstractScriptModuleHandler<Action> imp
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void compile() throws ScriptException {
|
||||
super.compileScript();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Map<String, Object> execute(final Map<String, Object> context) {
|
||||
Map<String, Object> resultMap = new HashMap<>();
|
||||
@ -71,13 +77,8 @@ public class ScriptActionHandler extends AbstractScriptModuleHandler<Action> imp
|
||||
|
||||
getScriptEngine().ifPresent(scriptEngine -> {
|
||||
setExecutionContext(scriptEngine, context);
|
||||
try {
|
||||
Object result = scriptEngine.eval(script);
|
||||
resultMap.put("result", result);
|
||||
} catch (ScriptException e) {
|
||||
logger.error("Script execution of rule with UID '{}' failed: {}", ruleUID, e.getMessage(),
|
||||
logger.isDebugEnabled() ? e : null);
|
||||
}
|
||||
Object result = eval(scriptEngine, script);
|
||||
resultMap.put("result", result);
|
||||
resetExecutionContext(scriptEngine, context);
|
||||
});
|
||||
|
||||
|
@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution
|
||||
* @author Simon Merschjohann - Initial contribution
|
||||
* @author Florian Hotze - Add support for script pre-compilation
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ScriptConditionHandler extends AbstractScriptModuleHandler<Condition> implements ConditionHandler {
|
||||
@ -42,6 +43,11 @@ public class ScriptConditionHandler extends AbstractScriptModuleHandler<Conditio
|
||||
super(module, ruleUID, scriptEngineManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void compile() throws ScriptException {
|
||||
super.compileScript();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSatisfied(final Map<String, Object> context) {
|
||||
boolean result = false;
|
||||
@ -55,18 +61,14 @@ public class ScriptConditionHandler extends AbstractScriptModuleHandler<Conditio
|
||||
if (engine.isPresent()) {
|
||||
ScriptEngine scriptEngine = engine.get();
|
||||
setExecutionContext(scriptEngine, context);
|
||||
try {
|
||||
Object returnVal = scriptEngine.eval(script);
|
||||
if (returnVal instanceof Boolean boolean1) {
|
||||
result = boolean1;
|
||||
} else {
|
||||
logger.error("Script of rule with UID '{}' did not return a boolean value, but '{}'", ruleUID,
|
||||
returnVal);
|
||||
}
|
||||
} catch (ScriptException e) {
|
||||
logger.error("Script execution of rule with UID '{}' failed: {}", ruleUID, e.getMessage(),
|
||||
logger.isDebugEnabled() ? e : null);
|
||||
Object returnVal = eval(scriptEngine, script);
|
||||
if (returnVal instanceof Boolean boolean1) {
|
||||
result = boolean1;
|
||||
} else {
|
||||
logger.error("Script of rule with UID '{}' did not return a boolean value, but '{}'", ruleUID,
|
||||
returnVal);
|
||||
}
|
||||
resetExecutionContext(scriptEngine, context);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@ -31,6 +31,14 @@ import org.openhab.core.automation.Trigger;
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ActionHandler extends ModuleHandler {
|
||||
/**
|
||||
* Called to compile an {@link Action} of the {@link Rule} when the rule is initialized.
|
||||
*
|
||||
* @throws Exception if the compilation fails
|
||||
*/
|
||||
default void compile() throws Exception {
|
||||
// Do nothing by default
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to execute an {@link Action} of the {@link Rule} when it is needed.
|
||||
|
@ -29,6 +29,14 @@ import org.openhab.core.automation.Trigger;
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ConditionHandler extends ModuleHandler {
|
||||
/**
|
||||
* Called to compile the {@link Condition} when the {@link Rule} is initialized.
|
||||
*
|
||||
* @throws Exception if the compilation fails
|
||||
*/
|
||||
default void compile() throws Exception {
|
||||
// Do nothing by default
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Condition is satisfied in the given {@code context}.
|
||||
|
@ -110,6 +110,7 @@ import org.slf4j.LoggerFactory;
|
||||
* @author Benedikt Niehues - change behavior for unregistering ModuleHandler
|
||||
* @author Markus Rathgeb - use a managed rule
|
||||
* @author Ana Dimova - new reference syntax: list[index], map["key"], bean.field
|
||||
* @author Florian Hotze - add support for script condition/action compilation
|
||||
*/
|
||||
@Component(immediate = true, service = { RuleManager.class })
|
||||
@NonNullByDefault
|
||||
@ -819,6 +820,8 @@ public class RuleEngineImpl implements RuleManager, RegistryChangeListener<Modul
|
||||
* <ul>
|
||||
* <li>Set the module handlers. If there are errors, set the rule status (handler error) and return with error
|
||||
* indication.
|
||||
* <li>Compile the conditions and actions. If there are errors, set the rule status (handler error) and return with
|
||||
* indication.
|
||||
* <li>Register the rule. Set the rule status and return with success indication.
|
||||
* </ul>
|
||||
*
|
||||
@ -845,6 +848,11 @@ public class RuleEngineImpl implements RuleManager, RegistryChangeListener<Modul
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compile the conditions and actions and so check if they are valid.
|
||||
if (!compileRule(rule)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Register the rule and set idle status.
|
||||
register(rule);
|
||||
setStatus(ruleUID, new RuleStatusInfo(RuleStatus.IDLE));
|
||||
@ -862,6 +870,58 @@ public class RuleEngineImpl implements RuleManager, RegistryChangeListener<Modul
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile the conditions and actions of the given rule.
|
||||
* If there are errors, set the rule status (handler error) and return with indication.
|
||||
*
|
||||
* @param rule the rule whose conditions and actions should be compiled
|
||||
* @return true if compilation succeeded, otherwise false
|
||||
*/
|
||||
private boolean compileRule(final WrappedRule rule) {
|
||||
try {
|
||||
compileConditions(rule);
|
||||
compileActions(rule);
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
setStatus(rule.getUID(), new RuleStatusInfo(RuleStatus.UNINITIALIZED,
|
||||
RuleStatusDetail.HANDLER_INITIALIZING_ERROR, t.getMessage()));
|
||||
unregister(rule);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile the conditions and actions of the given rule.
|
||||
* If there are errors, set the rule status (handler error).
|
||||
*
|
||||
* @param ruleUID the UID of the rule whose conditions and actions should be compiled
|
||||
*/
|
||||
private void compileRule(String ruleUID) {
|
||||
final WrappedRule rule = getManagedRule(ruleUID);
|
||||
if (rule == null) {
|
||||
logger.warn("Failed to compile rule '{}': Invalid Rule UID", ruleUID);
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
final RuleStatus ruleStatus = getRuleStatus(ruleUID);
|
||||
if (ruleStatus != null && ruleStatus != RuleStatus.IDLE) {
|
||||
logger.error("Failed to compile rule ‘{}' with status '{}'", ruleUID, ruleStatus.name());
|
||||
return;
|
||||
}
|
||||
// change state to INITIALIZING
|
||||
setStatus(ruleUID, new RuleStatusInfo(RuleStatus.INITIALIZING));
|
||||
}
|
||||
if (!compileRule(rule)) {
|
||||
return;
|
||||
}
|
||||
// change state to IDLE only if the rule has not been DISABLED.
|
||||
synchronized (this) {
|
||||
if (getRuleStatus(ruleUID) == RuleStatus.INITIALIZING) {
|
||||
setStatus(ruleUID, new RuleStatusInfo(RuleStatus.IDLE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable RuleStatusInfo getStatusInfo(String ruleUID) {
|
||||
final WrappedRule rule = managedRules.get(ruleUID);
|
||||
@ -1134,6 +1194,32 @@ public class RuleEngineImpl implements RuleManager, RegistryChangeListener<Modul
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method compiles conditions of the {@link Rule} when they exist.
|
||||
* It is called when the rule is initialized.
|
||||
*
|
||||
* @param rule compiled rule.
|
||||
*/
|
||||
private void compileConditions(WrappedRule rule) {
|
||||
final Collection<WrappedCondition> conditions = rule.getConditions();
|
||||
if (conditions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (WrappedCondition wrappedCondition : conditions) {
|
||||
final Condition condition = wrappedCondition.unwrap();
|
||||
ConditionHandler cHandler = wrappedCondition.getModuleHandler();
|
||||
if (cHandler != null) {
|
||||
try {
|
||||
cHandler.compile();
|
||||
} catch (Throwable t) {
|
||||
String errMessage = "Failed to pre-compile condition: " + condition.getId() + "(" + t.getMessage()
|
||||
+ ")";
|
||||
throw new RuntimeException(errMessage, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks if all rule's condition are satisfied or not.
|
||||
*
|
||||
@ -1163,6 +1249,31 @@ public class RuleEngineImpl implements RuleManager, RegistryChangeListener<Modul
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method compiles actions of the {@link Rule} when they exist.
|
||||
* It is called when the rule is initialized.
|
||||
*
|
||||
* @param rule compiled rule.
|
||||
*/
|
||||
private void compileActions(WrappedRule rule) {
|
||||
final Collection<WrappedAction> actions = rule.getActions();
|
||||
if (actions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (WrappedAction wrappedAction : actions) {
|
||||
final Action action = wrappedAction.unwrap();
|
||||
ActionHandler aHandler = wrappedAction.getModuleHandler();
|
||||
if (aHandler != null) {
|
||||
try {
|
||||
aHandler.compile();
|
||||
} catch (Throwable t) {
|
||||
String errMessage = "Failed to pre-compile action: " + action.getId() + "(" + t.getMessage() + ")";
|
||||
throw new RuntimeException(errMessage, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method evaluates actions of the {@link Rule} and set their {@link Output}s when they exist.
|
||||
*
|
||||
@ -1435,7 +1546,7 @@ public class RuleEngineImpl implements RuleManager, RegistryChangeListener<Modul
|
||||
|
||||
@Override
|
||||
public void onReadyMarkerAdded(ReadyMarker readyMarker) {
|
||||
executeRulesWithStartLevel();
|
||||
compileRules();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1443,6 +1554,20 @@ public class RuleEngineImpl implements RuleManager, RegistryChangeListener<Modul
|
||||
started = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method compiles the conditions and actions of all rules. It is called when the rule engine is started.
|
||||
* By compiling when the rule engine is started, we make sure all conditions and actions are compiled, even if their
|
||||
* handlers weren't available when the rule was added to the rule engine.
|
||||
*/
|
||||
private void compileRules() {
|
||||
getScheduledExecutor().submit(() -> {
|
||||
ruleRegistry.getAll().forEach(r -> {
|
||||
compileRule(r.getUID());
|
||||
});
|
||||
executeRulesWithStartLevel();
|
||||
});
|
||||
}
|
||||
|
||||
private void executeRulesWithStartLevel() {
|
||||
getScheduledExecutor().submit(() -> {
|
||||
ruleRegistry.getAll().stream() //
|
||||
|
Loading…
Reference in New Issue
Block a user