Regenerate rules from templates (#4718)

* Rule template regeneration support

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>
pull/4829/head
Nadahar 2025-05-26 22:53:17 +02:00 committed by GitHub
parent fb62bf33cd
commit 5b12280f5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 332 additions and 134 deletions

View File

@ -118,4 +118,9 @@ public class RuleSupportRuleRegistryDelegate implements RuleRegistry {
public Collection<Rule> getByTags(String... tags) {
return ruleRegistry.getByTags(tags);
}
@Override
public void regenerateFromTemplate(String ruleUID) {
ruleRegistry.regenerateFromTemplate(ruleUID);
}
}

View File

@ -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"
}

View File

@ -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

View File

@ -84,13 +84,13 @@ public class ScriptModuleTypeProvider extends AbstractProvider<ModuleType> imple
List<Output> 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<ModuleType> 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);
}

View File

@ -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<ModuleTypeDTO> modules = new ArrayList<>();
Map<String, List<ModuleTypeDTO>> modulesMap = null;
List<ModuleTypeDTO> modules = null;
if (asMap == null || !asMap.booleanValue()) {
modules = new ArrayList<>();
} else {
modulesMap = new LinkedHashMap<>();
}
if (type == null || "trigger".equals(type)) {
if (modules != null) {
modules.addAll(TriggerTypeDTOMapper.map(moduleTypeRegistry.getTriggers(locale, tags)));
} else if (modulesMap != null) {
modulesMap.put("triggers", new ArrayList<ModuleTypeDTO>(
TriggerTypeDTOMapper.map(moduleTypeRegistry.getTriggers(locale, tags))));
}
}
if (type == null || "condition".equals(type)) {
if (modules != null) {
modules.addAll(ConditionTypeDTOMapper.map(moduleTypeRegistry.getConditions(locale, tags)));
} else if (modulesMap != null) {
modulesMap.put("conditions", new ArrayList<ModuleTypeDTO>(
ConditionTypeDTOMapper.map(moduleTypeRegistry.getConditions(locale, tags))));
}
}
if (type == null || "action".equals(type)) {
if (modules != null) {
modules.addAll(ActionTypeDTOMapper.map(moduleTypeRegistry.getActions(locale, tags)));
} else if (modulesMap != null) {
modulesMap.put("actions", new ArrayList<ModuleTypeDTO>(
ActionTypeDTOMapper.map(moduleTypeRegistry.getActions(locale, tags))));
}
return Response.ok(modules).build();
}
return Response.ok(modules != null ? modules : modulesMap).build();
}
@GET

View File

@ -212,7 +212,8 @@ public class RuleResource implements RESTResource {
Stream<EnrichedRuleDTO> 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<String, Object> context)
throws IOException {

View File

@ -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<String> {
@ -66,6 +68,17 @@ public interface Rule extends Identifiable<String> {
@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<String> {
}
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;
}
}
}
}

View File

@ -85,4 +85,13 @@ public interface RuleRegistry extends Registry<Rule, String> {
* @return collection of {@link Rule}s having specified tags.
*/
Collection<Rule> 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);
}

View File

