Skip to content

Commit

Permalink
[rules] Add support for pre-compilation of conditions and actions (#4289
Browse files Browse the repository at this point in the history
)

* ScriptConditionHandler/ScriptActionHandler: Add support for pre-compilation of scripts

Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
  • Loading branch information
florian-h05 authored Jul 9, 2024
1 parent ea7d61b commit 918b4fa
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,6 +39,7 @@
*
* @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
*/
Expand All @@ -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;

Expand All @@ -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<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);
Expand Down Expand Up @@ -169,4 +203,26 @@ protected void resetExecutionContext(ScriptEngine engine, Map<String, ?> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Action> implements ActionHandler {
Expand Down Expand Up @@ -61,6 +62,11 @@ public void dispose() {
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<>();
Expand All @@ -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);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Condition> implements ConditionHandler {
Expand All @@ -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<String, Object> context) {
boolean result = false;
Expand All @@ -55,18 +61,14 @@ public boolean isSatisfied(final Map<String, Object> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -819,6 +820,8 @@ public synchronized void setEnabled(String uid, boolean enable) {
* <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>
*
Expand All @@ -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));
Expand All @@ -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);
Expand Down Expand Up @@ -1134,6 +1194,32 @@ private Map<String, Object> getContext(String ruleUID, @Nullable Set<Connection>
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.
*
Expand Down Expand Up @@ -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<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.
*
Expand Down Expand Up @@ -1435,14 +1546,28 @@ public String getOutputName() {

@Override
public void onReadyMarkerAdded(ReadyMarker readyMarker) {
executeRulesWithStartLevel();
compileRules();
}

@Override
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() //
Expand Down

0 comments on commit 918b4fa

Please sign in to comment.