From 5b12280f5b43f1456ac31885831d5b6ac5df283d Mon Sep 17 00:00:00 2001 From: Nadahar Date: Mon, 26 May 2025 22:53:17 +0200 Subject: [PATCH] Regenerate rules from templates (#4718) * Rule template regeneration support Signed-off-by: Ravi Nadahar --- .../RuleSupportRuleRegistryDelegate.java | 5 + .../moduletypes/PrivateScriptedTypes.json | 26 +-- .../resources/OH-INF/i18n/jsr223.properties | 26 +-- .../provider/ScriptModuleTypeProvider.java | 10 +- .../rest/internal/ModuleTypeResource.java | 36 +++- .../rest/internal/RuleResource.java | 28 +++- .../org/openhab/core/automation/Rule.java | 70 ++++++++ .../openhab/core/automation/RuleRegistry.java | 9 + .../core/automation/RuleStatusDetail.java | 13 +- .../openhab/core/automation/dto/RuleDTO.java | 14 +- .../core/automation/dto/RuleDTOMapper.java | 12 +- .../automation/internal/RuleEngineImpl.java | 16 +- .../core/automation/internal/RuleImpl.java | 22 ++- .../automation/internal/RuleRegistryImpl.java | 158 ++++++++++-------- .../core/automation/util/RuleBuilder.java | 16 +- .../test/AutomationIntegrationTest.java | 5 +- 16 files changed, 332 insertions(+), 134 deletions(-) diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/RuleSupportRuleRegistryDelegate.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/RuleSupportRuleRegistryDelegate.java index 5db02c78bb..d51c734d0a 100644 --- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/RuleSupportRuleRegistryDelegate.java +++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/RuleSupportRuleRegistryDelegate.java @@ -118,4 +118,9 @@ public class RuleSupportRuleRegistryDelegate implements RuleRegistry { public Collection getByTags(String... tags) { return ruleRegistry.getByTags(tags); } + + @Override + public void regenerateFromTemplate(String ruleUID) { + ruleRegistry.regenerateFromTemplate(ruleUID); + } } diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/resources/OH-INF/automation/moduletypes/PrivateScriptedTypes.json b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/resources/OH-INF/automation/moduletypes/PrivateScriptedTypes.json index 2529e85ce1..955953bcaf 100644 --- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/resources/OH-INF/automation/moduletypes/PrivateScriptedTypes.json +++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/resources/OH-INF/automation/moduletypes/PrivateScriptedTypes.json @@ -2,14 +2,14 @@ "conditions": [ { "uid": "jsr223.ScriptedCondition", - "label": "Scripted condition", - "description": "allows the definition of a condition by a script", + "label": "Opaque Condition", + "description": "Evaluates a condition using external code. See the rule source for details.", "visibility": "EXPERT", "configDescriptions": [ { "name": "privId", "type": "TEXT", - "description": "the identifier of the private method", + "description": "The identifier of the private method", "required": true } ] @@ -18,14 +18,14 @@ "actions": [ { "uid": "jsr223.ScriptedAction", - "label": "Scripted action", - "description": "allows the execution of a method defined by a script", + "label": "Opaque Action", + "description": "Executes external code. See the rule source for details.", "visibility": "EXPERT", "configDescriptions": [ { "name": "privId", "type": "TEXT", - "description": "the identifier of the private method", + "description": "The identifier of the private method", "required": true } ], @@ -33,8 +33,8 @@ { "name": "result", "type": "java.lang.Object", - "label": "result", - "description": "the script result.", + "label": "Result", + "description": "The script result", "reference": "" } ] @@ -43,14 +43,14 @@ "triggers": [ { "uid": "jsr223.ScriptedTrigger", - "label": "Scripted trigger", - "description": "allows the execution of a method defined by a script", + "label": "Opaque Trigger", + "description": "A trigger controlled by external code. See the rule source for details.", "visibility": "EXPERT", "configDescriptions": [ { "name": "privId", "type": "TEXT", - "description": "the identifier of the private method", + "description": "The identifier of the private method", "required": true } ], @@ -58,8 +58,8 @@ { "name": "triggerOutput", "type": "java.lang.String", - "label": "TriggerOutput label", - "description": "Text from the trigger", + "label": "Trigger Output", + "description": "The text from the trigger", "reference": "consoleInput", "defaultValue": "dtag" } diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/resources/OH-INF/i18n/jsr223.properties b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/resources/OH-INF/i18n/jsr223.properties index f6b3f78500..452f55cffa 100644 --- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/resources/OH-INF/i18n/jsr223.properties +++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/resources/OH-INF/i18n/jsr223.properties @@ -1,21 +1,21 @@ # jsr223.ScriptedAction -module-type.jsr223.ScriptedAction.label = Scripted action -module-type.jsr223.ScriptedAction.description = allows the execution of a method defined by a script -module-type.jsr223.ScriptedAction.config.privId.description = the identifier of the private method -module-type.jsr223.ScriptedAction.output.result.label = result -module-type.jsr223.ScriptedAction.output.result.description = the script result. +module-type.jsr223.ScriptedAction.label = Opaque Action +module-type.jsr223.ScriptedAction.description = Executes external code. See the rule source for details. +module-type.jsr223.ScriptedAction.config.privId.description = The identifier of the private method +module-type.jsr223.ScriptedAction.output.result.label = Result +module-type.jsr223.ScriptedAction.output.result.description = The script result # jsr223.ScriptedCondition -module-type.jsr223.ScriptedCondition.label = Scripted condition -module-type.jsr223.ScriptedCondition.description = allows the definition of a condition by a script -module-type.jsr223.ScriptedCondition.config.privId.description = the identifier of the private method +module-type.jsr223.ScriptedCondition.label = Opaque Condition +module-type.jsr223.ScriptedCondition.description = Evaluates a condition using external code. See the rule source for details. +module-type.jsr223.ScriptedCondition.config.privId.description = The identifier of the private method # jsr223.ScriptedTrigger -module-type.jsr223.ScriptedTrigger.label = Scripted trigger -module-type.jsr223.ScriptedTrigger.description = allows the execution of a method defined by a script -module-type.jsr223.ScriptedTrigger.config.privId.description = the identifier of the private method -module-type.jsr223.ScriptedTrigger.output.triggerOutput.label = TriggerOutput label -module-type.jsr223.ScriptedTrigger.output.triggerOutput.description = Text from the trigger +module-type.jsr223.ScriptedTrigger.label = Opaque Trigger +module-type.jsr223.ScriptedTrigger.description = A trigger controlled by external code. See the rule source for details. +module-type.jsr223.ScriptedTrigger.config.privId.description = The identifier of the private method +module-type.jsr223.ScriptedTrigger.output.triggerOutput.label = Trigger Output +module-type.jsr223.ScriptedTrigger.output.triggerOutput.description = The text from the trigger diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/provider/ScriptModuleTypeProvider.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/provider/ScriptModuleTypeProvider.java index 64ca6b3d33..80ebef9466 100644 --- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/provider/ScriptModuleTypeProvider.java +++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/provider/ScriptModuleTypeProvider.java @@ -84,13 +84,13 @@ public class ScriptModuleTypeProvider extends AbstractProvider imple List outputs = new ArrayList<>(); Output result = new Output("result", "java.lang.Object", "result", "the script result", null, null, null); outputs.add(result); - return new ActionType(ScriptActionHandler.TYPE_ID, getConfigDescriptions(locale), "execute an inline script", - "Allows the execution of a user-defined script.", null, Visibility.VISIBLE, null, outputs); + return new ActionType(ScriptActionHandler.TYPE_ID, getConfigDescriptions(locale), "Execute an inline script", + "Executes a user-defined script", null, Visibility.VISIBLE, null, outputs); } private ModuleType getScriptConditionType(@Nullable Locale locale) { return new ConditionType(ScriptConditionHandler.TYPE_ID, getConfigDescriptions(locale), - "an inline script evaluates to true", "Allows the definition of a condition through a script.", null, + "An inline script evaluates to true", "Allows the definition of a condition through a script", null, Visibility.VISIBLE, null); } @@ -111,11 +111,11 @@ public class ScriptModuleTypeProvider extends AbstractProvider imple } final ConfigDescriptionParameter scriptType = ConfigDescriptionParameterBuilder.create("type", Type.TEXT) .withRequired(true).withMultiple(false).withLabel("Script Type") - .withDescription("the scripting language used").withOptions(parameterOptionsList) + .withDescription("The scripting language used").withOptions(parameterOptionsList) .withLimitToOptions(true).build(); final ConfigDescriptionParameter script = ConfigDescriptionParameterBuilder.create("script", Type.TEXT) .withRequired(true).withReadOnly(false).withMultiple(false).withLabel("Script").withContext("script") - .withDescription("the script to execute").build(); + .withDescription("The script to execute").build(); return List.of(scriptType, script); } diff --git a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ModuleTypeResource.java b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ModuleTypeResource.java index 2494c3ee6d..a4a3d4ad06 100644 --- a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ModuleTypeResource.java +++ b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ModuleTypeResource.java @@ -13,8 +13,10 @@ package org.openhab.core.automation.rest.internal; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; @@ -100,21 +102,43 @@ public class ModuleTypeResource implements RESTResource { public Response getAll( @HeaderParam("Accept-Language") @Parameter(description = "language") @Nullable String language, @QueryParam("tags") @Parameter(description = "tags for filtering") @Nullable String tagList, - @QueryParam("type") @Parameter(description = "filtering by action, condition or trigger") @Nullable String type) { + @QueryParam("type") @Parameter(description = "filtering by action, condition or trigger") @Nullable String type, + @QueryParam("asMap") @Parameter(description = "returns an object of arrays by type instead of a mixed array") @Nullable Boolean asMap) { final Locale locale = localeService.getLocale(language); final String[] tags = tagList != null ? tagList.split(",") : new String[0]; - final List modules = new ArrayList<>(); + Map> modulesMap = null; + List modules = null; + if (asMap == null || !asMap.booleanValue()) { + modules = new ArrayList<>(); + } else { + modulesMap = new LinkedHashMap<>(); + } if (type == null || "trigger".equals(type)) { - modules.addAll(TriggerTypeDTOMapper.map(moduleTypeRegistry.getTriggers(locale, tags))); + if (modules != null) { + modules.addAll(TriggerTypeDTOMapper.map(moduleTypeRegistry.getTriggers(locale, tags))); + } else if (modulesMap != null) { + modulesMap.put("triggers", new ArrayList( + TriggerTypeDTOMapper.map(moduleTypeRegistry.getTriggers(locale, tags)))); + } } if (type == null || "condition".equals(type)) { - modules.addAll(ConditionTypeDTOMapper.map(moduleTypeRegistry.getConditions(locale, tags))); + if (modules != null) { + modules.addAll(ConditionTypeDTOMapper.map(moduleTypeRegistry.getConditions(locale, tags))); + } else if (modulesMap != null) { + modulesMap.put("conditions", new ArrayList( + ConditionTypeDTOMapper.map(moduleTypeRegistry.getConditions(locale, tags)))); + } } if (type == null || "action".equals(type)) { - modules.addAll(ActionTypeDTOMapper.map(moduleTypeRegistry.getActions(locale, tags))); + if (modules != null) { + modules.addAll(ActionTypeDTOMapper.map(moduleTypeRegistry.getActions(locale, tags))); + } else if (modulesMap != null) { + modulesMap.put("actions", new ArrayList( + ActionTypeDTOMapper.map(moduleTypeRegistry.getActions(locale, tags)))); + } } - return Response.ok(modules).build(); + return Response.ok(modules != null ? modules : modulesMap).build(); } @GET diff --git a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java index 840786e44b..518fb41694 100644 --- a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java +++ b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java @@ -212,7 +212,8 @@ public class RuleResource implements RESTResource { Stream rules = ruleRegistry.stream().filter(p) // filter according to Predicates .map(rule -> EnrichedRuleDTOMapper.map(rule, ruleManager, managedRuleProvider)); // map matching rules if (summary != null && summary) { - rules = dtoMapper.limitToFields(rules, "uid,templateUID,name,visibility,description,status,tags,editable"); + rules = dtoMapper.limitToFields(rules, + "uid,templateUID,templateState,name,visibility,description,status,tags,editable"); } return Response.ok(new Stream2JSONInputStream(rules)).build(); @@ -344,12 +345,12 @@ public class RuleResource implements RESTResource { @Consumes(MediaType.TEXT_PLAIN) @Operation(operationId = "enableRule", summary = "Sets the rule enabled status.", responses = { @ApiResponse(responseCode = "200", description = "OK"), - @ApiResponse(responseCode = "404", description = "Rule corresponding to the given UID does not found.") }) + @ApiResponse(responseCode = "404", description = "Rule corresponding to the given UID was not found.") }) public Response enableRule(@PathParam("ruleUID") @Parameter(description = "ruleUID") String ruleUID, @Parameter(description = "enable", required = true) String enabled) throws IOException { Rule rule = ruleRegistry.get(ruleUID); if (rule == null) { - logger.info("Received HTTP PUT request for set enabled at '{}' for the unknown rule '{}'.", + logger.info("Received HTTP POST request for set enabled at '{}' for the unknown rule '{}'.", uriInfo.getPath(), ruleUID); return Response.status(Status.NOT_FOUND).build(); } else { @@ -358,13 +359,32 @@ public class RuleResource implements RESTResource { } } + @POST + @Path("/{ruleUID}/regenerate") + @Consumes(MediaType.TEXT_PLAIN) + @Operation(operationId = "regenerateRule", summary = "Regenerates the rule from its template.", responses = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "404", description = "A template-based rule with the given UID was not found.") }) + public Response regenerateRule(@PathParam("ruleUID") @Parameter(description = "ruleUID") String ruleUID) + throws IOException { + try { + ruleRegistry.regenerateFromTemplate(ruleUID); + return Response.ok(null, MediaType.TEXT_PLAIN).build(); + } catch (IllegalArgumentException e) { + logger.info( + "Received HTTP POST request for regenerating rule from template at '{}' for an invalid rule UID '{}'.", + uriInfo.getPath(), ruleUID); + return Response.status(Status.NOT_FOUND).build(); + } + } + @POST @RolesAllowed({ Role.USER, Role.ADMIN }) @Path("/{ruleUID}/runnow") @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "runRuleNow", summary = "Executes actions of the rule.", responses = { @ApiResponse(responseCode = "200", description = "OK"), - @ApiResponse(responseCode = "404", description = "Rule corresponding to the given UID does not found.") }) + @ApiResponse(responseCode = "404", description = "Rule corresponding to the given UID was not found.") }) public Response runNow(@PathParam("ruleUID") @Parameter(description = "ruleUID") String ruleUID, @Nullable @Parameter(description = "the context for running this rule", allowEmptyValue = true) Map context) throws IOException { diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Rule.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Rule.java index dc5a45dfc2..adcdb1cd5b 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Rule.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Rule.java @@ -13,6 +13,7 @@ package org.openhab.core.automation; import java.util.List; +import java.util.Locale; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -40,6 +41,7 @@ import org.openhab.core.config.core.Configuration; * They can help the user to classify or label the Rules, and to filter and search them. * * @author Kai Kreuzer - Initial contribution + * @author Ravi Nadahar - Added TemplateState */ @NonNullByDefault public interface Rule extends Identifiable { @@ -66,6 +68,17 @@ public interface Rule extends Identifiable { @Nullable String getTemplateUID(); + /** + * This method is used to track the template processing state by the {@link RuleRegistry}. The default + * implementation doesn't support templates and must be overridden if the {@link Rule} implementation + * supports templates. + * + * @return the current template processing state. + */ + default TemplateState getTemplateState() { + return TemplateState.NO_TEMPLATE; + } + /** * This method is used to obtain the {@link Rule}'s human-readable name. * @@ -154,4 +167,61 @@ public interface Rule extends Identifiable { } return null; } + + /** + * This enum represent the different states a rule can have in respect to rule templates. + */ + public enum TemplateState { + + /** This {@link Rule} isn't associated with a template */ + NO_TEMPLATE, + + /** This {@link Rule} is associated with a template and it has yet to be instantiated */ + PENDING, + + /** This {@link Rule} is associated with a template that wasn't found */ + TEMPLATE_MISSING, + + /** This {@link Rule} is associated with a template and has been instantiated */ + INSTANTIATED; + + @Override + public String toString() { + switch (this) { + case INSTANTIATED: + return "instantiated"; + case PENDING: + return "pending"; + case TEMPLATE_MISSING: + return "template-missing"; + case NO_TEMPLATE: + default: + return "no-template"; + } + } + + /** + * Returns the {@link TemplateState} that best represents the specified string. If no match is found, + * {@link TemplateState#NO_TEMPLATE} is returned. + * + * @param templateState the string to convert. + * @return The resulting {@link TemplateState}. + */ + public static TemplateState typeOf(@Nullable String templateState) { + if (templateState == null) { + return NO_TEMPLATE; + } + String s = templateState.trim().toLowerCase(Locale.ROOT); + switch (s) { + case "instantiated": + return INSTANTIATED; + case "pending": + return PENDING; + case "template-missing": + return TEMPLATE_MISSING; + default: + return NO_TEMPLATE; + } + } + } } diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleRegistry.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleRegistry.java index 5370302c95..f3d8fa71b4 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleRegistry.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleRegistry.java @@ -85,4 +85,13 @@ public interface RuleRegistry extends Registry { * @return collection of {@link Rule}s having specified tags. */ Collection getByTags(String... tags); + + /** + * This method triggers a new generation of the rule from its template by reverting the rule to its + * "rule stub" state only containing the template configuration. + * + * @param ruleUID the UID of the {@link Rule}. + * @throws IllegalArgumentException if the rule doesn't exist or isn't linked to a template. + */ + void regenerateFromTemplate(String ruleUID); } diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleStatusDetail.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleStatusDetail.java index f912c37c12..3e378d5494 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleStatusDetail.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleStatusDetail.java @@ -64,6 +64,13 @@ import org.eclipse.jdt.annotation.NonNullByDefault; * N/A * * + * {@link #TEMPLATE_PENDING} + * Template processing pending + * Template processing pending + * Template processing pending + * N/A + * + * * {@link #INVALID_RULE} * Resolving failed * N/A @@ -82,6 +89,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; * @author Yordan Mihaylov - Initial contribution * @author Kai Kreuzer - Refactored to match ThingStatusDetail implementation * @author Ana Dimova - add java doc + * @author Ravi Nadahar - added {@link #TEMPLATE_PENDING} */ @NonNullByDefault public enum RuleStatusDetail { @@ -90,8 +98,9 @@ public enum RuleStatusDetail { HANDLER_INITIALIZING_ERROR(2), CONFIGURATION_ERROR(3), TEMPLATE_MISSING_ERROR(4), - INVALID_RULE(5), - DISABLED(6); + TEMPLATE_PENDING(5), + INVALID_RULE(6), + DISABLED(7); private final int value; diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleDTO.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleDTO.java index 594341ecf3..86c6fb36b6 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleDTO.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleDTO.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.eclipse.jdt.annotation.NonNull; import org.openhab.core.automation.Visibility; import org.openhab.core.config.core.dto.ConfigDescriptionParameterDTO; @@ -26,15 +27,16 @@ import org.openhab.core.config.core.dto.ConfigDescriptionParameterDTO; */ public class RuleDTO { - public List triggers; - public List conditions; - public List actions; - public Map configuration; - public List configDescriptions; + public List<@NonNull TriggerDTO> triggers; + public List<@NonNull ConditionDTO> conditions; + public List<@NonNull ActionDTO> actions; + public Map<@NonNull String, @NonNull Object> configuration; + public List<@NonNull ConfigDescriptionParameterDTO> configDescriptions; public String templateUID; + public String templateState; public String uid; public String name; - public Set tags; + public Set<@NonNull String> tags; public Visibility visibility; public String description; } diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleDTOMapper.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleDTOMapper.java index 56f591ab82..cced227070 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleDTOMapper.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleDTOMapper.java @@ -14,6 +14,7 @@ package org.openhab.core.automation.dto; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.automation.Rule; +import org.openhab.core.automation.Rule.TemplateState; import org.openhab.core.automation.util.RuleBuilder; import org.openhab.core.config.core.Configuration; import org.openhab.core.config.core.dto.ConfigDescriptionDTOMapper; @@ -34,13 +35,19 @@ public class RuleDTOMapper { } public static Rule map(final RuleDTO ruleDto) { - return RuleBuilder.create(ruleDto.uid).withActions(ActionDTOMapper.mapDto(ruleDto.actions)) + RuleBuilder builder = RuleBuilder.create(ruleDto.uid).withActions(ActionDTOMapper.mapDto(ruleDto.actions)) .withConditions(ConditionDTOMapper.mapDto(ruleDto.conditions)) .withTriggers(TriggerDTOMapper.mapDto(ruleDto.triggers)) .withConfiguration(new Configuration(ruleDto.configuration)) .withConfigurationDescriptions(ConfigDescriptionDTOMapper.map(ruleDto.configDescriptions)) .withTemplateUID(ruleDto.templateUID).withVisibility(ruleDto.visibility).withTags(ruleDto.tags) - .withName(ruleDto.name).withDescription(ruleDto.description).build(); + .withName(ruleDto.name).withDescription(ruleDto.description); + if (ruleDto.templateState == null) { + builder.withTemplateState(ruleDto.templateUID == null ? TemplateState.NO_TEMPLATE : TemplateState.PENDING); + } else { + builder.withTemplateState(TemplateState.typeOf(ruleDto.templateState)); + } + return builder.build(); } protected static void fillProperties(final Rule from, final RuleDTO to) { @@ -50,6 +57,7 @@ public class RuleDTOMapper { to.configuration = from.getConfiguration().getProperties(); to.configDescriptions = ConfigDescriptionDTOMapper.mapParameters(from.getConfigurationDescriptions()); to.templateUID = from.getTemplateUID(); + to.templateState = from.getTemplateState().toString(); to.uid = from.getUID(); to.name = from.getName(); to.tags = from.getTags(); 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 f08083150b..f8c171becf 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 @@ -39,6 +39,7 @@ import org.openhab.core.automation.Condition; import org.openhab.core.automation.Module; import org.openhab.core.automation.ModuleHandlerCallback; import org.openhab.core.automation.Rule; +import org.openhab.core.automation.Rule.TemplateState; import org.openhab.core.automation.RuleExecution; import org.openhab.core.automation.RuleManager; import org.openhab.core.automation.RuleRegistry; @@ -838,8 +839,17 @@ public class RuleEngineImpl implements RuleManager, RegistryChangeListener { - ruleRegistry.getAll().stream() // + ruleRegistry.stream() // .filter(r -> isEnabled(r.getUID())) // .forEach(r -> compileRule(r.getUID())); executeRulesWithStartLevel(); @@ -1571,7 +1581,7 @@ public class RuleEngineImpl implements RuleManager, RegistryChangeListener { - ruleRegistry.getAll().stream() // + ruleRegistry.stream() // .filter(this::mustTrigger) // .forEach(r -> runNow(r.getUID(), true, Map.of(SystemTriggerHandler.OUT_STARTLEVEL, StartLevelService.STARTLEVEL_RULES, "event", diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleImpl.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleImpl.java index 3858cdb0c1..3bb6a534f6 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleImpl.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleImpl.java @@ -15,6 +15,7 @@ package org.openhab.core.automation.internal; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -48,6 +49,7 @@ public class RuleImpl implements Rule { protected Configuration configuration; protected List configDescriptions; protected @Nullable String templateUID; + protected TemplateState templateStatus; protected String uid; protected @Nullable String name; protected Set tags; @@ -62,7 +64,7 @@ public class RuleImpl implements Rule { * @param uid the rule's identifier, or {@code null} if a random identifier should be generated. */ public RuleImpl(@Nullable String uid) { - this(uid, null, null, null, null, null, null, null, null, null, null); + this(uid, null, null, null, null, null, null, null, null, null, TemplateState.NO_TEMPLATE, null); } /** @@ -90,7 +92,8 @@ public class RuleImpl implements Rule { public RuleImpl(@Nullable String uid, final @Nullable String name, final @Nullable String description, final @Nullable Set tags, @Nullable List triggers, @Nullable List conditions, @Nullable List actions, @Nullable List configDescriptions, - @Nullable Configuration configuration, @Nullable String templateUID, @Nullable Visibility visibility) { + @Nullable Configuration configuration, @Nullable String templateUID, TemplateState templateStatus, + @Nullable Visibility visibility) { this.uid = uid == null ? UUID.randomUUID().toString() : uid; this.name = name; this.description = description; @@ -102,6 +105,7 @@ public class RuleImpl implements Rule { this.configuration = configuration == null ? new Configuration() : new Configuration(configuration.getProperties()); this.templateUID = templateUID; + this.templateStatus = templateStatus; this.visibility = visibility == null ? Visibility.VISIBLE : visibility; } @@ -124,6 +128,20 @@ public class RuleImpl implements Rule { this.templateUID = templateUID; } + @Override + public TemplateState getTemplateState() { + return templateStatus; + } + + /** + * This method is used to specify the current rule template state. + * + * @param templateState the {@link TemplateState} to set. + */ + public void setTemplateStatus(TemplateState templateState) { + this.templateStatus = Objects.requireNonNull(templateState); + } + @Override public @Nullable String getName() { return name; diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleRegistryImpl.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleRegistryImpl.java index 7d26dd4a84..f808c22cc2 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleRegistryImpl.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleRegistryImpl.java @@ -20,18 +20,21 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.Action; +import org.openhab.core.automation.Condition; import org.openhab.core.automation.ManagedRuleProvider; import org.openhab.core.automation.Module; import org.openhab.core.automation.Rule; +import org.openhab.core.automation.Rule.TemplateState; import org.openhab.core.automation.RuleProvider; import org.openhab.core.automation.RuleRegistry; import org.openhab.core.automation.RuleStatus; import org.openhab.core.automation.RuleStatusInfo; +import org.openhab.core.automation.Trigger; import org.openhab.core.automation.internal.template.RuleTemplateRegistry; import org.openhab.core.automation.template.RuleTemplate; import org.openhab.core.automation.template.TemplateRegistry; @@ -101,6 +104,7 @@ import org.slf4j.LoggerFactory; * @author Kai Kreuzer - refactored (managed) provider and registry implementation and other fixes * @author Benedikt Niehues - added events for rules * @author Victor Toni - return only copies of {@link Rule}s + * @author Ravi Nadahar - added support for regenerating {@link Rule}s from {@link RuleTemplate}s. */ @NonNullByDefault @Component(service = RuleRegistry.class, immediate = true) @@ -108,17 +112,14 @@ public class RuleRegistryImpl extends AbstractRegistry { private static final String SOURCE = RuleRegistryImpl.class.getSimpleName(); + private static final Set PROCESSABLE_TEMPLATE_STATES = Set.of(TemplateState.PENDING, + TemplateState.TEMPLATE_MISSING); private final Logger logger = LoggerFactory.getLogger(RuleRegistryImpl.class.getName()); private @NonNullByDefault({}) ModuleTypeRegistry moduleTypeRegistry; private @NonNullByDefault({}) RuleTemplateRegistry templateRegistry; - /** - * {@link Map} of template UIDs to rules where these templates participated. - */ - private final Map> mapTemplateToRules = new HashMap<>(); - /** * Constructor that is responsible to invoke the super constructor with appropriate providerClazz * {@link RuleProvider} - the class of the providers that should be tracked automatically after activation. @@ -290,15 +291,6 @@ public class RuleRegistryImpl extends AbstractRegistry) null).withConditions((List) null) + .withTriggers((List) null).withTemplateState(TemplateState.PENDING).build()); + Provider provider = getProvider(rule.getUID()); + if (provider == null) { + logger.error("Regenerating rule '{}' from template failed because the provider is unknown", + rule.getUID()); + return; + } + if (provider instanceof ManagedRuleProvider) { + update(resolvedRule); + } else { + updated(provider, rule, resolvedRule); + } + if (resolvedRule.getTemplateState() == TemplateState.TEMPLATE_MISSING) { + logger.warn("Failed to regenerate rule '{}' from template since the template is missing", + rule.getUID()); + } else { + logger.info("Rule '{}' was regenerated from template '{}'", rule.getUID(), rule.getTemplateUID()); + } + } catch (IllegalArgumentException e) { + logger.error("Regenerating rule '{}' from template failed: {}", rule.getUID(), e.getMessage(), e); + throw e; + } + } + /** * The method checks if the rule has to be resolved by template or not. If the rule does not contain tempateUID it * returns same rule, otherwise it tries to resolve the rule created from template. If the template is available @@ -347,46 +377,30 @@ public class RuleRegistryImpl extends AbstractRegistry ruleUIDs = Objects - .requireNonNull(mapTemplateToRules.computeIfAbsent(templateUID, k -> new HashSet<>())); - if (resolved) { - ruleUIDs.remove(ruleUID); - } else { - ruleUIDs.add(ruleUID); - } - } - } - @Override protected void addProvider(Provider provider) { super.addProvider(provider); @@ -474,7 +488,8 @@ public class RuleRegistryImpl extends AbstractRegistry configurationProperties = configuration.getProperties(); - if (rule.getTemplateUID() == null) { + TemplateState templateState = rule.getTemplateState(); + if (templateState == TemplateState.INSTANTIATED || templateState == TemplateState.NO_TEMPLATE) { String uid = rule.getUID(); try { validateConfiguration(configDescriptions, new HashMap<>(configurationProperties)); @@ -622,38 +637,7 @@ public class RuleRegistryImpl extends AbstractRegistry rules = new HashSet<>(); - synchronized (this) { - Set rulesForResolving = mapTemplateToRules.get(templateUID); - if (rulesForResolving != null) { - rules.addAll(rulesForResolving); - } - } - for (String rUID : rules) { - try { - Rule unresolvedRule = get(rUID); - if (unresolvedRule != null) { - Rule resolvedRule = resolveRuleByTemplate(unresolvedRule); - Provider provider = getProvider(rUID); - if (provider instanceof ManagedRuleProvider) { - update(resolvedRule); - } else if (provider != null) { - updated(provider, unresolvedRule, unresolvedRule); - } else { - logger.error( - "Resolving the rule '{}' by template '{}' failed because the provider is not known", - rUID, templateUID); - } - } else { - logger.error( - "Resolving the rule '{}' by template '{}' failed because it is not known to the registry", - rUID, templateUID); - } - } catch (IllegalArgumentException e) { - logger.error("Resolving the rule '{}' by template '{}' failed", rUID, templateUID, e); - } - } + processRuleStubs(element); } @Override @@ -663,6 +647,34 @@ public class RuleRegistryImpl extends AbstractRegistry rules = stream().filter((r) -> PROCESSABLE_TEMPLATE_STATES.contains(r.getTemplateState())).toList(); + for (Rule unresolvedRule : rules) { + try { + Rule resolvedRule = resolveRuleByTemplate(unresolvedRule); + Provider provider = getProvider(unresolvedRule.getUID()); + if (provider instanceof ManagedRuleProvider) { + update(resolvedRule); + } else if (provider != null) { + updated(provider, unresolvedRule, resolvedRule); + } else { + logger.error("Resolving the rule '{}' by template '{}' failed because the provider is not known", + unresolvedRule.getUID(), templateUID); + } + } catch (IllegalArgumentException e) { + logger.error("Resolving the rule '{}' by template '{}' failed", unresolvedRule.getUID(), templateUID, + e); + } + } } } diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/RuleBuilder.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/RuleBuilder.java index 68032098a2..dc3d039caa 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/RuleBuilder.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/RuleBuilder.java @@ -24,6 +24,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.automation.Action; import org.openhab.core.automation.Condition; import org.openhab.core.automation.Rule; +import org.openhab.core.automation.Rule.TemplateState; import org.openhab.core.automation.Trigger; import org.openhab.core.automation.Visibility; import org.openhab.core.automation.internal.RuleImpl; @@ -45,6 +46,7 @@ public class RuleBuilder { private Configuration configuration; private List configDescriptions; private @Nullable String templateUID; + private TemplateState templateState; private final String uid; private @Nullable String name; private Set tags; @@ -58,6 +60,7 @@ public class RuleBuilder { this.configuration = new Configuration(rule.getConfiguration()); this.configDescriptions = new LinkedList<>(rule.getConfigurationDescriptions()); this.templateUID = rule.getTemplateUID(); + this.templateState = TemplateState.NO_TEMPLATE; this.uid = rule.getUID(); this.name = rule.getName(); this.tags = new HashSet<>(rule.getTags()); @@ -74,7 +77,8 @@ public class RuleBuilder { return create(r.getUID()).withActions(r.getActions()).withConditions(r.getConditions()) .withTriggers(r.getTriggers()).withConfiguration(r.getConfiguration()) .withConfigurationDescriptions(r.getConfigurationDescriptions()).withDescription(r.getDescription()) - .withName(r.getName()).withTags(r.getTags()); + .withName(r.getName()).withTags(r.getTags()).withTemplateUID(r.getTemplateUID()) + .withTemplateState(r.getTemplateState()); } public static RuleBuilder create(RuleTemplate template, String uid, @Nullable String name, @@ -82,7 +86,8 @@ public class RuleBuilder { return create(uid).withActions(template.getActions()).withConditions(template.getConditions()) .withTriggers(template.getTriggers()).withConfiguration(configuration) .withConfigurationDescriptions(template.getConfigurationDescriptions()) - .withDescription(template.getDescription()).withName(name).withTags(template.getTags()); + .withDescription(template.getDescription()).withName(name).withTags(template.getTags()) + .withTemplateState(TemplateState.INSTANTIATED).withTemplateUID(template.getUID()); } public RuleBuilder withName(@Nullable String name) { @@ -100,6 +105,11 @@ public class RuleBuilder { return this; } + public RuleBuilder withTemplateState(TemplateState templateState) { + this.templateState = templateState; + return this; + } + public RuleBuilder withVisibility(@Nullable Visibility visibility) { this.visibility = visibility; return this; @@ -166,6 +176,6 @@ public class RuleBuilder { public Rule build() { return new RuleImpl(uid, name, description, tags, triggers, conditions, actions, configDescriptions, - configuration, templateUID, visibility); + configuration, templateUID, templateState, visibility); } } diff --git a/itests/org.openhab.core.automation.integration.tests/src/main/java/org/openhab/core/automation/integration/test/AutomationIntegrationTest.java b/itests/org.openhab.core.automation.integration.tests/src/main/java/org/openhab/core/automation/integration/test/AutomationIntegrationTest.java index 35e1be3e89..48387eb692 100644 --- a/itests/org.openhab.core.automation.integration.tests/src/main/java/org/openhab/core/automation/integration/test/AutomationIntegrationTest.java +++ b/itests/org.openhab.core.automation.integration.tests/src/main/java/org/openhab/core/automation/integration/test/AutomationIntegrationTest.java @@ -38,6 +38,7 @@ import org.openhab.core.automation.Action; import org.openhab.core.automation.Condition; import org.openhab.core.automation.ManagedRuleProvider; import org.openhab.core.automation.Rule; +import org.openhab.core.automation.Rule.TemplateState; import org.openhab.core.automation.RuleManager; import org.openhab.core.automation.RuleProvider; import org.openhab.core.automation.RuleRegistry; @@ -678,7 +679,7 @@ public class AutomationIntegrationTest extends JavaOSGiTest { configs.put("updateItem", "templ_LampItem"); configs.put("updateCommand", "ON"); Rule templateRule = RuleBuilder.create("templateRuleUID").withTemplateUID("SimpleTestTemplate") - .withConfiguration(new Configuration(configs)).build(); + .withTemplateState(TemplateState.PENDING).withConfiguration(new Configuration(configs)).build(); ruleRegistry.add(templateRule); assertThat(ruleRegistry.get(templateRule.getUID()), is(notNullValue())); @@ -727,7 +728,7 @@ public class AutomationIntegrationTest extends JavaOSGiTest { configs.put("updateCommand", "ON"); Configuration config = new Configuration(configs); Rule templateRule = RuleBuilder.create("xtemplateRuleUID").withTemplateUID("TestTemplateWithCompositeModules") - .withConfiguration(config).build(); + .withTemplateState(TemplateState.PENDING).withConfiguration(config).build(); ruleRegistry.add(templateRule); assertThat(ruleRegistry.get(templateRule.getUID()), is(notNullValue()));