@ -64,6 +64,13 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
* <td><b>N/A</b></td>
* </tr>
* <tr>
* <td><b>{@link #TEMPLATE_PENDING}</b></td>
* <td>Template processing pending</td>
* <td>Template processing pending</td>
* <td>Template processing pending</td>
* <td><b>N/A</b></td>
* </tr>
* <tr>
* <td><b>{@link #INVALID_RULE}</b></td>
* <td>Resolving failed</td>
* <td><b>N/A</b></td>
@ -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;

View File

@ -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<TriggerDTO> triggers;
public List<ConditionDTO> conditions;
public List<ActionDTO> actions;
public Map<String, Object> configuration;
public List<ConfigDescriptionParameterDTO> 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<String> tags;
public Set<@NonNull String> tags;
public Visibility visibility;
public String description;
}

View File

@ -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();

View File

@ -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<Modul
return false;
}
// Set the module handlers and so check if all handlers are available.
final String ruleUID = rule.getUID();
TemplateState templateState = rule.unwrap().getTemplateState();
if (templateState == TemplateState.TEMPLATE_MISSING || templateState == TemplateState.PENDING) {
setStatus(ruleUID,
new RuleStatusInfo(RuleStatus.UNINITIALIZED,
templateState == TemplateState.TEMPLATE_MISSING ? RuleStatusDetail.TEMPLATE_MISSING_ERROR
: RuleStatusDetail.TEMPLATE_PENDING));
return false;
}
// Set the module handlers and so check if all handlers are available.
final String errMsgs = setModuleHandlers(ruleUID, rule.getModules());
if (errMsgs != null) {
setStatus(ruleUID,
@ -1562,7 +1572,7 @@ public class RuleEngineImpl implements RuleManager, RegistryChangeListener<Modul
*/
private void compileRules() {
getScheduledExecutor().submit(() -> {
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<Modul
private void executeRulesWithStartLevel() {
getScheduledExecutor().submit(() -> {
ruleRegistry.getAll().stream() //
ruleRegistry.stream() //
.filter(this::mustTrigger) //
.forEach(r -> runNow(r.getUID(), true,
Map.of(SystemTriggerHandler.OUT_STARTLEVEL, StartLevelService.STARTLEVEL_RULES, "event",

View File

@ -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<ConfigDescriptionParameter> configDescriptions;
protected @Nullable String templateUID;
protected TemplateState templateStatus;
protected String uid;
protected @Nullable String name;
protected Set<String> 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<String> tags, @Nullable List<Trigger> triggers, @Nullable List<Condition> conditions,
@Nullable List<Action> actions, @Nullable List<ConfigDescriptionParameter> 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;

View File

@ -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<Rule, String, RuleProvide
implements RuleRegistry, RegistryChangeListener<RuleTemplate> {
private static final String SOURCE = RuleRegistryImpl.class.getSimpleName();
private static final Set<TemplateState> 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<String, Set<String>> 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<Rule, String, RuleProvide
postEvent(RuleEventFactory.createRuleStatusInfoEvent(statusInfo, ruleUID, SOURCE));
}
@Override
protected void onRemoveElement(Rule rule) {
String uid = rule.getUID();
String templateUID = rule.getTemplateUID();
if (templateUID != null) {
updateRuleTemplateMapping(templateUID, uid, true);
}
}
@Override
protected void notifyListenersAboutRemovedElement(Rule element) {
super.notifyListenersAboutRemovedElement(element);
@ -336,6 +328,44 @@ public class RuleRegistryImpl extends AbstractRegistry<Rule, String, RuleProvide
return result;
}
@Override
public void regenerateFromTemplate(String ruleUID) {
Rule rule = get(ruleUID);
if (rule == null) {
throw new IllegalArgumentException(
"Can't regenerate rule from template because no rule with UID \"" + ruleUID + "\" exists");
}
if (rule.getTemplateUID() == null || rule.getTemplateState() == TemplateState.NO_TEMPLATE) {
throw new IllegalArgumentException(
"Can't regenerate rule from template because the rule isn't linked to a template");
}
try {
Rule resolvedRule = resolveRuleByTemplate(
RuleBuilder.create(rule).withActions((List<Action>) null).withConditions((List<Condition>) null)
.withTriggers((List<Trigger>) null).withTemplateState(TemplateState.PENDING).build());
Provider<Rule> 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<Rule, String, RuleProvide
* missing.
*/
private Rule resolveRuleByTemplate(Rule rule) {
TemplateState templateState = rule.getTemplateState();
if (templateState == TemplateState.NO_TEMPLATE || templateState == TemplateState.INSTANTIATED) {
return rule;
}
String templateUID = rule.getTemplateUID();
if (templateUID == null) {
return rule;
}
RuleTemplate template = templateRegistry.get(templateUID);
String uid = rule.getUID();
if (template == null) {
updateRuleTemplateMapping(templateUID, uid, false);
logger.debug("Rule template {} does not exist.", templateUID);
if (templateState == TemplateState.TEMPLATE_MISSING) {
return rule;
}
logger.debug("Rule template {} does not exist.", templateUID);
return RuleBuilder.create(rule).withTemplateState(TemplateState.TEMPLATE_MISSING).build();
} else {
RuleImpl resolvedRule = (RuleImpl) RuleBuilder
.create(template, rule.getUID(), rule.getName(), rule.getConfiguration(), rule.getVisibility())
.build();
resolveConfigurations(resolvedRule);
updateRuleTemplateMapping(templateUID, uid, true);
return resolvedRule;
}
}
/**
* Updates the content of the {@link Map} that maps the template to rules, using it to complete their definitions.
*
* @param templateUID the {@link RuleTemplate}'s UID specifying the template.
* @param ruleUID the {@link Rule}'s UID specifying a rule created by the specified template.
* @param resolved specifies if the {@link Map} should be updated by adding or removing the specified rule
* accordingly if the rule is resolved or not.
*/
private void updateRuleTemplateMapping(String templateUID, String ruleUID, boolean resolved) {
synchronized (this) {
Set<String> ruleUIDs = Objects
.requireNonNull(mapTemplateToRules.computeIfAbsent(templateUID, k -> new HashSet<>()));
if (resolved) {
ruleUIDs.remove(ruleUID);
} else {
ruleUIDs.add(ruleUID);
}
}
}
@Override
protected void addProvider(Provider<Rule> provider) {
super.addProvider(provider);
@ -474,7 +488,8 @@ public class RuleRegistryImpl extends AbstractRegistry<Rule, String, RuleProvide
ConfigurationNormalizer.normalizeConfiguration(configuration,
ConfigurationNormalizer.getConfigDescriptionMap(configDescriptions));
Map<String, Object> 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<Rule, String, RuleProvide
@Override
public void added(RuleTemplate element) {
String templateUID = element.getUID();
Set<String> rules = new HashSet<>();
synchronized (this) {
Set<String> 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<Rule> 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<Rule, String, RuleProvide
@Override
public void updated(RuleTemplate oldElement, RuleTemplate element) {
// Do nothing - resolved rules are independent from templates
processRuleStubs(element);
}
/**
* Processes any existing rule stubs (rules with a template specified that haven't yet been converted into "proper
* rules") that references the specified rule template using the new or updated rule template.
*
* @param template the {@link RuleTemplate} to use for processing matching rule stubs.
*/
protected void processRuleStubs(RuleTemplate template) {
String templateUID = template.getUID();
List<Rule> rules = stream().filter((r) -> PROCESSABLE_TEMPLATE_STATES.contains(r.getTemplateState())).toList();
for (Rule unresolvedRule : rules) {
try {
Rule resolvedRule = resolveRuleByTemplate(unresolvedRule);
Provider<Rule> 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);
}
}
}
}

View File

@ -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<ConfigDescriptionParameter> configDescriptions;
private @Nullable String templateUID;
private TemplateState templateState;
private final String uid;
private @Nullable String name;
private Set<String> 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);
}
}

View File

@ -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()));