diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/AbstractScriptModuleHandler.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/AbstractScriptModuleHandler.java index de3661021eb..325d742be58 100644 --- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/AbstractScriptModuleHandler.java +++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/AbstractScriptModuleHandler.java @@ -18,10 +18,14 @@ 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 @@ * * @author Kai Kreuzer - Initial contribution * @author Simon Merschjohann - Initial contribution + * @author Florian Hotze - Add support for script pre-compilation * * @param the type of module the concrete handler can handle */ @@ -54,6 +59,7 @@ public abstract class AbstractScriptModuleHandler extends Base private final String engineIdentifier; private Optional scriptEngine = Optional.empty(); + private Optional compiledScript = Optional.empty(); private final String type; protected final String script; @@ -80,6 +86,34 @@ private static String getValidConfigParameter(String parameter, Configuration co } } + /** + * 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 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 @@ protected void resetExecutionContext(ScriptEngine engine, Map context 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; + } + } } diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptActionHandler.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptActionHandler.java index 772a0ced3f2..a170683b881 100644 --- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptActionHandler.java +++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptActionHandler.java @@ -31,6 +31,7 @@ * * @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 implements ActionHandler { @@ -61,6 +62,11 @@ public void dispose() { super.dispose(); } + @Override + public void compile() throws ScriptException { + super.compileScript(); + } + @Override public @Nullable Map execute(final Map context) { Map resultMap = new HashMap<>(); @@ -71,13 +77,8 @@ public void dispose() { 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); }); diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptConditionHandler.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptConditionHandler.java index d25ff4e1077..a51ad384195 100644 --- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptConditionHandler.java +++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptConditionHandler.java @@ -30,6 +30,7 @@ * * @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 implements ConditionHandler { @@ -42,6 +43,11 @@ public ScriptConditionHandler(Condition module, String ruleUID, ScriptEngineMana super(module, ruleUID, scriptEngineManager); } + @Override + public void compile() throws ScriptException { + super.compileScript(); + } + @Override public boolean isSatisfied(final Map context) { boolean result = false; @@ -55,18 +61,14 @@ public boolean isSatisfied(final Map context) { 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; diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ActionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ActionHandler.java index 355dba470c6..bc3599ee9eb 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ActionHandler.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ActionHandler.java @@ -31,6 +31,14 @@ */ @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. diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ConditionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ConditionHandler.java index 7adc0b6ce2d..5bb5cf4bb00 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ConditionHandler.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ConditionHandler.java @@ -29,6 +29,14 @@ */ @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}. diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleEngineImpl.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleEngineImpl.java index 07b803cca73..69fb534fadc 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleEngineImpl.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleEngineImpl.java @@ -110,6 +110,7 @@ * @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 synchronized void setEnabled(String uid, boolean enable) { *
    *
  • Set the module handlers. If there are errors, set the rule status (handler error) and return with error * indication. + *
  • Compile the conditions and actions. If there are errors, set the rule status (handler error) and return with + * indication. *
  • Register the rule. Set the rule status and return with success indication. *
* @@ -845,6 +848,11 @@ private boolean activateRule(final WrappedRule rule) { 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 @@ private boolean activateRule(final WrappedRule rule) { 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 @@ private Map getContext(String ruleUID, @Nullable Set 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 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 @@ private boolean calculateConditions(WrappedRule rule) { 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 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 String getOutputName() { @Override public void onReadyMarkerAdded(ReadyMarker readyMarker) { - executeRulesWithStartLevel(); + compileRules(); } @Override @@ -1443,6 +1554,20 @@ public void onReadyMarkerRemoved(ReadyMarker readyMarker) { 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() //