From 6ad019297549caadd246d8c4894bc887ba7a1d63 Mon Sep 17 00:00:00 2001 From: Nadahar Date: Fri, 12 Jun 2026 19:49:35 +0200 Subject: [PATCH] Rule and RuleTemplate file format conversion (#5572) * Create ObjectParser and ObjectSerializer interfaces for rules and rule templates Signed-off-by: Ravi Nadahar * Embed DSL rule source with the Rule Signed-off-by: Ravi Nadahar * Detect shared context DSL rules Signed-off-by: Ravi Nadahar * Update YAML DTOs to support serialization Signed-off-by: Ravi Nadahar * Create rule converters Signed-off-by: Ravi Nadahar * Create rule template YAML converter Signed-off-by: Ravi Nadahar * Implement rule and rule template conversion in the REST API Signed-off-by: Ravi Nadahar * Handle DSL conditions Signed-off-by: Ravi Nadahar * Create check serializability endpoint Signed-off-by: Ravi Nadahar * Don't deserialize TemplateState - deduce it from templateUID Signed-off-by: Ravi Nadahar * Add GenericEventTrigger to ModuleTypeAliases Signed-off-by: Ravi Nadahar * Fix JavaDoc errors Signed-off-by: Ravi Nadahar * Allow triggerless DSL rules Signed-off-by: Ravi Nadahar * Fix negative indentation issue during DSL formatting Signed-off-by: Ravi Nadahar * Add rule configuration to "rule summary" Signed-off-by: Ravi Nadahar * Fix rule template REST API schema Signed-off-by: Ravi Nadahar * Address review feedback Signed-off-by: Ravi Nadahar * Address review feedback Signed-off-by: Ravi Nadahar * Handle single digit hour specification during DSL serialization Signed-off-by: Ravi Nadahar * Address review comments Signed-off-by: Ravi Nadahar * Address review comments Signed-off-by: Ravi Nadahar * Apply review suggestion Signed-off-by: Ravi Nadahar * Remove seemingly needless import Signed-off-by: Ravi Nadahar * Apply review derived fixes Signed-off-by: Ravi Nadahar * Review fixes Signed-off-by: Ravi Nadahar * Address review formatter feedback Signed-off-by: Ravi Nadahar * Fix YAML bundle feature dependency Signed-off-by: Ravi Nadahar --------- Signed-off-by: Ravi Nadahar Co-authored-by: Ravi Nadahar --- .../rest/internal/RuleResource.java | 2 +- .../rest/internal/TemplateResource.java | 10 +- .../org/openhab/core/automation/Module.java | 7 + .../org/openhab/core/automation/Rule.java | 14 + .../core/automation/converter/RuleParser.java | 51 ++ .../automation/converter/RuleSerializer.java | 97 ++ .../converter/RuleTemplateParser.java | 51 ++ .../converter/RuleTemplateSerializer.java | 115 +++ .../core/automation/util/RuleBuilder.java | 10 +- bundles/org.openhab.core.io.rest.core/pom.xml | 5 + .../rest/core/fileformat/FileFormatDTO.java | 8 +- .../fileformat/FileFormatResource.java | 617 ++++++++++++- .../rule/SerializabilityResultsDTO.java | 34 + .../runtime/internal/DSLRuleProvider.java | 46 +- .../internal/converter/DslRuleConverter.java | 854 ++++++++++++++++++ bundles/org.openhab.core.model.rule/bnd.bnd | 1 + .../core/model/rule/RulesRuntimeModule.xtend | 6 + .../RuleFormattingConfigBasedStream.java | 51 ++ .../model/rule/formatting/RulesFormatter.java | 140 +++ .../rule/formatting/RulesFormatter.xtend | 42 - .../config/YamlConfigDescriptionDTO.java | 8 +- .../YamlConfigDescriptionParameterDTO.java | 36 +- ...amlConfigDescriptionParameterGroupDTO.java | 9 +- .../internal/rules/ModuleTypeAliases.java | 2 + .../yaml/internal/rules/YamlActionDTO.java | 26 +- .../yaml/internal/rules/YamlConditionDTO.java | 26 +- .../yaml/internal/rules/YamlModuleDTO.java | 28 +- .../yaml/internal/rules/YamlRuleDTO.java | 97 +- .../internal/rules/YamlRuleTemplateDTO.java | 21 +- .../rules/converter/YamlRuleConverter.java | 163 ++++ .../converter/YamlRuleTemplateConverter.java | 139 +++ .../internal/rules/YamlActionDTOTest.java | 5 +- .../internal/rules/YamlConditionDTOTest.java | 5 +- .../internal/rules/YamlModuleDTOTest.java | 4 +- .../yaml/internal/rules/YamlRuleDTOTest.java | 23 +- .../internal/rules/YamlRuleProviderTest.java | 8 +- .../rules/YamlRuleTemplateDTOTest.java | 17 +- .../core/converter/SerializabilityResult.java | 35 + .../openhab-core/src/main/feature/feature.xml | 1 + 39 files changed, 2651 insertions(+), 163 deletions(-) create mode 100644 bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/converter/RuleParser.java create mode 100644 bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/converter/RuleSerializer.java create mode 100644 bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/converter/RuleTemplateParser.java create mode 100644 bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/converter/RuleTemplateSerializer.java create mode 100644 bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/rule/SerializabilityResultsDTO.java create mode 100644 bundles/org.openhab.core.model.rule.runtime/src/org/openhab/core/model/rule/runtime/internal/converter/DslRuleConverter.java create mode 100644 bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/formatting/RuleFormattingConfigBasedStream.java create mode 100644 bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/formatting/RulesFormatter.java delete mode 100644 bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/formatting/RulesFormatter.xtend create mode 100644 bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/converter/YamlRuleConverter.java create mode 100644 bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/converter/YamlRuleTemplateConverter.java create mode 100644 bundles/org.openhab.core/src/main/java/org/openhab/core/converter/SerializabilityResult.java 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 4a36f382a..fa4414d6f 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 @@ -213,7 +213,7 @@ public class RuleResource implements RESTResource { .map(rule -> EnrichedRuleDTOMapper.map(rule, ruleManager, managedRuleProvider)); // map matching rules if (summary != null && summary) { rules = dtoMapper.limitToFields(rules, - "uid,templateUID,templateState,name,visibility,description,status,tags,editable"); + "uid,templateUID,templateState,name,visibility,description,status,tags,configuration,editable"); } return Response.ok(new Stream2JSONInputStream(rules)).build(); diff --git a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/TemplateResource.java b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/TemplateResource.java index 04673cf1a..df9cf2b1b 100644 --- a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/TemplateResource.java +++ b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/TemplateResource.java @@ -24,13 +24,11 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; -import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.automation.dto.RuleTemplateDTO; import org.openhab.core.automation.dto.RuleTemplateDTOMapper; import org.openhab.core.automation.template.RuleTemplate; -import org.openhab.core.automation.template.Template; import org.openhab.core.automation.template.TemplateRegistry; import org.openhab.core.io.rest.LocaleService; import org.openhab.core.io.rest.RESTConstants; @@ -73,12 +71,12 @@ public class TemplateResource implements RESTResource { public static final String PATH_TEMPLATES = "templates"; private final LocaleService localeService; - private final TemplateRegistry<@NonNull RuleTemplate> templateRegistry; + private final TemplateRegistry templateRegistry; @Activate public TemplateResource( // final @Reference LocaleService localeService, - final @Reference TemplateRegistry<@NonNull RuleTemplate> templateRegistry) { + final @Reference TemplateRegistry templateRegistry) { this.localeService = localeService; this.templateRegistry = templateRegistry; } @@ -86,7 +84,7 @@ public class TemplateResource implements RESTResource { @GET @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getTemplates", summary = "Get all available templates.", responses = { - @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = Template.class)))) }) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = RuleTemplateDTO.class)))) }) public Response getAll( @HeaderParam("Accept-Language") @Parameter(description = "language") @Nullable String language) { Locale locale = localeService.getLocale(language); @@ -99,7 +97,7 @@ public class TemplateResource implements RESTResource { @Path("/{templateUID}") @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getTemplateById", summary = "Gets a template corresponding to the given UID.", responses = { - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = Template.class))), + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RuleTemplateDTO.class))), @ApiResponse(responseCode = "404", description = "Template corresponding to the given UID does not found.") }) public Response getByUID( @HeaderParam("Accept-Language") @Parameter(description = "language") @Nullable String language, diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Module.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Module.java index c42341060..a4bc9e4d1 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Module.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Module.java @@ -77,4 +77,11 @@ public interface Module { * @return the current configuration values of the {@link Module}. */ Configuration getConfiguration(); + + /** + * The "magic key" used for {@link Module}s that depend on shared context that isn't part of the {@link Module} + * object itself, for example the {@link Action}s of DSL rules with definitions outside the {@code rule} section. + * The information is stored in the {@link Module}'s {@link Configuration}. The payload is a {@link Boolean}. + */ + static String SHARED_CONTEXT = "sharedContext"; } 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 f0ae3dce1..4c139ddee 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 @@ -167,6 +167,20 @@ public interface Rule extends Identifiable { return null; } + /** + * The "magic key" used for the source code string itself when storing {@link Rule}'s source code in its + * {@link Configuration}. A {@link Rule} can only have a source code if it is created by a script. The payload + * is a string containing the whole source code. + */ + static String SOURCE = "source"; + + /** + * The "magic key" used for the source code type when storing {@link Rule}'s source code in its + * {@link Configuration}. A {@link Rule} can only have a source code if it is created by a script. The payload + * is a string containing OH's "quasi MIME-type" for the scripting language of the source code. + */ + static String SOURCE_TYPE = "sourceType"; + /** * This enum represent the different states a rule can have in respect to rule templates. */ diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/converter/RuleParser.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/converter/RuleParser.java new file mode 100644 index 000000000..1c85cfd95 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/converter/RuleParser.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2010-2026 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.automation.converter; + +import java.util.Collection; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.Rule; +import org.openhab.core.converter.ObjectParser; + +/** + * {@link RuleParser} is the interface to implement by any file parser for {@link Rule} object. + * + * @author Ravi Nadahar - Initial contribution + */ +@NonNullByDefault +public interface RuleParser extends ObjectParser { + + /** + * Parse the provided {@code syntax} string without impacting the rule registry. + * + * @param syntax the syntax in format. + * @param errors the {@link List} to use to report errors. + * @param warnings the {@link List} to be used to report warnings. + * @return The model name used for parsing if the parsing succeeded without errors; {@code null} otherwise. + */ + @Override + @Nullable + String startParsingFormat(String syntax, List errors, List warnings); + + /** + * Get the {@link Rule} objects found when parsing the format. + * + * @param modelName the model name used when parsing. + * @return The {@link Collection} of {@link Rule}s. + */ + @Override + Collection getParsedObjects(String modelName); +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/converter/RuleSerializer.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/converter/RuleSerializer.java new file mode 100644 index 000000000..d8a8f7381 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/converter/RuleSerializer.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2010-2026 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.automation.converter; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.Rule; +import org.openhab.core.converter.ObjectSerializer; +import org.openhab.core.converter.SerializabilityResult; +import org.openhab.core.io.dto.SerializationException; + +/** + * {@link RuleSerializer} is the interface to implement by any file generator for {@link Rule} object. + * + * @author Ravi Nadahar - Initial contribution + */ +@NonNullByDefault +public interface RuleSerializer extends ObjectSerializer { + + /** + * Checks if the specified rules are serializable with this {@link RuleSerializer}. Returned results are in the same + * order as the specified rules. + * + * @param rules the {@link List} of {@link Rule}s to check. + * @return The resulting {@link List} of {@link SerializabilityResult}s. + */ + List> checkSerializability(Collection rules); + + /** + * Specify the {@link List} of {@link Rule}s to be serialized and associate them with an identifier. + * + * @param id the identifier of the {@link Rule} format generation. + * @param rules the {@link List} of {@link Rule}s to serialize. + * @param option the option that determines how to serialize the {@link Rule}s. + * @throws SerializationException If one or more of the rules can't be serialized. + */ + void setRulesToBeSerialized(String id, List rules, RuleSerializationOption option) + throws SerializationException; + + /** + * An enum representing the different rule serialization options. + */ + public enum RuleSerializationOption { + + /** Empty collections and normally irrelevant fields are hidden */ + NORMAL("Normal"), + + /** Everything is serialized, including empty collections */ + INCLUDE_ALL("Include all"), + + /** Only the fields required in a rule stub to be used with a template is serialized */ + STUB_ONLY("Stub only"), + + /** Template information is stripped, otherwise like {@link #NORMAL} */ + STRIP_TEMPLATE("Strip template"); + + private final String friendlyName; + + private RuleSerializationOption(String friendlyName) { + this.friendlyName = friendlyName; + } + + @Override + public String toString() { + return friendlyName; + } + + public static @Nullable RuleSerializationOption fromString(@Nullable String id) { + if (id == null || id.isBlank()) { + return null; + } + String upId = id.toUpperCase(Locale.ROOT).trim(); + for (RuleSerializationOption option : values()) { + if (upId.equals(option.name()) || upId.equalsIgnoreCase(option.friendlyName) + || upId.equalsIgnoreCase(option.friendlyName.replace(" ", "")) + || upId.equalsIgnoreCase(option.friendlyName.replace(" ", "-"))) { + return option; + } + } + return null; + } + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/converter/RuleTemplateParser.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/converter/RuleTemplateParser.java new file mode 100644 index 000000000..e8d4772c6 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/converter/RuleTemplateParser.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2010-2026 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.automation.converter; + +import java.util.Collection; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.template.RuleTemplate; +import org.openhab.core.converter.ObjectParser; + +/** + * {@link RuleTemplateParser} is the interface to implement by any file parser for {@link RuleTemplate} object. + * + * @author Ravi Nadahar - Initial contribution + */ +@NonNullByDefault +public interface RuleTemplateParser extends ObjectParser { + + /** + * Parse the provided {@code syntax} string without impacting the rule template registry. + * + * @param syntax the syntax in format. + * @param errors the {@link List} to use to report errors. + * @param warnings the {@link List} to be used to report warnings. + * @return The model name used for parsing if the parsing succeeded without errors; {@code null} otherwise. + */ + @Override + @Nullable + String startParsingFormat(String syntax, List errors, List warnings); + + /** + * Get the {@link RuleTemplate} objects found when parsing the format. + * + * @param modelName the model name used when parsing. + * @return The {@link Collection} of {@link RuleTemplate}s. + */ + @Override + Collection getParsedObjects(String modelName); +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/converter/RuleTemplateSerializer.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/converter/RuleTemplateSerializer.java new file mode 100644 index 000000000..a4724ab68 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/converter/RuleTemplateSerializer.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2010-2026 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.automation.converter; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.converter.RuleSerializer.RuleSerializationOption; +import org.openhab.core.automation.template.RuleTemplate; +import org.openhab.core.converter.ObjectSerializer; +import org.openhab.core.converter.SerializabilityResult; +import org.openhab.core.io.dto.SerializationException; + +/** + * {@link RuleTemplateSerializer} is the interface to implement by any file generator for {@link RuleTemplate} object. + * + * @author Ravi Nadahar - Initial contribution + */ +@NonNullByDefault +public interface RuleTemplateSerializer extends ObjectSerializer { + + /** + * Checks if the specified rule templates are serializable with this {@link RuleTemplateSerializer}. Returned + * results are in the same order as the specified rule templates. + * + * @param templates the {@link List} of {@link RuleTemplate}s to check. + * @return The resulting {@link List} of {@link SerializabilityResult}s. + */ + List> checkSerializability(Collection templates); + + /** + * Specify the {@link List} of {@link RuleTemplate}s to be serialized and associate them with an identifier. + * + * @param id the identifier of the {@link RuleTemplate} format generation. + * @param templates the {@link List} of {@link RuleTemplate}s to serialize. + * @param option the option that determines how to serialize the {@link RuleTemplate}s. + * @throws SerializationException If one or more of the rule templates can't be serialized. + */ + void setTemplatesToBeSerialized(String id, List templates, RuleTemplateSerializationOption option) + throws SerializationException; + + /** + * An enum representing the different rule template serialization options. + */ + public enum RuleTemplateSerializationOption { + + /** Empty collections and normally irrelevant fields are hidden */ + NORMAL("Normal"), + + /** Everything is serialized, including empty collections */ + INCLUDE_ALL("Include all"); + + private final String friendlyName; + + private RuleTemplateSerializationOption(String friendlyName) { + this.friendlyName = friendlyName; + } + + /** + * Converts this {@link RuleTemplateSerializationOption} to a corresponding {@link RuleSerializationOption}. + * + * @return The corresponding {@link RuleSerializationOption}. + */ + public RuleSerializationOption toRuleSerializationOption() { + switch (this) { + case INCLUDE_ALL: + return RuleSerializationOption.INCLUDE_ALL; + case NORMAL: + return RuleSerializationOption.NORMAL; + default: + throw new UnsupportedOperationException( + "Missing toRuleSerializationOption() implementation for " + name()); + } + } + + @Override + public String toString() { + return friendlyName; + } + + /** + * Try to parse the specified string into a {@link RuleTemplateSerializationOption}. + * + * @param id The {@link String} to parse. + * @return The resulting {@link RuleTemplateSerializationOption} or {@code null}. + */ + public static @Nullable RuleTemplateSerializationOption fromString(@Nullable String id) { + if (id == null || id.isBlank()) { + return null; + } + String upId = id.toUpperCase(Locale.ROOT).trim(); + for (RuleTemplateSerializationOption option : values()) { + if (upId.equals(option.name()) || upId.equalsIgnoreCase(option.friendlyName) + || upId.equalsIgnoreCase(option.friendlyName.replace(" ", "")) + || upId.equalsIgnoreCase(option.friendlyName.replace(" ", "-"))) { + return option; + } + } + return null; + } + } +} 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 d12a316f0..ce6c82b78 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 @@ -60,8 +60,10 @@ public class RuleBuilder { this.actions = new LinkedList<>(rule.getActions()); this.configuration = new Configuration(rule.getConfiguration()); this.configDescriptions = new LinkedList<>(rule.getConfigurationDescriptions()); - this.templateUID = rule.getTemplateUID(); - this.templateState = TemplateState.NO_TEMPLATE; + String templateUID = rule.getTemplateUID(); + this.templateUID = templateUID; + this.templateState = templateUID == null || templateUID.isBlank() ? TemplateState.NO_TEMPLATE + : TemplateState.PENDING; this.uid = rule.getUID(); this.name = rule.getName(); this.tags = new HashSet<>(rule.getTags()); @@ -87,7 +89,7 @@ public class RuleBuilder { * @return The new {@link RuleBuilder}. */ public static RuleBuilder create(Rule r) { - return create(r.getUID(), r); + return new RuleBuilder(r); } /** @@ -108,7 +110,7 @@ public class RuleBuilder { /** * Build a new {@link Rule} from the specified {@link RuleTemplate} and {@link Configuration}. The resulting rule * will be ready for template placeholder substitution, but the placeholders won't actually have been substituted. - * This constructor is only suitable for preparing to substitute placeholders. + * This method is only suitable for preparing to substitute placeholders. * * @param template the {@link RuleTemplate} to use. * @param uid the UID of the resulting {@link Rule}. diff --git a/bundles/org.openhab.core.io.rest.core/pom.xml b/bundles/org.openhab.core.io.rest.core/pom.xml index 4a53460b6..d5843f378 100644 --- a/bundles/org.openhab.core.io.rest.core/pom.xml +++ b/bundles/org.openhab.core.io.rest.core/pom.xml @@ -20,6 +20,11 @@ org.openhab.core ${project.version} + + org.openhab.core.bundles + org.openhab.core.automation + ${project.version} + org.openhab.core.bundles org.openhab.core.config.core diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/fileformat/FileFormatDTO.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/fileformat/FileFormatDTO.java index 23021b068..ad44057be 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/fileformat/FileFormatDTO.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/fileformat/FileFormatDTO.java @@ -14,6 +14,8 @@ package org.openhab.core.io.rest.core.fileformat; import java.util.List; +import org.openhab.core.automation.dto.RuleDTO; +import org.openhab.core.automation.dto.RuleTemplateDTO; import org.openhab.core.semantics.dto.SemanticTagDTO; import org.openhab.core.sitemap.dto.SitemapDefinitionDTO; import org.openhab.core.thing.dto.ThingDTO; @@ -22,7 +24,7 @@ import io.swagger.v3.oas.annotations.media.Schema; /** * This is a data transfer object to serialize the different components that can be contained - * in a file format (items, things, ...). + * in a file format (items, things, rules, ...). * * @author Laurent Garnier - Initial contribution * @author Mark Herwege - Add sitemaps @@ -39,4 +41,8 @@ public class FileFormatDTO { public List things; @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED) public List sitemaps; + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED) + public List rules; + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED) + public List ruleTemplates; } diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/fileformat/FileFormatResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/fileformat/FileFormatResource.java index d98386cab..d4911603a 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/fileformat/FileFormatResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/fileformat/FileFormatResource.java @@ -15,6 +15,7 @@ package org.openhab.core.io.rest.core.internal.fileformat; import static org.openhab.core.config.discovery.inbox.InboxPredicates.forThingUID; import java.io.ByteArrayOutputStream; +import java.lang.reflect.Type; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -23,9 +24,11 @@ import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -41,10 +44,26 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status.Family; +import javax.ws.rs.core.Response.StatusType; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.auth.Role; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.RuleRegistry; +import org.openhab.core.automation.converter.RuleParser; +import org.openhab.core.automation.converter.RuleSerializer; +import org.openhab.core.automation.converter.RuleSerializer.RuleSerializationOption; +import org.openhab.core.automation.converter.RuleTemplateParser; +import org.openhab.core.automation.converter.RuleTemplateSerializer; +import org.openhab.core.automation.converter.RuleTemplateSerializer.RuleTemplateSerializationOption; +import org.openhab.core.automation.dto.RuleDTO; +import org.openhab.core.automation.dto.RuleDTOMapper; +import org.openhab.core.automation.dto.RuleTemplateDTO; +import org.openhab.core.automation.dto.RuleTemplateDTOMapper; +import org.openhab.core.automation.template.RuleTemplate; +import org.openhab.core.automation.template.TemplateRegistry; import org.openhab.core.config.core.ConfigDescription; import org.openhab.core.config.core.ConfigDescriptionParameter; import org.openhab.core.config.core.ConfigDescriptionRegistry; @@ -53,12 +72,15 @@ import org.openhab.core.config.core.Configuration; import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.inbox.Inbox; import org.openhab.core.converter.ObjectParser; +import org.openhab.core.io.dto.SerializationException; +import org.openhab.core.io.rest.JSONResponse; import org.openhab.core.io.rest.RESTConstants; import org.openhab.core.io.rest.RESTResource; import org.openhab.core.io.rest.core.fileformat.ExtendedFileFormatDTO; import org.openhab.core.io.rest.core.fileformat.FileFormatDTO; import org.openhab.core.io.rest.core.fileformat.FileFormatItemDTO; import org.openhab.core.io.rest.core.fileformat.FileFormatItemDTOMapper; +import org.openhab.core.io.rest.core.internal.rule.SerializabilityResultsDTO; import org.openhab.core.items.GroupItem; import org.openhab.core.items.Item; import org.openhab.core.items.ItemBuilderFactory; @@ -68,6 +90,7 @@ import org.openhab.core.items.MetadataKey; import org.openhab.core.items.MetadataRegistry; import org.openhab.core.items.fileconverter.ItemParser; import org.openhab.core.items.fileconverter.ItemSerializer; +import org.openhab.core.library.types.DateTimeType; import org.openhab.core.semantics.SemanticTag; import org.openhab.core.semantics.SemanticTagRegistry; import org.openhab.core.semantics.dto.SemanticTagDTO; @@ -117,9 +140,15 @@ import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; + import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -215,6 +244,137 @@ public class FileFormatResource implements RESTResource { param: my param value """; + private static final String DSL_RULE_EXAMPLE = """ + rule "My Rule" + when + Time is noon + then + logInfo("Test", "MyRule is running") + end + """; + + private static final String YAML_RULE_EXAMPLE = """ + version: 1 + rules: + MyRule: + label: My Rule + description: My rule description + actions: + - id: "2" + config: + type: DSL + script: | + logInfo("Test", "MyRule is running") + type: Script + triggers: + - id: "1" + config: + time: 12:00 + type: TimeOfDay + """; + + private static final String JSON_RULE_UID_CHECK_EXAMPLE = """ + [ "ruleUid1", "ruleUid2" ] + """; + + private static final String JSON_RULE_CHECK_EXAMPLE = """ + { + "rules": [ + { + "uid": "string", + "name": "string", + "description": "string", + "tags": [ + "string" + ], + "triggers": [ + { + "id": "string", + "label": "string", + "description": "string", + "configuration": { + "additionalProp1": {}, + "additionalProp2": {}, + "additionalProp3": {} + }, + "type": "string" + } + ], + "conditions": [ + { + "id": "string", + "label": "string", + "description": "string", + "configuration": { + "additionalProp1": {}, + "additionalProp2": {}, + "additionalProp3": {} + }, + "type": "string", + "inputs": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string" + } + } + ], + "actions": [ + { + "id": "string", + "label": "string", + "description": "string", + "configuration": { + "additionalProp1": {}, + "additionalProp2": {}, + "additionalProp3": {} + }, + "type": "string", + "inputs": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string" + } + } + ] + } + ] + } + """; + + private static final String YAML_RULE_TEMPLATE_EXAMPLE = """ + version: 1 + ruleTemplates: + my-template: + label: My Template + description: Logs when an Item state changes to ON + configDescriptions: + sourceItem: + context: item + description: The source Item whose state to monitor + label: Source Item + required: false + type: TEXT + readOnly: false + multiple: false + advanced: false + verify: false + limitToOptions: true + actions: + - id: "2" + config: + type: DSL + script: | + logInfo("Test", "{{sourceItem}} turned on") + type: Script + triggers: + - id: "1" + config: + itemName: "{{sourceItem}}" + state: "ON" + previousState: "OFF" + type: ItemChanged + """; + private static final String DSL_SITEMAPS_EXAMPLE = """ sitemap MySitemap label="My Sitemap" { Frame { @@ -297,6 +457,49 @@ public class FileFormatResource implements RESTResource { value: my value config: param: my param value + rules: + MyRule: + label: Label + actions: + - config: + type: DSL + script: | + logInfo("Test", "MyRule is running") + type: Script + triggers: + - config: + itemName: MyItem + type: ItemReceivedCommand + ruleTemplates: + my-template: + label: My Template + description: Logs when an Item state changes to ON + configDescriptions: + sourceItem: + context: item + description: The source Item whose state to monitor + label: Source Item + required: false + type: TEXT + readOnly: false + multiple: false + advanced: false + verify: false + limitToOptions: true + actions: + - id: "2" + config: + type: DSL + script: | + logInfo("Test", "{{sourceItem}} turned on") + type: Script + triggers: + - id: "1" + config: + itemName: "{{sourceItem}}" + state: "ON" + previousState: "OFF" + type: ItemChanged sitemaps: MySitemap: label: My Sitemap @@ -310,8 +513,27 @@ public class FileFormatResource implements RESTResource { private static final String GEN_ID_PATTERN = "gen_file_format_%d"; + private static final Response.StatusType UNPROCESSABLE_ENTITY = new StatusType() { + + @Override + public int getStatusCode() { + return 422; + } + + @Override + public String getReasonPhrase() { + return "Unprocessable Entity"; + } + + @Override + public Family getFamily() { + return Family.CLIENT_ERROR; + } + }; + private final Logger logger = LoggerFactory.getLogger(FileFormatResource.class); + private final Gson gson = new GsonBuilder().setDateFormat(DateTimeType.DATE_PATTERN_JSON_COMPAT).create(); private final SemanticTagRegistry semanticTagRegistry; private final ItemBuilderFactory itemBuilderFactory; private final ItemRegistry itemRegistry; @@ -322,6 +544,8 @@ public class FileFormatResource implements RESTResource { private final ThingTypeRegistry thingTypeRegistry; private final ChannelTypeRegistry channelTypeRegistry; private final ConfigDescriptionRegistry configDescRegistry; + private final RuleRegistry ruleRegistry; + private final TemplateRegistry templateRegistry; private final SitemapFactory sitemapFactory; private final SitemapRegistry sitemapRegistry; private final Map tagSerializers = new ConcurrentHashMap<>(); @@ -330,10 +554,14 @@ public class FileFormatResource implements RESTResource { private final Map itemParsers = new ConcurrentHashMap<>(); private final Map thingSerializers = new ConcurrentHashMap<>(); private final Map thingParsers = new ConcurrentHashMap<>(); + private final Map ruleSerializers = new ConcurrentHashMap<>(); + private final Map ruleParsers = new ConcurrentHashMap<>(); + private final Map templateSerializers = new ConcurrentHashMap<>(); + private final Map templateParsers = new ConcurrentHashMap<>(); private final Map sitemapSerializers = new ConcurrentHashMap<>(); private final Map sitemapParsers = new ConcurrentHashMap<>(); - private int counter; + private final AtomicInteger counter = new AtomicInteger(); @Activate public FileFormatResource(// @@ -347,6 +575,8 @@ public class FileFormatResource implements RESTResource { final @Reference ThingTypeRegistry thingTypeRegistry, // final @Reference ChannelTypeRegistry channelTypeRegistry, // final @Reference ConfigDescriptionRegistry configDescRegistry, // + final @Reference RuleRegistry ruleRegistry, // + final @Reference TemplateRegistry templateRegistry, // final @Reference SitemapFactory sitemapFactory, // final @Reference SitemapRegistry sitemapRegistry) { this.semanticTagRegistry = semanticTagRegistry; @@ -359,6 +589,8 @@ public class FileFormatResource implements RESTResource { this.thingTypeRegistry = thingTypeRegistry; this.channelTypeRegistry = channelTypeRegistry; this.configDescRegistry = configDescRegistry; + this.ruleRegistry = ruleRegistry; + this.templateRegistry = templateRegistry; this.sitemapFactory = sitemapFactory; this.sitemapRegistry = sitemapRegistry; } @@ -421,6 +653,42 @@ public class FileFormatResource implements RESTResource { thingParsers.remove(thingParser.getParserFormat()); } + @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE) + protected void addRuleSerializer(RuleSerializer ruleSerializer) { + ruleSerializers.put(ruleSerializer.getGeneratedFormat(), ruleSerializer); + } + + protected void removeRuleSerializer(RuleSerializer ruleSerializer) { + ruleSerializers.remove(ruleSerializer.getGeneratedFormat()); + } + + @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE) + protected void addRuleParser(RuleParser ruleParser) { + ruleParsers.put(ruleParser.getParserFormat(), ruleParser); + } + + protected void removeRuleParser(RuleParser ruleParser) { + ruleParsers.remove(ruleParser.getParserFormat()); + } + + @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE) + protected void addRuleTemplateSerializer(RuleTemplateSerializer templateSerializer) { + templateSerializers.put(templateSerializer.getGeneratedFormat(), templateSerializer); + } + + protected void removeRuleTemplateSerializer(RuleTemplateSerializer templateSerializer) { + templateSerializers.remove(templateSerializer.getGeneratedFormat()); + } + + @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE) + protected void addRuleTemplateParser(RuleTemplateParser templateParser) { + templateParsers.put(templateParser.getParserFormat(), templateParser); + } + + protected void removeRuleTemplateParser(RuleTemplateParser templateParser) { + templateParsers.remove(templateParser.getParserFormat()); + } + @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE) protected void addSitemapSerializer(SitemapSerializer sitemapSerializer) { sitemapSerializers.put(sitemapSerializer.getGeneratedFormat(), sitemapSerializer); @@ -529,6 +797,170 @@ public class FileFormatResource implements RESTResource { return Response.ok(outputStream.toString(StandardCharsets.UTF_8)).build(); } + @POST + @RolesAllowed({ Role.ADMIN }) + @Path("/rules") + @Consumes(MediaType.APPLICATION_JSON) + @Produces({ "application/vnd.openhab.dsl.rule", "application/yaml", MediaType.APPLICATION_JSON }) + @Operation(operationId = "createFileFormatForRules", summary = "Create file format for a list of rules in the registry.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK", content = { + @Content(mediaType = "application/vnd.openhab.dsl.rule", schema = @Schema(example = DSL_RULE_EXAMPLE)), + @Content(mediaType = "application/yaml", schema = @Schema(example = YAML_RULE_EXAMPLE)) }), + @ApiResponse(responseCode = "404", description = "One or more rules not found in the registry."), + @ApiResponse(responseCode = "415", description = "Unsupported media type."), + @ApiResponse(responseCode = "422", description = "Unable to serialize rule.") }) + public Response createFileFormatForRules(@Context HttpHeaders httpHeaders, + @DefaultValue("Normal") @QueryParam("serializationOption") @Parameter(description = "Decides what to include in serialized rules") RuleSerializationOption option, + @Parameter(description = "Array of rule UIDs. If empty or omitted, return all rules.") @Nullable List ruleUIDs) { + String acceptHeader = httpHeaders.getHeaderString(HttpHeaders.ACCEPT); + logger.debug("createFileFormatForRules: mediaType = {}, ruleUIDs = {}", acceptHeader, ruleUIDs); + RuleSerializer serializer = getRuleSerializer(acceptHeader); + if (serializer == null) { + return JSONResponse.createErrorResponse(Response.Status.UNSUPPORTED_MEDIA_TYPE, + "Unsupported media type '" + acceptHeader + "'"); + } + List rules; + if (ruleUIDs == null || ruleUIDs.isEmpty()) { + Collection all = ruleRegistry.getAll(); + if (all instanceof List allList) { + rules = allList; + } else { + rules = new ArrayList<>(all); + } + } else { + rules = new ArrayList<>(); + for (String ruleUID : ruleUIDs) { + Rule rule = ruleRegistry.get(ruleUID); + if (rule == null) { + return JSONResponse.createErrorResponse(Response.Status.NOT_FOUND, + "Rule with UID '" + ruleUID + "' not found in the rule registry!"); + } + rules.add(rule); + } + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + String genId = newIdForSerialization(); + try { + serializer.setRulesToBeSerialized(genId, rules, option); + } catch (SerializationException e) { + return JSONResponse.createErrorResponse(UNPROCESSABLE_ENTITY, e.getMessage()); + } + serializer.generateFormat(genId, outputStream); + return Response.ok(outputStream.toString(StandardCharsets.UTF_8)).build(); + } + + @POST + @RolesAllowed({ Role.ADMIN }) + @Path("/rules/check") + @Consumes(MediaType.APPLICATION_JSON) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(operationId = "canSerializeRules", summary = "Checks if the specified rule(s) can be serialized to the target format.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = SerializabilityResultsDTO.class))), + @ApiResponse(responseCode = "400", description = "No rule specified."), + @ApiResponse(responseCode = "404", description = "One or more rules not found in the registry."), + @ApiResponse(responseCode = "415", description = "Unsupported media type.") }) + public Response canSerializeRules(@Context HttpHeaders httpHeaders, + @DefaultValue("application/yaml") @QueryParam("targetFormat") @Parameter(description = "Target format", schema = @Schema(type = "string", allowableValues = { + "application/vnd.openhab.dsl.rule", "application/yaml" })) String targetFormat, + @RequestBody(description = "JSON rule data", content = @Content(mediaType = MediaType.APPLICATION_JSON, examples = { + @ExampleObject(name = "Rule UID example", value = JSON_RULE_UID_CHECK_EXAMPLE), + @ExampleObject(name = "Rule example", value = JSON_RULE_CHECK_EXAMPLE) }, schema = @Schema(oneOf = { + StringList.class, FileFormatDTO.class }))) JsonElement data) { + if (logger.isDebugEnabled()) { + logger.debug("createFileFormatForRules: targetFormat = {}, data = {}", targetFormat, data.getAsString()); + } + RuleSerializer serializer = getRuleSerializer(targetFormat); + if (serializer == null) { + return JSONResponse.createErrorResponse(Response.Status.UNSUPPORTED_MEDIA_TYPE, + "Unsupported target format '" + targetFormat + "'"); + } + List rules = new ArrayList<>(); + if (data.isJsonArray()) { + Type uidListType = new TypeToken>() { + }.getType(); + List uids = gson.fromJson(data, uidListType); + if (uids == null || uids.isEmpty()) { + return JSONResponse.createErrorResponse(Response.Status.BAD_REQUEST, "No Rule UID specified"); + } + for (String ruleUID : uids) { + Rule rule = ruleRegistry.get(ruleUID); + if (rule == null) { + return JSONResponse.createErrorResponse(Response.Status.NOT_FOUND, + "Rule with UID '" + ruleUID + "' not found in the rule registry!"); + } + rules.add(rule); + } + } else if (data.isJsonObject()) { + FileFormatDTO dto = gson.fromJson(data, FileFormatDTO.class); + if (dto != null && dto.rules != null) { + for (RuleDTO ruleData : dto.rules) { + rules.add(RuleDTOMapper.map(ruleData)); + } + } + if (rules.isEmpty()) { + return JSONResponse.createErrorResponse(Response.Status.BAD_REQUEST, "No Rule specified"); + } + } else { + return JSONResponse.createErrorResponse(Response.Status.BAD_REQUEST, "Unsupported input"); + } + return JSONResponse.createResponse(Response.Status.OK, + new SerializabilityResultsDTO(serializer.checkSerializability(rules)), null); + } + + @POST + @RolesAllowed({ Role.ADMIN }) + @Path("/ruletemplates") + @Consumes(MediaType.APPLICATION_JSON) + @Produces({ "application/yaml", MediaType.APPLICATION_JSON }) + @Operation(operationId = "createFileFormatForRuleTemplates", summary = "Create file format for a list of rule templates in the registry.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK", content = { + @Content(mediaType = "application/yaml", schema = @Schema(example = YAML_RULE_TEMPLATE_EXAMPLE)) }), + @ApiResponse(responseCode = "404", description = "One or more rule templates not found in the registry."), + @ApiResponse(responseCode = "415", description = "Unsupported media type."), + @ApiResponse(responseCode = "422", description = "Unable to serialize rule template.") }) + public Response createFileFormatForRuleTemplates(@Context HttpHeaders httpHeaders, + @DefaultValue("Normal") @QueryParam("serializationOption") @Parameter(description = "Decides what to include in serialized rule templates") RuleTemplateSerializationOption option, + @Parameter(description = "Array of rule template UIDs. If empty or omitted, return all rule templates.") @Nullable List templateUIDs) { + String acceptHeader = httpHeaders.getHeaderString(HttpHeaders.ACCEPT); + logger.debug("createFileFormatForRuleTemplates: mediaType = {}, templateUIDs = {}", acceptHeader, templateUIDs); + RuleTemplateSerializer serializer = getRuleTemplateSerializer(acceptHeader); + if (serializer == null) { + return JSONResponse.createErrorResponse(Response.Status.UNSUPPORTED_MEDIA_TYPE, + "Unsupported media type '" + acceptHeader + "'"); + } + List templates; + if (templateUIDs == null || templateUIDs.isEmpty()) { + Collection all = templateRegistry.getAll(); + if (all instanceof List allList) { + templates = allList; + } else { + templates = new ArrayList<>(all); + } + } else { + templates = new ArrayList<>(); + for (String templateUID : templateUIDs) { + RuleTemplate template = templateRegistry.get(templateUID); + if (template == null) { + return JSONResponse.createErrorResponse(Response.Status.NOT_FOUND, + "Rule template with UID '" + templateUID + "' not found in the registry!"); + } + templates.add(template); + } + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + String genId = newIdForSerialization(); + try { + serializer.setTemplatesToBeSerialized(genId, templates, option); + } catch (SerializationException e) { + return JSONResponse.createErrorResponse(UNPROCESSABLE_ENTITY, e.getMessage()); + } + serializer.generateFormat(genId, outputStream); + return Response.ok(outputStream.toString(StandardCharsets.UTF_8)).build(); + } + @POST @RolesAllowed({ Role.ADMIN }) @Path("/sitemaps") @@ -633,21 +1065,24 @@ public class FileFormatResource implements RESTResource { @RolesAllowed({ Role.ADMIN }) @Path("/create") @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ "text/vnd.openhab.dsl.thing", "text/vnd.openhab.dsl.item", "text/vnd.openhab.dsl.sitemap", - "application/yaml" }) + @Produces({ "text/vnd.openhab.dsl.thing", "text/vnd.openhab.dsl.item", "application/vnd.openhab.dsl.rule", + "text/vnd.openhab.dsl.sitemap", "application/yaml" }) @Operation(operationId = "create", summary = "Create file format.", security = { @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { @ApiResponse(responseCode = "200", description = "OK", content = { @Content(mediaType = "application/yaml", schema = @Schema(example = YAML_FULL_EXAMPLE)), @Content(mediaType = "text/vnd.openhab.dsl.thing", schema = @Schema(example = DSL_THINGS_EXAMPLE)), @Content(mediaType = "text/vnd.openhab.dsl.item", schema = @Schema(example = DSL_ITEMS_EXAMPLE)), + @Content(mediaType = "application/vnd.openhab.dsl.rule", schema = @Schema(example = DSL_RULE_EXAMPLE)), @Content(mediaType = "text/vnd.openhab.dsl.sitemap", schema = @Schema(example = DSL_SITEMAPS_EXAMPLE)) }), @ApiResponse(responseCode = "400", description = "Invalid JSON data."), - @ApiResponse(responseCode = "415", description = "Unsupported media type.") }) + @ApiResponse(responseCode = "415", description = "Unsupported media type."), + @ApiResponse(responseCode = "422", description = "Unable to serialize entity.") }) public Response create(final @Context HttpHeaders httpHeaders, @DefaultValue("false") @QueryParam("hideDefaultParameters") @Parameter(description = "if true, exclude the configuration parameters having the default value from the result.") boolean hideDefaultParameters, @DefaultValue("false") @QueryParam("hideDefaultChannels") @Parameter(description = "if true, exclude the non extensible channels having a default configuration from the result.") boolean hideDefaultChannels, @DefaultValue("false") @QueryParam("hideChannelLinksAndMetadata") @Parameter(description = "if true, exclude the channel links and metadata for items from the result.") boolean hideChannelLinksAndMetadata, + @DefaultValue("Normal") @QueryParam("ruleSerializationOption") @Parameter(description = "Decides what to include in serialized rules and rule templates") RuleSerializationOption ruleOption, @RequestBody(description = "JSON data", required = true, content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = FileFormatDTO.class))) FileFormatDTO data) { String acceptHeader = httpHeaders.getHeaderString(HttpHeaders.ACCEPT); logger.debug("create: mediaType = {}", acceptHeader); @@ -657,15 +1092,20 @@ public class FileFormatResource implements RESTResource { List items = new ArrayList<>(); List metadata = new ArrayList<>(); Map stateFormatters = new HashMap<>(); + List rules = new ArrayList<>(); + List templates = new ArrayList<>(); List sitemaps = new ArrayList<>(); List errors = new ArrayList<>(); - if (!convertFromFileFormatDTO(data, tags, things, items, metadata, stateFormatters, sitemaps, errors)) { + if (!convertFromFileFormatDTO(data, tags, things, items, metadata, stateFormatters, rules, templates, sitemaps, + errors)) { return Response.status(Response.Status.BAD_REQUEST).entity(String.join("\n", errors)).build(); } SemanticTagSerializer tagSerializer = getSemanticTagSerializer(acceptHeader); ThingSerializer thingSerializer = getThingSerializer(acceptHeader); ItemSerializer itemSerializer = getItemSerializer(acceptHeader); + RuleSerializer ruleSerializer = getRuleSerializer(acceptHeader); + RuleTemplateSerializer templateSerializer = getRuleTemplateSerializer(acceptHeader); SitemapSerializer sitemapSerializer = getSitemapSerializer(acceptHeader); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); String genId = newIdForSerialization(); @@ -691,6 +1131,21 @@ public class FileFormatResource implements RESTResource { stateFormatters, hideDefaultParameters); itemSerializer.generateFormat(genId, outputStream); break; + case "application/vnd.openhab.dsl.rule": + if (ruleSerializer == null) { + return JSONResponse.createErrorResponse(Response.Status.UNSUPPORTED_MEDIA_TYPE, + "Unsupported media type '" + acceptHeader + "'"); + } + if (rules.isEmpty()) { + return JSONResponse.createErrorResponse(Response.Status.BAD_REQUEST, "No rule loaded from input"); + } + try { + ruleSerializer.setRulesToBeSerialized(genId, rules, ruleOption); + } catch (SerializationException e) { + return JSONResponse.createErrorResponse(UNPROCESSABLE_ENTITY, e.getMessage()); + } + ruleSerializer.generateFormat(genId, outputStream); + break; case "text/vnd.openhab.dsl.sitemap": if (sitemapSerializer == null) { return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE) @@ -712,6 +1167,23 @@ public class FileFormatResource implements RESTResource { itemSerializer.setItemsToBeSerialized(genId, items, hideChannelLinksAndMetadata ? List.of() : metadata, stateFormatters, hideDefaultParameters); } + if (ruleSerializer != null) { + try { + ruleSerializer.setRulesToBeSerialized(genId, rules, ruleOption); + } catch (SerializationException e) { + return JSONResponse.createErrorResponse(UNPROCESSABLE_ENTITY, e.getMessage()); + } + } + if (templateSerializer != null) { + try { + templateSerializer.setTemplatesToBeSerialized(genId, templates, + ruleOption == RuleSerializationOption.INCLUDE_ALL + ? RuleTemplateSerializationOption.INCLUDE_ALL + : RuleTemplateSerializationOption.NORMAL); + } catch (SerializationException e) { + return JSONResponse.createErrorResponse(UNPROCESSABLE_ENTITY, e.getMessage()); + } + } if (sitemapSerializer != null) { sitemapSerializer.setSitemapsToBeSerialized(genId, sitemaps); } @@ -721,6 +1193,10 @@ public class FileFormatResource implements RESTResource { thingSerializer.generateFormat(genId, outputStream); } else if (itemSerializer != null) { itemSerializer.generateFormat(genId, outputStream); + } else if (ruleSerializer != null) { + ruleSerializer.generateFormat(genId, outputStream); + } else if (templateSerializer != null) { + templateSerializer.generateFormat(genId, outputStream); } else if (sitemapSerializer != null) { sitemapSerializer.generateFormat(genId, outputStream); } @@ -735,8 +1211,8 @@ public class FileFormatResource implements RESTResource { @POST @RolesAllowed({ Role.ADMIN }) @Path("/parse") - @Consumes({ "text/vnd.openhab.dsl.thing", "text/vnd.openhab.dsl.item", "text/vnd.openhab.dsl.sitemap", - "application/yaml" }) + @Consumes({ "text/vnd.openhab.dsl.thing", "text/vnd.openhab.dsl.item", "application/vnd.openhab.dsl.rule", + "text/vnd.openhab.dsl.sitemap", "application/yaml" }) @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "parse", summary = "Parse file format.", security = { @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { @@ -748,6 +1224,7 @@ public class FileFormatResource implements RESTResource { @Content(mediaType = "application/yaml", schema = @Schema(example = YAML_FULL_EXAMPLE)), @Content(mediaType = "text/vnd.openhab.dsl.thing", schema = @Schema(example = DSL_THINGS_EXAMPLE)), @Content(mediaType = "text/vnd.openhab.dsl.item", schema = @Schema(example = DSL_ITEMS_EXAMPLE)), + @Content(mediaType = "application/vnd.openhab.dsl.rule", schema = @Schema(example = DSL_RULE_EXAMPLE)), @Content(mediaType = "text/vnd.openhab.dsl.sitemap", schema = @Schema(example = DSL_SITEMAPS_EXAMPLE)) }) String input) { String contentTypeHeader = httpHeaders.getHeaderString(HttpHeaders.CONTENT_TYPE); logger.debug("parse: contentType = {}", contentTypeHeader); @@ -760,11 +1237,15 @@ public class FileFormatResource implements RESTResource { Collection metadata = List.of(); Collection channelLinks = List.of(); Map stateFormatters = Map.of(); + Collection rules = List.of(); + Collection templates = List.of(); List errors = new ArrayList<>(); List warnings = new ArrayList<>(); SemanticTagParser tagParser = getSemanticTagParser(contentTypeHeader); ThingParser thingParser = getThingParser(contentTypeHeader); ItemParser itemParser = getItemParser(contentTypeHeader); + RuleParser ruleParser = getRuleParser(contentTypeHeader); + RuleTemplateParser templateParser = getRuleTemplateParser(contentTypeHeader); SitemapParser sitemapParser = getSitemapParser(contentTypeHeader); ObjectParser parserStartingParsing = null; String modelName = null; @@ -808,6 +1289,22 @@ public class FileFormatResource implements RESTResource { channelLinks = thingParser.getParsedChannelLinks(modelName); } break; + case "application/vnd.openhab.dsl.rule": + if (ruleParser == null) { + return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE) + .entity("Unsupported content type '" + contentTypeHeader + "'!").build(); + } + parserStartingParsing = ruleParser; + modelName = ruleParser.startParsingFormat(input, errors, warnings); + if (modelName == null) { + return Response.status(Response.Status.BAD_REQUEST).entity(String.join("\n", errors)).build(); + } + rules = ruleParser.getParsedObjects(modelName); + if (rules.isEmpty()) { + ruleParser.finishParsingFormat(modelName); + return Response.status(Response.Status.BAD_REQUEST).entity("No rule loaded from input").build(); + } + break; case "text/vnd.openhab.dsl.sitemap": if (sitemapParser == null) { return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE) @@ -835,7 +1332,7 @@ public class FileFormatResource implements RESTResource { channelLinks = thingParser.getParsedChannelLinks(modelName); } if (itemParser != null) { - // Avoid parsing the input a second time + // Avoid parsing the input again if (modelName == null) { parserStartingParsing = itemParser; modelName = itemParser.startParsingFormat(input, errors, warnings); @@ -848,8 +1345,32 @@ public class FileFormatResource implements RESTResource { metadata = itemParser.getParsedMetadata(modelName); stateFormatters = itemParser.getParsedStateFormatters(modelName); } + if (ruleParser != null) { + // Avoid parsing the input again + if (modelName == null) { + parserStartingParsing = ruleParser; + modelName = ruleParser.startParsingFormat(input, errors, warnings); + if (modelName == null) { + return Response.status(Response.Status.BAD_REQUEST).entity(String.join("\n", errors)) + .build(); + } + } + rules = ruleParser.getParsedObjects(modelName); + } + if (templateParser != null) { + // Avoid parsing the input again + if (modelName == null) { + parserStartingParsing = templateParser; + modelName = templateParser.startParsingFormat(input, errors, warnings); + if (modelName == null) { + return Response.status(Response.Status.BAD_REQUEST).entity(String.join("\n", errors)) + .build(); + } + } + templates = templateParser.getParsedObjects(modelName); + } if (sitemapParser != null) { - // Avoid parsing the input a second time + // Avoid parsing the input again if (modelName == null) { parserStartingParsing = sitemapParser; modelName = sitemapParser.startParsingFormat(input, errors, warnings); @@ -861,7 +1382,7 @@ public class FileFormatResource implements RESTResource { sitemaps = sitemapParser.getParsedObjects(modelName); } if (tagParser != null) { - // Avoid parsing the input a second time + // Avoid parsing the input again if (modelName == null) { parserStartingParsing = tagParser; modelName = tagParser.startParsingFormat(input, errors, warnings); @@ -878,7 +1399,7 @@ public class FileFormatResource implements RESTResource { .entity("Unsupported content type '" + contentTypeHeader + "'!").build(); } ExtendedFileFormatDTO result = convertToFileFormatDTO(tags, things, items, metadata, stateFormatters, - channelLinks, sitemaps, warnings); + channelLinks, rules, templates, sitemaps, warnings); if (modelName != null && parserStartingParsing != null) { parserStartingParsing.finishParsingFormat(modelName); } @@ -886,7 +1407,7 @@ public class FileFormatResource implements RESTResource { } private String newIdForSerialization() { - return GEN_ID_PATTERN.formatted(++counter); + return String.format(Locale.ROOT, GEN_ID_PATTERN, counter.incrementAndGet()); } /* @@ -1056,6 +1577,26 @@ public class FileFormatResource implements RESTResource { }; } + private @Nullable RuleSerializer getRuleSerializer(String mediaType) { + switch (mediaType) { + case "application/yaml": + return ruleSerializers.get("YAML"); + case "application/vnd.openhab.dsl.rule": + return ruleSerializers.get("DSL"); + default: + return null; + } + } + + private @Nullable RuleTemplateSerializer getRuleTemplateSerializer(String mediaType) { + switch (mediaType) { + case "application/yaml": + return templateSerializers.get("YAML"); + default: + return null; + } + } + private @Nullable SitemapSerializer getSitemapSerializer(String mediaType) { return switch (mediaType) { case "text/vnd.openhab.dsl.sitemap" -> sitemapSerializers.get("DSL"); @@ -1088,6 +1629,26 @@ public class FileFormatResource implements RESTResource { }; } + private @Nullable RuleParser getRuleParser(String contentType) { + switch (contentType) { + case "application/yaml": + return ruleParsers.get("YAML"); + case "application/vnd.openhab.dsl.rule": + return ruleParsers.get("DSL"); + default: + return null; + } + } + + private @Nullable RuleTemplateParser getRuleTemplateParser(String contentType) { + switch (contentType) { + case "application/yaml": + return templateParsers.get("YAML"); + default: + return null; + } + } + private @Nullable SitemapParser getSitemapParser(String contentType) { return switch (contentType) { case "text/vnd.openhab.dsl.sitemap" -> sitemapParsers.get("DSL"); @@ -1118,8 +1679,8 @@ public class FileFormatResource implements RESTResource { } private boolean convertFromFileFormatDTO(FileFormatDTO data, List tags, List things, - List items, List metadata, Map stateFormatters, List sitemaps, - List errors) { + List items, List metadata, Map stateFormatters, List rules, + List templates, List sitemaps, List errors) { boolean ok = true; if (data.things != null) { for (ThingDTO thingData : data.things) { @@ -1202,6 +1763,16 @@ public class FileFormatResource implements RESTResource { } } } + if (data.rules != null) { + for (RuleDTO ruleData : data.rules) { + rules.add(RuleDTOMapper.map(ruleData)); + } + } + if (data.ruleTemplates != null) { + for (RuleTemplateDTO templateData : data.ruleTemplates) { + templates.add(RuleTemplateDTOMapper.map(templateData)); + } + } if (data.sitemaps != null) { for (SitemapDefinitionDTO sitemapData : data.sitemaps) { String name = sitemapData.name; @@ -1239,7 +1810,8 @@ public class FileFormatResource implements RESTResource { private ExtendedFileFormatDTO convertToFileFormatDTO(Collection tags, Collection things, Collection items, Collection metadata, Map stateFormatters, - Collection channelLinks, Collection sitemaps, List warnings) { + Collection channelLinks, Collection rules, Collection templates, + Collection sitemaps, List warnings) { ExtendedFileFormatDTO dto = new ExtendedFileFormatDTO(); dto.warnings = warnings.isEmpty() ? null : warnings; if (!things.isEmpty()) { @@ -1286,6 +1858,18 @@ public class FileFormatResource implements RESTResource { FileFormatItemDTOMapper.map(item, metadata, stateFormatters.get(item.getName()), channelLinks)); }); } + if (!rules.isEmpty()) { + dto.rules = new ArrayList<>(); + for (Rule rule : rules) { + dto.rules.add(RuleDTOMapper.map(rule)); + } + } + if (!templates.isEmpty()) { + dto.ruleTemplates = new ArrayList<>(); + for (RuleTemplate template : templates) { + dto.ruleTemplates.add(RuleTemplateDTOMapper.map(template)); + } + } if (!sitemaps.isEmpty()) { dto.sitemaps = new ArrayList<>(); sitemaps.forEach(sitemap -> { @@ -1400,4 +1984,7 @@ public class FileFormatResource implements RESTResource { throw new BadRequestException("Invalid URI syntax: " + uriString); } } + + private static interface StringList extends List { + } } diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/rule/SerializabilityResultsDTO.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/rule/SerializabilityResultsDTO.java new file mode 100644 index 000000000..c5a0cebc0 --- /dev/null +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/rule/SerializabilityResultsDTO.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2026 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.rest.core.internal.rule; + +import java.util.List; + +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.converter.SerializabilityResult; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * A DTO representing a list of {@link SerializabilityResult}s. + * + * @author Ravi Nadahar - initial contribution + */ +@Schema(name = "SerializabilityResults") +public class SerializabilityResultsDTO { + public List> results; + + public SerializabilityResultsDTO(@Nullable List> results) { + this.results = results; + } +} diff --git a/bundles/org.openhab.core.model.rule.runtime/src/org/openhab/core/model/rule/runtime/internal/DSLRuleProvider.java b/bundles/org.openhab.core.model.rule.runtime/src/org/openhab/core/model/rule/runtime/internal/DSLRuleProvider.java index 2b6a6959b..f4e171d91 100644 --- a/bundles/org.openhab.core.model.rule.runtime/src/org/openhab/core/model/rule/runtime/internal/DSLRuleProvider.java +++ b/bundles/org.openhab.core.model.rule.runtime/src/org/openhab/core/model/rule/runtime/internal/DSLRuleProvider.java @@ -28,11 +28,14 @@ import org.eclipse.emf.ecore.EObject; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.xtext.nodemodel.ICompositeNode; +import org.eclipse.xtext.nodemodel.ILeafNode; +import org.eclipse.xtext.nodemodel.INode; import org.eclipse.xtext.nodemodel.util.NodeModelUtils; import org.eclipse.xtext.xbase.XExpression; import org.eclipse.xtext.xbase.interpreter.IEvaluationContext; import org.openhab.core.automation.Action; import org.openhab.core.automation.Condition; +import org.openhab.core.automation.Module; import org.openhab.core.automation.Rule; import org.openhab.core.automation.RuleProvider; import org.openhab.core.automation.Trigger; @@ -88,6 +91,7 @@ import org.openhab.core.model.rule.rules.TimeOfDayCondition; import org.openhab.core.model.rule.rules.TimerTrigger; import org.openhab.core.model.rule.rules.UpdateEventTrigger; import org.openhab.core.model.rule.rules.WeekdayCondition; +import org.openhab.core.model.rule.rules.impl.RuleImpl; import org.openhab.core.model.script.runtime.DSLScriptContextProvider; import org.openhab.core.model.script.script.Script; import org.openhab.core.service.ReadyMarker; @@ -114,7 +118,7 @@ import org.slf4j.LoggerFactory; public class DSLRuleProvider implements RuleProvider, ModelRepositoryChangeListener, DSLScriptContextProvider, ReadyTracker { - static final String MIMETYPE_OPENHAB_DSL_RULE = "application/vnd.openhab.dsl.rule"; + public static final String MIMETYPE_OPENHAB_DSL_RULE = "application/vnd.openhab.dsl.rule"; private final Logger logger = LoggerFactory.getLogger(DSLRuleProvider.class); private final Collection> listeners = new ArrayList<>(); @@ -357,11 +361,49 @@ public class DSLRuleProvider Configuration cfg = new Configuration(); cfg.put(AbstractScriptModuleHandler.CONFIG_SCRIPT, context + removeIndentation(script)); cfg.put(AbstractScriptModuleHandler.CONFIG_SCRIPT_TYPE, MIMETYPE_OPENHAB_DSL_RULE); + INode ruleNode = NodeModelUtils.findActualNodeFor(rule); + boolean sharedContext = hasSharedContext(ruleNode); + if (sharedContext) { + cfg.put(Module.SHARED_CONTEXT, Boolean.TRUE); + } List actions = List.of(ActionBuilder.create().withId("script").withTypeUID(ScriptActionHandler.TYPE_ID) .withConfiguration(cfg).build()); + Configuration ruleCfg = new Configuration(); + String source = sharedContext ? ruleNode.getParent().getText() : ruleNode.getText().replaceFirst("^\\R+", ""); + source = source.replaceAll("\\R", "\n"); + if (!source.endsWith("\n")) { + source += '\n'; + } + ruleCfg.put(Rule.SOURCE, source); + ruleCfg.put(Rule.SOURCE_TYPE, MIMETYPE_OPENHAB_DSL_RULE); return RuleBuilder.create(uid).withTags(rule.getTags()).withName(name).withTriggers(triggers) - .withActions(actions).withConditions(conditions).build(); + .withActions(actions).withConditions(conditions).withConfiguration(ruleCfg).build(); + } + + private boolean hasSharedContext(INode ruleNode) { + INode node = ruleNode; + EObject eObject; + while ((node = node.getPreviousSibling()) != null) { + if (node instanceof ILeafNode leaf && leaf.isHidden()) { + continue; + } + if ((eObject = node.getSemanticElement()) != null && !(eObject instanceof RuleImpl)) { + return true; + } + } + + node = ruleNode; + while ((node = node.getNextSibling()) != null) { + if (node instanceof ILeafNode leaf && leaf.isHidden()) { + continue; + } + if ((eObject = node.getSemanticElement()) != null && !(eObject instanceof RuleImpl)) { + return true; + } + } + + return false; } private String removeIndentation(String script) { diff --git a/bundles/org.openhab.core.model.rule.runtime/src/org/openhab/core/model/rule/runtime/internal/converter/DslRuleConverter.java b/bundles/org.openhab.core.model.rule.runtime/src/org/openhab/core/model/rule/runtime/internal/converter/DslRuleConverter.java new file mode 100644 index 000000000..c7af26cb6 --- /dev/null +++ b/bundles/org.openhab.core.model.rule.runtime/src/org/openhab/core/model/rule/runtime/internal/converter/DslRuleConverter.java @@ -0,0 +1,854 @@ +/* + * Copyright (c) 2010-2026 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.model.rule.runtime.internal.converter; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.emf.common.util.EList; +import org.eclipse.emf.ecore.util.Diagnostician; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.xtext.xbase.XBlockExpression; +import org.eclipse.xtext.xbase.XStringLiteral; +import org.eclipse.xtext.xbase.XVariableDeclaration; +import org.eclipse.xtext.xbase.XbaseFactory; +import org.openhab.core.automation.Action; +import org.openhab.core.automation.Condition; +import org.openhab.core.automation.Module; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.Visibility; +import org.openhab.core.automation.converter.RuleParser; +import org.openhab.core.automation.converter.RuleSerializer; +import org.openhab.core.automation.internal.module.handler.ChannelEventTriggerHandler; +import org.openhab.core.automation.internal.module.handler.DateTimeTriggerHandler; +import org.openhab.core.automation.internal.module.handler.DayOfWeekConditionHandler; +import org.openhab.core.automation.internal.module.handler.EphemerisConditionHandler; +import org.openhab.core.automation.internal.module.handler.GenericCronTriggerHandler; +import org.openhab.core.automation.internal.module.handler.GroupCommandTriggerHandler; +import org.openhab.core.automation.internal.module.handler.GroupStateTriggerHandler; +import org.openhab.core.automation.internal.module.handler.IntervalConditionHandler; +import org.openhab.core.automation.internal.module.handler.ItemCommandTriggerHandler; +import org.openhab.core.automation.internal.module.handler.ItemStateConditionHandler; +import org.openhab.core.automation.internal.module.handler.ItemStateTriggerHandler; +import org.openhab.core.automation.internal.module.handler.SystemTriggerHandler; +import org.openhab.core.automation.internal.module.handler.ThingStatusConditionHandler; +import org.openhab.core.automation.internal.module.handler.ThingStatusTriggerHandler; +import org.openhab.core.automation.internal.module.handler.TimeOfDayConditionHandler; +import org.openhab.core.automation.internal.module.handler.TimeOfDayTriggerHandler; +import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRule; +import org.openhab.core.automation.util.ActionBuilder; +import org.openhab.core.automation.util.RuleBuilder; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.converter.SerializabilityResult; +import org.openhab.core.io.dto.SerializationException; +import org.openhab.core.model.core.ModelRepository; +import org.openhab.core.model.rule.rules.ChangedEventTrigger; +import org.openhab.core.model.rule.rules.CommandEventTrigger; +import org.openhab.core.model.rule.rules.DateTimeTrigger; +import org.openhab.core.model.rule.rules.DayOfWeekCondition; +import org.openhab.core.model.rule.rules.EventEmittedTrigger; +import org.openhab.core.model.rule.rules.EventTrigger; +import org.openhab.core.model.rule.rules.GroupMemberChangedEventTrigger; +import org.openhab.core.model.rule.rules.GroupMemberCommandEventTrigger; +import org.openhab.core.model.rule.rules.GroupMemberUpdateEventTrigger; +import org.openhab.core.model.rule.rules.HolidayCondition; +import org.openhab.core.model.rule.rules.InDaysetCondition; +import org.openhab.core.model.rule.rules.IntervalCondition; +import org.openhab.core.model.rule.rules.ItemStateCondition; +import org.openhab.core.model.rule.rules.RuleModel; +import org.openhab.core.model.rule.rules.RulesFactory; +import org.openhab.core.model.rule.rules.SystemStartlevelTrigger; +import org.openhab.core.model.rule.rules.ThingStateChangedEventTrigger; +import org.openhab.core.model.rule.rules.ThingStateUpdateEventTrigger; +import org.openhab.core.model.rule.rules.ThingStatusCondition; +import org.openhab.core.model.rule.rules.TimeOfDayCondition; +import org.openhab.core.model.rule.rules.TimerTrigger; +import org.openhab.core.model.rule.rules.UpdateEventTrigger; +import org.openhab.core.model.rule.rules.ValidCommand; +import org.openhab.core.model.rule.rules.ValidState; +import org.openhab.core.model.rule.rules.ValidTrigger; +import org.openhab.core.model.rule.rules.WeekdayCondition; +import org.openhab.core.model.rule.runtime.internal.DSLRuleProvider; +import org.openhab.core.model.script.scoping.StateAndCommandProvider; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link DslRuleFileConverter} is the DSL converter for {@link Rule} objects, which can parse and generate Rule DSL + * syntax. + * + * @author Ravi Nadahar - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = { RuleSerializer.class, RuleParser.class }) +public class DslRuleConverter implements RuleSerializer, RuleParser { + + private static final String SCRIPT_PLACEHOLDER_PREFIX = "SCRIPT_PLACEHOLDER_"; + private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile( + "(?<=then\\R)^\\s*val\\splaceholder=\"SCRIPT_PLACEHOLDER_(?[^\"]+)\"\\s*$\\R*", Pattern.MULTILINE); + private static final Pattern CONTEXT_COMMENT_PATTERN = Pattern.compile("^// context:.*$\\R", Pattern.MULTILINE); + private static final Pattern INDENTATION_PATTERN = Pattern.compile("^(?=.)", Pattern.MULTILINE); + private static final Pattern NUMERIC_PATTERN = Pattern.compile("-?\\d+(\\.\\d+)?"); + private static final Pattern TIME_PATTERN = Pattern.compile("^([012]?\\d):([0-5]\\d)$"); + private static final Pattern INDEX_PATTERN = Pattern.compile("-(?\\d+)$"); + private final Set enumStates; + private final Set enumCommands; + + private final Logger logger = LoggerFactory.getLogger(DslRuleConverter.class); + + private final ModelRepository modelRepository; + private final DSLRuleProvider ruleProvider; + + private record ScriptElement(String placeholderLiteral, String scriptContent) { + } + + private final Map elementsToGenerate = new ConcurrentHashMap<>(); + + private final Map> scriptElements = new ConcurrentHashMap<>(); + + @Activate + public DslRuleConverter(@Reference ModelRepository modelRepository, @Reference DSLRuleProvider ruleProvider) { + this.modelRepository = modelRepository; + this.ruleProvider = ruleProvider; + + Set enums = new LinkedHashSet<>(); + for (State state : StateAndCommandProvider.getAllStates()) { + enums.add(state.toString()); + } + this.enumStates = Set.copyOf(enums); + + enums = new LinkedHashSet<>(); + for (Command command : StateAndCommandProvider.getAllCommands()) { + enums.add(command.toString()); + } + this.enumCommands = Set.copyOf(enums); + } + + @Override + public String getParserFormat() { + return "DSL"; + } + + @Override + public String getGeneratedFormat() { + return "DSL"; + } + + @Override + public List> checkSerializability(Collection rules) { + List> result = new ArrayList<>(rules.size()); + List errors = new ArrayList<>(); + String s; + for (Rule rule : rules) { + if (rule instanceof SimpleRule) { + result.add(new SerializabilityResult<>(rule.getUID(), false, + "Rule '" + rule.getUID() + "' is a SimpleRule with an inaccessible action.")); + continue; + } + errors.clear(); + if (rule.getVisibility() != Visibility.VISIBLE) { + errors.add("isn't visible"); + } + if ((s = rule.getDescription()) != null && !s.isBlank()) { + errors.add("has a description"); + } + for (Trigger trigger : rule.getTriggers()) { + try { + buildModelTrigger(trigger); + } catch (SerializationException e) { + errors.add("trigger '" + trigger.getId() + "': " + e.getMessage()); + } + } + + for (Condition condition : rule.getConditions()) { + if (!condition.getInputs().isEmpty()) { + errors.add("condition '" + condition.getId() + "' has mapped inputs"); + continue; + } + try { + buildModelCondition(condition); + } catch (SerializationException e) { + errors.add("condition '" + condition.getId() + "': " + e.getMessage()); + } + } + + if (rule.getActions().size() != 1) { + errors.add("has " + rule.getActions().size() + " actions but exactly 1 is required"); + } else { + Action action = rule.getActions().getFirst(); + if (!action.getInputs().isEmpty()) { + errors.add("action '" + action.getId() + "' has mapped inputs"); + } else if (action.getConfiguration().get("type") instanceof String type) { + if (DSLRuleProvider.MIMETYPE_OPENHAB_DSL_RULE.equals(type)) { + if (!(action.getConfiguration().get("script") instanceof String)) { + errors.add("has no action script"); + } + if (action.getConfiguration().get(Module.SHARED_CONTEXT) instanceof Boolean shared + && shared.booleanValue()) { + errors.add("action '" + action.getId() + "' has shared context"); + } + } else { + errors.add("doesn't have a scripted DSL action"); + } + } else { + errors.add("doesn't have a scripted action"); + } + } + + if (errors.isEmpty()) { + result.add(new SerializabilityResult<>(rule.getUID(), true, "")); + } else { + result.add(new SerializabilityResult<>(rule.getUID(), false, + "Rule '" + rule.getUID() + "': " + String.join(", ", errors) + '.')); + } + } + + return result; + } + + @Override + public void setRulesToBeSerialized(String modelName, List rules, RuleSerializationOption option) + throws SerializationException { + if (rules.isEmpty()) { + return; + } + if (option != RuleSerializationOption.NORMAL) { + throw new SerializationException("DSL rules don't support serialization option '" + option + '\''); + } + List errors = null; + List> checks = checkSerializability(rules); + for (SerializabilityResult check : checks) { + if (!check.ok()) { + if (errors == null) { + errors = new ArrayList<>(); + } + errors.add(check.failureReason()); + } + } + if (errors != null) { + throw new SerializationException( + "Rule serialization attempt failed with:\n " + String.join("\n ", errors)); + } + + RuleModel model = RulesFactory.eINSTANCE.createRuleModel(); + + // Ensure that the variable collection is not null, calling get() creates an empty collection. + model.getVariables(); + + Set handledRules = new HashSet<>(); + for (Rule rule : rules) { + if (handledRules.contains(rule)) { + continue; + } + org.openhab.core.model.rule.rules.Rule modelRule = RulesFactory.eINSTANCE.createRule(); + model.getRules().add(modelRule); + try { + String placeholderUid = UUID.randomUUID().toString(); + String placeholderLiteral = SCRIPT_PLACEHOLDER_PREFIX + placeholderUid; + buildModelRule(rule, modelRule, placeholderLiteral, handledRules); + scriptElements.compute(modelName, (k, v) -> { + List r = v == null ? new ArrayList<>() : v; + if (rule.getActions().getFirst().getConfiguration().get("script") instanceof String script) { + r.add(new ScriptElement(placeholderUid, script)); + } else { + r.add(new ScriptElement(placeholderUid, "")); + } + return r; + }); + } catch (SerializationException e) { + logger.warn("Failed to serialize rule '{}': {}", rule.getUID(), e.getMessage()); + throw new SerializationException("Rule '" + rule.getUID() + "': " + e.getMessage(), e); + } + } + elementsToGenerate.put(modelName, model); + } + + @Override + public void generateFormat(String modelName, OutputStream out) { + RuleModel model = elementsToGenerate.remove(modelName); + if (model != null) { + if (logger.isDebugEnabled()) { + org.eclipse.emf.common.util.Diagnostic diagnostic = Diagnostician.INSTANCE.validate(model); + if (diagnostic.getSeverity() != org.eclipse.emf.common.util.Diagnostic.OK) { + for (org.eclipse.emf.common.util.Diagnostic child : diagnostic.getChildren()) { + logger.debug("Model Validation Error: {}", child.getMessage()); + } + } + } + + // Replace the placeholder with the actual script content + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + modelRepository.generateFileFormat(outputStream, "rules", model); + String generated = new String(outputStream.toByteArray(), StandardCharsets.UTF_8); + Matcher m = PLACEHOLDER_PATTERN.matcher(generated); + int safetyValve = 0; + List elements; + ScriptElement element; + String scriptContent; + while (m.find() && safetyValve < 1000) { + elements = scriptElements.get(modelName); + String uid = m.group("uid"); + if (uid != null && elements != null) { + element = elements.stream().filter(e -> uid.equals(e.placeholderLiteral)).findAny().orElse(null); + if (element != null) { + scriptContent = CONTEXT_COMMENT_PATTERN.matcher(element.scriptContent).replaceFirst(""); + scriptContent = INDENTATION_PATTERN.matcher(scriptContent).replaceAll("\t"); + if (!scriptContent.endsWith("\n")) { + scriptContent += '\n'; + } + generated = m.replaceFirst(scriptContent); + } + } + m = PLACEHOLDER_PATTERN.matcher(generated); + safetyValve++; + } + if (safetyValve >= 1000) { + logger.warn( + "Aborted replacing placeholders with script content to avoid endless loop, generated Rule DSL for '{}' will be invalid", + modelName); + } + + try { + out.write(generated.getBytes()); + } catch (IOException e) { + logger.warn("Exception when writing the generated syntax {}", e.getMessage()); + } + } + } + + @Override + public @Nullable String startParsingFormat(String syntax, List errors, List warnings) { + ByteArrayInputStream inputStream = new ByteArrayInputStream(syntax.getBytes(StandardCharsets.UTF_8)); + String result = modelRepository.createIsolatedModel("rules", inputStream, errors, warnings); + return result; + } + + @Override + public Collection getParsedObjects(String modelName) { + List result = new ArrayList<>(); + RuleBuilder builder; + List actions = new ArrayList<>(); + ActionBuilder aBuilder; + LinkedHashMap props; + Set usedUids = new HashSet<>(); + String fileNamePrefix = modelName.replace(".rules", "") + '-'; + String uid; + for (Rule rule : ruleProvider.getAllFromModel(modelName)) { + if ((uid = rule.getUID()).startsWith(fileNamePrefix) || usedUids.contains(uid)) { + builder = RuleBuilder.create(generateUid(rule, usedUids), rule); + } else { + usedUids.add(uid); + builder = RuleBuilder.create(uid, rule); + } + actions.clear(); + for (Action action : rule.getActions()) { + aBuilder = ActionBuilder.create(action); + props = new LinkedHashMap<>(action.getConfiguration().getProperties()); + if (props.get("script") instanceof String script) { + props.put("script", CONTEXT_COMMENT_PATTERN.matcher(script).replaceFirst("")); + } + aBuilder.withConfiguration(new Configuration(props)); + actions.add(aBuilder.build()); + } + builder.withActions(actions); + result.add(builder.build()); + } + return result; + } + + private String generateUid(Rule rule, Set usedUids) { + String result = rule.getName(); + if (result == null || result.isBlank()) { + result = "generated-1"; + } else { + result = result.trim().toLowerCase(Locale.ROOT).replaceAll("\\s+", "-"); + } + + Matcher matcher; + while (usedUids.contains(result)) { + matcher = INDEX_PATTERN.matcher(result); + if (matcher.find()) { + result = matcher.replaceFirst("-" + Integer.parseInt(matcher.group("idx")) + 1); + } else { + result += "-2"; + } + } + + usedUids.add(result); + return result; + } + + @Override + public void finishParsingFormat(String modelName) { + modelRepository.removeModel(modelName); + scriptElements.remove(modelName); + } + + private org.openhab.core.model.rule.rules.Rule buildModelRule(Rule rule, + org.openhab.core.model.rule.rules.Rule model, String placeholderLiteral, Set handledRules) + throws SerializationException { + model.setUid(rule.getUID()); + model.setName(rule.getName()); + EList tags = model.getTags(); + for (String tag : rule.getTags()) { + tags.add(tag); + } + + for (Condition condition : rule.getConditions()) { + model.getConditions().add(buildModelCondition(condition)); + } + + for (Trigger trigger : rule.getTriggers()) { + model.getEventtrigger().add(buildModelTrigger(trigger)); + } + + model.setScript(createPlaceholder(placeholderLiteral)); + + handledRules.add(rule); + + return model; + } + + private EventTrigger buildModelTrigger(Trigger trigger) throws SerializationException { + String type = trigger.getTypeUID(); + Object value; + RulesFactory factory = RulesFactory.eINSTANCE; + switch (type) { + case SystemTriggerHandler.STARTLEVEL_MODULE_TYPE_ID: + value = trigger.getConfiguration().get(SystemTriggerHandler.CFG_STARTLEVEL); + if (value instanceof Number num) { + int level = num.intValue(); + if (level == 40) { + return factory.createSystemOnStartupTrigger(); + } + SystemStartlevelTrigger result = factory.createSystemStartlevelTrigger(); + result.setLevel(level); + return result; + } + throw new SerializationException("Invalid trigger: " + trigger); + case ItemCommandTriggerHandler.MODULE_TYPE_ID: + value = trigger.getConfiguration().get(ItemCommandTriggerHandler.CFG_ITEMNAME); + if (value instanceof String str) { + CommandEventTrigger result = factory.createCommandEventTrigger(); + result.setItem(str); + value = trigger.getConfiguration().get(ItemCommandTriggerHandler.CFG_COMMAND); + if (value instanceof String command) { + result.setCommand(createValidCommand(command)); + } + return result; + } + throw new SerializationException("Invalid trigger: " + trigger); + case GroupCommandTriggerHandler.MODULE_TYPE_ID: + value = trigger.getConfiguration().get(GroupCommandTriggerHandler.CFG_GROUPNAME); + if (value instanceof String str) { + GroupMemberCommandEventTrigger result = factory.createGroupMemberCommandEventTrigger(); + result.setGroup(str); + value = trigger.getConfiguration().get(GroupCommandTriggerHandler.CFG_COMMAND); + if (value instanceof String command) { + result.setCommand(createValidCommand(command)); + } + return result; + } + throw new SerializationException("Invalid trigger: " + trigger); + case ItemStateTriggerHandler.UPDATE_MODULE_TYPE_ID: + value = trigger.getConfiguration().get(ItemStateTriggerHandler.CFG_ITEMNAME); + if (value instanceof String str) { + UpdateEventTrigger result = factory.createUpdateEventTrigger(); + result.setItem(str); + value = trigger.getConfiguration().get(ItemStateTriggerHandler.CFG_STATE); + if (value instanceof String state) { + result.setState(createValidState(state)); + } + return result; + } + throw new SerializationException("Invalid trigger: " + trigger); + case ItemStateTriggerHandler.CHANGE_MODULE_TYPE_ID: + value = trigger.getConfiguration().get(ItemStateTriggerHandler.CFG_ITEMNAME); + if (value instanceof String str) { + ChangedEventTrigger result = factory.createChangedEventTrigger(); + result.setItem(str); + value = trigger.getConfiguration().get(ItemStateTriggerHandler.CFG_STATE); + if (value instanceof String state) { + result.setNewState(createValidState(state)); + } + value = trigger.getConfiguration().get(ItemStateTriggerHandler.CFG_PREVIOUS_STATE); + if (value instanceof String state) { + result.setOldState(createValidState(state)); + } + return result; + } + throw new SerializationException("Invalid trigger: " + trigger); + case GroupStateTriggerHandler.UPDATE_MODULE_TYPE_ID: + value = trigger.getConfiguration().get(GroupStateTriggerHandler.CFG_GROUPNAME); + if (value instanceof String str) { + GroupMemberUpdateEventTrigger result = factory.createGroupMemberUpdateEventTrigger(); + result.setGroup(str); + value = trigger.getConfiguration().get(GroupStateTriggerHandler.CFG_STATE); + if (value instanceof String state) { + result.setState(createValidState(state)); + } + return result; + } + throw new SerializationException("Invalid trigger: " + trigger); + case GroupStateTriggerHandler.CHANGE_MODULE_TYPE_ID: + value = trigger.getConfiguration().get(GroupStateTriggerHandler.CFG_GROUPNAME); + if (value instanceof String str) { + GroupMemberChangedEventTrigger result = factory.createGroupMemberChangedEventTrigger(); + result.setGroup(str); + value = trigger.getConfiguration().get(GroupStateTriggerHandler.CFG_STATE); + if (value instanceof String state) { + result.setNewState(createValidState(state)); + } + value = trigger.getConfiguration().get(GroupStateTriggerHandler.CFG_PREVIOUS_STATE); + if (value instanceof String state) { + result.setOldState(createValidState(state)); + } + return result; + } + throw new SerializationException("Invalid trigger: " + trigger); + case GenericCronTriggerHandler.MODULE_TYPE_ID: + value = trigger.getConfiguration().get(GenericCronTriggerHandler.CFG_CRON_EXPRESSION); + if (value instanceof String str) { + TimerTrigger result = factory.createTimerTrigger(); + if ("0 0 12 * * ?".equals(str)) { + result.setTime("noon"); + } else if ("0 0 0 * * ?".equals(str)) { + result.setTime("midnight"); + } else { + result.setCron(str); + } + return result; + } + throw new SerializationException("Invalid trigger: " + trigger); + case TimeOfDayTriggerHandler.MODULE_TYPE_ID: + value = trigger.getConfiguration().get(TimeOfDayTriggerHandler.CFG_TIME); + if (value instanceof String str) { + TimerTrigger result = factory.createTimerTrigger(); + Matcher m = TIME_PATTERN.matcher(str); + String time = m.find() && m.groupCount() > 1 && m.group(1).length() < 2 ? '0' + str : str; + if ("12:00".equals(time)) { + result.setTime("noon"); + } else if ("00:00".equals(time)) { + result.setTime("midnight"); + } else { + result.setTime(time); + } + return result; + } + throw new SerializationException("Invalid trigger: " + trigger); + case DateTimeTriggerHandler.MODULE_TYPE_ID: + value = trigger.getConfiguration().get(DateTimeTriggerHandler.CONFIG_ITEM_NAME); + if (value instanceof String str) { + DateTimeTrigger result = factory.createDateTimeTrigger(); + result.setItem(str); + value = trigger.getConfiguration().get(DateTimeTriggerHandler.CONFIG_TIME_ONLY); + if (value instanceof Boolean timeOnly) { + result.setTimeOnly(timeOnly); + } + value = trigger.getConfiguration().get(DateTimeTriggerHandler.CONFIG_OFFSET); + if (value instanceof String offset) { + result.setOffset(offset); + } + return result; + } + throw new SerializationException("Invalid trigger: " + trigger); + case ChannelEventTriggerHandler.MODULE_TYPE_ID: + value = trigger.getConfiguration().get(ChannelEventTriggerHandler.CFG_CHANNEL); + if (value instanceof String str) { + EventEmittedTrigger result = factory.createEventEmittedTrigger(); + result.setChannel(str); + value = trigger.getConfiguration().get(ChannelEventTriggerHandler.CFG_CHANNEL_EVENT); + if (value instanceof String event) { + ValidTrigger trg = factory.createValidTrigger(); + trg.setValue(event); + result.setTrigger(trg); + } + return result; + } + throw new SerializationException("Invalid trigger: " + trigger); + case ThingStatusTriggerHandler.UPDATE_MODULE_TYPE_ID: + value = trigger.getConfiguration().get(ThingStatusTriggerHandler.CFG_THING_UID); + if (value instanceof String str) { + ThingStateUpdateEventTrigger result = factory.createThingStateUpdateEventTrigger(); + result.setThing(str); + value = trigger.getConfiguration().get(ThingStatusTriggerHandler.CFG_STATUS); + if (value instanceof String status) { + result.setState(status); + } + return result; + } + throw new SerializationException("Invalid trigger: " + trigger); + case ThingStatusTriggerHandler.CHANGE_MODULE_TYPE_ID: + value = trigger.getConfiguration().get(ThingStatusTriggerHandler.CFG_THING_UID); + if (value instanceof String str) { + ThingStateChangedEventTrigger result = factory.createThingStateChangedEventTrigger(); + result.setThing(str); + value = trigger.getConfiguration().get(ThingStatusTriggerHandler.CFG_STATUS); + if (value instanceof String status) { + result.setNewState(status); + value = trigger.getConfiguration().get(ThingStatusTriggerHandler.CFG_PREVIOUS_STATUS); + if (value instanceof String previousStatus) { + result.setOldState(previousStatus); + } + } + return result; + } + throw new SerializationException("Invalid trigger: " + trigger); + default: + throw new SerializationException("Unsupported trigger: " + trigger); + } + } + + private org.openhab.core.model.rule.rules.Condition buildModelCondition(Condition condition) + throws SerializationException { + String type = condition.getTypeUID(); + Object value; + RulesFactory factory = RulesFactory.eINSTANCE; + int i; + + switch (type) { + case TimeOfDayConditionHandler.MODULE_TYPE_ID: + TimeOfDayCondition todCond = factory.createTimeOfDayCondition(); + value = condition.getConfiguration().get(TimeOfDayConditionHandler.CFG_START_TIME); + if (value instanceof String start) { + Matcher m = TIME_PATTERN.matcher(start); + String time = m.find() && m.groupCount() > 1 && m.group(1).length() < 2 ? '0' + start : start; + todCond.setStart(time); + } + value = condition.getConfiguration().get(TimeOfDayConditionHandler.CFG_END_TIME); + if (value instanceof String end) { + Matcher m = TIME_PATTERN.matcher(end); + String time = m.find() && m.groupCount() > 1 && m.group(1).length() < 2 ? '0' + end : end; + todCond.setEnd(time); + } + return todCond; + case DayOfWeekConditionHandler.MODULE_TYPE_ID: + DayOfWeekCondition dowCond = factory.createDayOfWeekCondition(); + value = condition.getConfiguration().get(DayOfWeekConditionHandler.CFG_DAYS); + if (value == null) { + value = List.of(); + } + if (value instanceof List weekDays) { + EList eWeekDays = dowCond.getWeekDays(); + for (Object dayObject : weekDays) { + if (dayObject instanceof String day) { + switch (day) { + case "MON": + eWeekDays.add("Monday"); + break; + case "TUE": + eWeekDays.add("Tuesday"); + break; + case "WED": + eWeekDays.add("Wednesday"); + break; + case "THU": + eWeekDays.add("Thursday"); + break; + case "FRI": + eWeekDays.add("Friday"); + break; + case "SAT": + eWeekDays.add("Saturday"); + break; + case "SUN": + eWeekDays.add("Sunday"); + break; + default: + throw new SerializationException("Invalid condition: " + condition); + } + } else { + throw new SerializationException("Invalid condition: " + condition); + } + } + } else { + throw new SerializationException("Invalid condition: " + condition); + } + return dowCond; + case EphemerisConditionHandler.WEEKDAY_MODULE_TYPE_ID: + WeekdayCondition wdCond = factory.createWeekdayCondition(); + wdCond.setType("weekday"); + value = condition.getConfiguration().get("offset"); + if (value instanceof Number offset) { + if ((i = offset.intValue()) != 0) { + wdCond.setOffset(Integer.toString(i)); + } + } else if (value != null) { + throw new SerializationException("Invalid condition: " + condition); + } + return wdCond; + case EphemerisConditionHandler.WEEKEND_MODULE_TYPE_ID: + WeekdayCondition weCond = factory.createWeekdayCondition(); + weCond.setType("weekend"); + value = condition.getConfiguration().get("offset"); + if (value instanceof Number offset) { + if ((i = offset.intValue()) != 0) { + weCond.setOffset(Integer.toString(i)); + } + } else if (value != null) { + throw new SerializationException("Invalid condition: " + condition); + } + return weCond; + case EphemerisConditionHandler.HOLIDAY_MODULE_TYPE_ID: + HolidayCondition hdCond = factory.createHolidayCondition(); + hdCond.setHoliday("holiday"); + hdCond.setNegation(false); + value = condition.getConfiguration().get("offset"); + if (value instanceof Number offset) { + if ((i = offset.intValue()) != 0) { + hdCond.setOffset(Integer.toString(i)); + } + } else if (value != null) { + throw new SerializationException("Invalid condition: " + condition); + } + return hdCond; + case EphemerisConditionHandler.NOT_HOLIDAY_MODULE_TYPE_ID: + HolidayCondition nhdCond = factory.createHolidayCondition(); + nhdCond.setHoliday("holiday"); + nhdCond.setNegation(true); + value = condition.getConfiguration().get("offset"); + if (value instanceof Number offset) { + if ((i = offset.intValue()) != 0) { + nhdCond.setOffset(Integer.toString(i)); + } + } else if (value != null) { + throw new SerializationException("Invalid condition: " + condition); + } + return nhdCond; + case EphemerisConditionHandler.DAYSET_MODULE_TYPE_ID: + InDaysetCondition idsCond = factory.createInDaysetCondition(); + value = condition.getConfiguration().get("dayset"); + if (value instanceof String dayset) { + idsCond.setDayset(dayset); + } else { + throw new SerializationException("Invalid condition: " + condition); + } + value = condition.getConfiguration().get("offset"); + if (value instanceof Number offset) { + if ((i = offset.intValue()) != 0) { + idsCond.setOffset(Integer.toString(i)); + } + } else if (value != null) { + throw new SerializationException("Invalid condition: " + condition); + } + return idsCond; + case IntervalConditionHandler.MODULE_TYPE_ID: + IntervalCondition ivCond = factory.createIntervalCondition(); + value = condition.getConfiguration().get(IntervalConditionHandler.CFG_MIN_INTERVAL); + if (value instanceof Number interval) { + ivCond.setInterval(interval.intValue()); + } else { + throw new SerializationException("Invalid condition: " + condition); + } + return ivCond; + case ThingStatusConditionHandler.THING_STATUS_CONDITION: + ThingStatusCondition tsCond = factory.createThingStatusCondition(); + value = condition.getConfiguration().get(ThingStatusConditionHandler.CFG_THING_UID); + if (value instanceof String thingUid) { + tsCond.setThing(thingUid); + } else { + throw new SerializationException("Invalid condition: " + condition); + } + value = condition.getConfiguration().get(ThingStatusConditionHandler.CFG_STATUS); + if (value instanceof String status) { + tsCond.setStatus(status); + } else { + throw new SerializationException("Invalid condition: " + condition); + } + value = condition.getConfiguration().get(ThingStatusConditionHandler.CFG_OPERATOR); + tsCond.setNegation(value instanceof String op && "!=".equals(op)); + return tsCond; + case ItemStateConditionHandler.ITEM_STATE_CONDITION: + ItemStateCondition isCond = factory.createItemStateCondition(); + value = condition.getConfiguration().get(ItemStateConditionHandler.ITEM_NAME); + if (value instanceof String itemName) { + isCond.setItem(itemName); + } else { + throw new SerializationException("Invalid condition: " + condition); + } + value = condition.getConfiguration().get(ItemStateConditionHandler.STATE); + if (value instanceof String state) { + isCond.setState(createValidState(state)); + } else { + throw new SerializationException("Invalid condition: " + condition); + } + value = condition.getConfiguration().get(ItemStateConditionHandler.OPERATOR); + isCond.setOperator(value instanceof String op && !op.isBlank() ? op : "="); + return isCond; + default: + throw new SerializationException("Unsupported condition: " + condition); + } + } + + private ValidState createValidState(String stateValue) { + ValidState result; + if (NUMERIC_PATTERN.matcher(stateValue).matches()) { + result = RulesFactory.eINSTANCE.createValidStateNumber(); + } else if (enumStates.contains(stateValue)) { + result = RulesFactory.eINSTANCE.createValidStateId(); + } else { + result = RulesFactory.eINSTANCE.createValidStateString(); + } + result.setValue(stateValue); + return result; + } + + private ValidCommand createValidCommand(String commandValue) { + ValidCommand result; + if (NUMERIC_PATTERN.matcher(commandValue).matches()) { + result = RulesFactory.eINSTANCE.createValidCommandNumber(); + } else if (enumCommands.contains(commandValue)) { + result = RulesFactory.eINSTANCE.createValidCommandId(); + } else { + result = RulesFactory.eINSTANCE.createValidCommandString(); + } + result.setValue(commandValue); + return result; + } + + private XBlockExpression createPlaceholder(String placeholderLiteral) { + // Creates expression: 'val placeholder=""' + XbaseFactory factory = XbaseFactory.eINSTANCE; + XBlockExpression result = factory.createXBlockExpression(); + XVariableDeclaration varDecl = factory.createXVariableDeclaration(); + varDecl.setName("placeholder"); + XStringLiteral stringLit = factory.createXStringLiteral(); + stringLit.setValue(placeholderLiteral); + varDecl.setRight(stringLit); + result.getExpressions().add(varDecl); + return result; + } +} diff --git a/bundles/org.openhab.core.model.rule/bnd.bnd b/bundles/org.openhab.core.model.rule/bnd.bnd index f5f9e5cb0..cf6f6868a 100644 --- a/bundles/org.openhab.core.model.rule/bnd.bnd +++ b/bundles/org.openhab.core.model.rule/bnd.bnd @@ -13,6 +13,7 @@ Export-Package: org.openhab.core.model.rule,\ org.openhab.core.model.rule.validation Import-Package: \ org.openhab.core.automation,\ + org.openhab.core.automation.converter,\ org.openhab.core.automation.module.script.rulesupport.shared,\ org.openhab.core.automation.util,\ org.openhab.core.common,\ diff --git a/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/RulesRuntimeModule.xtend b/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/RulesRuntimeModule.xtend index abec2525b..98a992162 100644 --- a/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/RulesRuntimeModule.xtend +++ b/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/RulesRuntimeModule.xtend @@ -37,6 +37,8 @@ import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider import org.eclipse.xtext.xbase.interpreter.IExpressionInterpreter import org.eclipse.xtext.xbase.scoping.batch.ImplicitlyImportedFeatures import org.eclipse.xtext.xbase.typesystem.computation.ITypeComputer +import org.eclipse.xtext.formatting.IFormatter +import org.openhab.core.model.rule.formatting.RulesFormatter /** * Use this class to register components to be used at runtime / without the Equinox extension registry. @@ -56,6 +58,10 @@ import org.eclipse.xtext.xbase.typesystem.computation.ITypeComputer return ScriptImplicitlyImportedTypes } + override Class bindIFormatter() { + return RulesFormatter; + } + override Class bindIGenerator() { return NullGenerator } diff --git a/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/formatting/RuleFormattingConfigBasedStream.java b/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/formatting/RuleFormattingConfigBasedStream.java new file mode 100644 index 000000000..dd98c5cfb --- /dev/null +++ b/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/formatting/RuleFormattingConfigBasedStream.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2010-2026 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.model.rule.formatting; + +import java.util.Set; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.xtext.formatting.IElementMatcherProvider.IElementMatcher; +import org.eclipse.xtext.formatting.impl.AbstractFormattingConfig.ElementLocator; +import org.eclipse.xtext.formatting.impl.AbstractFormattingConfig.ElementPattern; +import org.eclipse.xtext.formatting.impl.FormattingConfig; +import org.eclipse.xtext.formatting.impl.FormattingConfigBasedStream; +import org.eclipse.xtext.parsetree.reconstr.IHiddenTokenHelper; +import org.eclipse.xtext.parsetree.reconstr.ITokenStream; + +/** + * This class exists only to override indentation logic. + * + * @author Ravi Nadahar - Initial contribution + */ +@NonNullByDefault +public class RuleFormattingConfigBasedStream extends FormattingConfigBasedStream { + + public RuleFormattingConfigBasedStream(@Nullable ITokenStream out, @Nullable String initialIndentation, + FormattingConfig cfg, IElementMatcher<@Nullable ElementPattern> matcher, + IHiddenTokenHelper hiddenTokenHelper, boolean preserveSpaces) { + super(out, initialIndentation, cfg, matcher, hiddenTokenHelper, preserveSpaces); + } + + @Override + protected @NonNullByDefault({}) Set collectLocators(EObject ele) { + + Set<@Nullable ElementLocator> result = super.collectLocators(ele); + if (indentationLevel < 0) { + indentationLevel = 0; + } + return result; + } +} diff --git a/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/formatting/RulesFormatter.java b/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/formatting/RulesFormatter.java new file mode 100644 index 000000000..069536abf --- /dev/null +++ b/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/formatting/RulesFormatter.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2010-2026 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.model.rule.formatting; + +import java.util.List; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.xtext.Keyword; +import org.eclipse.xtext.formatting.impl.AbstractDeclarativeFormatter; +import org.eclipse.xtext.formatting.impl.AbstractFormattingConfig; +import org.eclipse.xtext.formatting.impl.FormattingConfig; +import org.eclipse.xtext.parsetree.reconstr.ITokenStream; +import org.eclipse.xtext.util.Pair; +import org.eclipse.xtext.xbase.lib.Extension; +import org.eclipse.xtext.xtext.XtextFormatter; +import org.openhab.core.model.rule.services.RulesGrammarAccess; + +import com.google.inject.Inject; + +/** + * This class contains custom formatting description. + * + * see : http://www.eclipse.org/Xtext/documentation.html#formatting + * on how and when to use it + * + * Also see {@link XtextFormatter} as an example + * + * @author Ravi Nadahar - Initial contribution + */ +@NonNullByDefault +public class RulesFormatter extends AbstractDeclarativeFormatter { + + @Inject + @Extension + private RulesGrammarAccess rulesGrammarAccess; + + @Override + protected void configureFormatting(@NonNullByDefault({}) FormattingConfig c) { + c.setLinewrap(1, 1, 2).before(rulesGrammarAccess.getRuleModelRule()); + c.setLinewrap(1, 1, 2).after(rulesGrammarAccess.getRuleModelRule()); + c.setLinewrap(1, 1, 2).after(rulesGrammarAccess.getXImportDeclarationRule()); + c.setLinewrap(1, 1, 2).after(rulesGrammarAccess.getXFunctionTypeRefRule()); + c.setLinewrap(1, 1, 2).after(rulesGrammarAccess.getXBlockExpressionRule()); + c.setLinewrap(1, 2, 2).before(rulesGrammarAccess.getRuleAccess().getRuleKeyword_0()); + c.setLinewrap(1, 1, 2).after(rulesGrammarAccess.getRuleAccess().getOrKeyword_4_2_0()); + c.setLinewrap(1, 1, 2).after(rulesGrammarAccess.getRuleAccess().getAndKeyword_5_4_0()); + c.setLinewrap(1, 1, 2).before(rulesGrammarAccess.getRuleAccess().getWhenKeyword_4_0()); + c.setLinewrap(1, 1, 2).after(rulesGrammarAccess.getRuleAccess().getWhenKeyword_4_0()); + c.setLinewrap(1, 1, 2).before(rulesGrammarAccess.getRuleAccess().getButKeyword_5_0()); + c.setLinewrap(1, 1, 2).after(rulesGrammarAccess.getRuleAccess().getIfKeyword_5_2()); + c.setLinewrap(1, 1, 2).before(rulesGrammarAccess.getRuleAccess().getThenKeyword_6()); + c.setLinewrap(1, 1, 2).after(rulesGrammarAccess.getRuleAccess().getThenKeyword_6()); + c.setLinewrap(1, 1, 2).before(rulesGrammarAccess.getRuleAccess().getEndKeyword_8()); + + after(c.setIndentationIncrement(), "{"); + before(c.setIndentationDecrement(), "}"); + c.setIndentationIncrement().after(rulesGrammarAccess.getRuleAccess().getWhenKeyword_4_0()); + c.setIndentationDecrement().before(rulesGrammarAccess.getRuleAccess().getButKeyword_5_0()); + c.setIndentationIncrement().after(rulesGrammarAccess.getRuleAccess().getIfKeyword_5_2()); + c.setIndentationDecrement().before(rulesGrammarAccess.getRuleAccess().getThenKeyword_6()); + c.setIndentationIncrement().after(rulesGrammarAccess.getRuleAccess().getThenKeyword_6()); + c.setIndentationDecrement().before(rulesGrammarAccess.getRuleAccess().getEndKeyword_8()); + + before(c.setLinewrap(), "}"); + + withinKeywordPairs(c.setNoSpace(), "[", "]"); + around(c.setNoSpace(), "=", ":"); + before(c.setNoSpace(), ","); + + c.setAutoLinewrap(120); + + c.setLinewrap(0, 1, 2).before(rulesGrammarAccess.getSL_COMMENTRule()); + c.setLinewrap(0, 1, 2).before(rulesGrammarAccess.getML_COMMENTRule()); + c.setLinewrap(0, 1, 1).after(rulesGrammarAccess.getML_COMMENTRule()); + } + + public void withinKeywordPairs(FormattingConfig.NoSpaceLocator locator, String leftKW, String rightKW) { + List> keywordPairs = rulesGrammarAccess.findKeywordPairs(leftKW, rightKW); + for (Pair pair : keywordPairs) { + { + locator.after(pair.getFirst()); + locator.before(pair.getSecond()); + } + } + } + + public void around(final AbstractFormattingConfig.ElementLocator locator, String... listKW) { + List keywords = rulesGrammarAccess.findKeywords(listKW); + for (Keyword keyword : keywords) { + locator.around(keyword); + } + } + + public void after(final AbstractFormattingConfig.ElementLocator locator, String... listKW) { + List keywords = rulesGrammarAccess.findKeywords(listKW); + for (Keyword keyword : keywords) { + locator.after(keyword); + } + } + + public void before(AbstractFormattingConfig.ElementLocator locator, String... listKW) { + List keywords = rulesGrammarAccess.findKeywords(listKW); + for (Keyword keyword : keywords) { + locator.before(keyword); + } + } + + @Override + public ITokenStream createFormatterStream(@Nullable String indent, @Nullable ITokenStream out, + boolean preserveWhitespaces) { + return new RuleFormattingConfigBasedStream(out, indent, getConfig(), createMatcher(), getHiddenTokenHelper(), + preserveWhitespaces); + } + + @Override + public ITokenStream createFormatterStream(@Nullable EObject context, @Nullable String indent, + @Nullable ITokenStream out, boolean preserveWhitespaces) { + + // We have no choice but to call super() and then ignore the returned ITokenStream. The reason is that + // contextResourceURI is private, so we can't set it from here, making the call to super() required. But, + // we don't want the returned instance because it's of the wrong type, so we have to create another one. + + super.createFormatterStream(context, indent, out, preserveWhitespaces); + + return new RuleFormattingConfigBasedStream(out, indent, getConfig(), createMatcher(), getHiddenTokenHelper(), + preserveWhitespaces); + } +} diff --git a/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/formatting/RulesFormatter.xtend b/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/formatting/RulesFormatter.xtend deleted file mode 100644 index 71ec9479f..000000000 --- a/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/formatting/RulesFormatter.xtend +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2010-2026 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -/* - * generated by Xtext - */ -package org.openhab.core.model.rule.formatting - -import org.eclipse.xtext.formatting.impl.AbstractDeclarativeFormatter -import org.eclipse.xtext.formatting.impl.FormattingConfig -// import com.google.inject.Inject; -// import org.openhab.core.model.rule.services.RulesGrammarAccess - -/** - * This class contains custom formatting description. - * - * see : http://www.eclipse.org/Xtext/documentation.html#formatting - * on how and when to use it - * - * Also see {@link org.eclipse.xtext.xtext.XtextFormatter} as an example - */ -class RulesFormatter extends AbstractDeclarativeFormatter { - -// @Inject extension RulesGrammarAccess - - override protected void configureFormatting(FormattingConfig c) { -// It's usually a good idea to activate the following three statements. -// They will add and preserve newlines around comments -// c.setLinewrap(0, 1, 2).before(SL_COMMENTRule) -// c.setLinewrap(0, 1, 2).before(ML_COMMENTRule) -// c.setLinewrap(0, 1, 1).after(ML_COMMENTRule) - } -} diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/config/YamlConfigDescriptionDTO.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/config/YamlConfigDescriptionDTO.java index 6efeaa668..7813617e3 100644 --- a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/config/YamlConfigDescriptionDTO.java +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/config/YamlConfigDescriptionDTO.java @@ -59,14 +59,15 @@ public class YamlConfigDescriptionDTO { * Creates a new instance based on the specified {@link ConfigDescription}. * * @param configDescription the {@link ConfigDescription}. + * @param includeDefault whether boolean values with the default value should be included. */ - public YamlConfigDescriptionDTO(@NonNull ConfigDescription configDescription) { + public YamlConfigDescriptionDTO(@NonNull ConfigDescription configDescription, boolean includeDefault) { this.uri = toDecodedString(configDescription.getUID()); List<@NonNull ConfigDescriptionParameter> fromParams = configDescription.getParameters(); if (!fromParams.isEmpty()) { Map params = new LinkedHashMap<>(); for (ConfigDescriptionParameter param : fromParams) { - params.put(param.getName(), new YamlConfigDescriptionParameterDTO(param)); + params.put(param.getName(), new YamlConfigDescriptionParameterDTO(param, includeDefault)); } this.params = params; } @@ -74,7 +75,8 @@ public class YamlConfigDescriptionDTO { if (!fromParamGroups.isEmpty()) { Map paramGroups = new LinkedHashMap<>(); for (ConfigDescriptionParameterGroup paramGroup : fromParamGroups) { - paramGroups.put(paramGroup.getName(), new YamlConfigDescriptionParameterGroupDTO(paramGroup)); + paramGroups.put(paramGroup.getName(), + new YamlConfigDescriptionParameterGroupDTO(paramGroup, includeDefault)); } this.paramGroups = paramGroups; } diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/config/YamlConfigDescriptionParameterDTO.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/config/YamlConfigDescriptionParameterDTO.java index 46188340c..18fe3ff4e 100644 --- a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/config/YamlConfigDescriptionParameterDTO.java +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/config/YamlConfigDescriptionParameterDTO.java @@ -39,13 +39,20 @@ import com.fasterxml.jackson.annotation.JsonProperty; */ public class YamlConfigDescriptionParameterDTO { + private static final Boolean REQUIRED_DEFAULT = Boolean.FALSE; + private static final Boolean READ_ONLY_DEFAULT = Boolean.FALSE; + private static final Boolean MULTIPLE_DEFAULT = Boolean.FALSE; + private static final Boolean LIMIT_TO_OPTIONS_DEFAULT = Boolean.TRUE; + private static final Boolean ADVANCED_DEFAULT = Boolean.FALSE; + private static final Boolean VERIFY_DEFAULT = Boolean.FALSE; + public String context; @JsonProperty("default") @JsonAlias("defaultValue") public String defaultValue; public String description; public String label; - public boolean required; + public Boolean required; public Type type; public BigDecimal min; public BigDecimal max; @@ -75,17 +82,27 @@ public class YamlConfigDescriptionParameterDTO { * Creates a new instance based on the specified {@link ConfigDescriptionParameter}. * * @param parameter the {@link ConfigDescriptionParameter}. + * @param includeDefault whether boolean values with the default value should be included. */ - public YamlConfigDescriptionParameterDTO(@NonNull ConfigDescriptionParameter parameter) { + public YamlConfigDescriptionParameterDTO(@NonNull ConfigDescriptionParameter parameter, boolean includeDefault) { this.type = parameter.getType(); this.min = parameter.getMinimum(); this.max = parameter.getMaximum(); this.step = parameter.getStepSize(); this.pattern = parameter.getPattern(); this.readOnly = parameter.isReadOnly(); + if (!includeDefault && READ_ONLY_DEFAULT.equals(this.readOnly)) { + this.readOnly = null; + } this.multiple = parameter.isMultiple(); + if (!includeDefault && MULTIPLE_DEFAULT.equals(this.multiple)) { + this.multiple = null; + } this.context = parameter.getContext(); this.required = parameter.isRequired(); + if (!includeDefault && REQUIRED_DEFAULT.equals(this.required)) { + this.required = null; + } this.defaultValue = parameter.getDefault(); this.label = parameter.getLabel(); this.description = parameter.getDescription(); @@ -107,11 +124,20 @@ public class YamlConfigDescriptionParameterDTO { } this.groupName = parameter.getGroupName(); this.advanced = parameter.isAdvanced(); + if (!includeDefault && ADVANCED_DEFAULT.equals(this.advanced)) { + this.advanced = null; + } this.limitToOptions = parameter.getLimitToOptions(); + if (!includeDefault && LIMIT_TO_OPTIONS_DEFAULT.equals(this.limitToOptions)) { + this.limitToOptions = null; + } this.multipleLimit = parameter.getMultipleLimit(); this.unit = parameter.getUnit(); this.unitLabel = parameter.getUnitLabel(); this.verify = parameter.isVerifyable(); + if (!includeDefault && VERIFY_DEFAULT.equals(this.verify)) { + this.verify = null; + } } @Override @@ -137,7 +163,7 @@ public class YamlConfigDescriptionParameterDTO { && Objects.equals(max, other.max) && Objects.equals(min, other.min) && Objects.equals(multiple, other.multiple) && Objects.equals(multipleLimit, other.multipleLimit) && Objects.equals(options, other.options) && Objects.equals(pattern, other.pattern) - && Objects.equals(readOnly, other.readOnly) && required == other.required + && Objects.equals(readOnly, other.readOnly) && Objects.equals(required, other.required) && Objects.equals(step, other.step) && type == other.type && Objects.equals(unit, other.unit) && Objects.equals(unitLabel, other.unitLabel) && Objects.equals(verify, other.verify); } @@ -158,7 +184,9 @@ public class YamlConfigDescriptionParameterDTO { if (label != null) { builder.append("label=").append(label).append(", "); } - builder.append("required=").append(required).append(", "); + if (required != null) { + builder.append("required=").append(required).append(", "); + } if (type != null) { builder.append("type=").append(type).append(", "); } diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/config/YamlConfigDescriptionParameterGroupDTO.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/config/YamlConfigDescriptionParameterGroupDTO.java index d2ccdf1f7..123f71eec 100644 --- a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/config/YamlConfigDescriptionParameterGroupDTO.java +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/config/YamlConfigDescriptionParameterGroupDTO.java @@ -27,6 +27,8 @@ import org.openhab.core.config.core.dto.ConfigDescriptionParameterGroupDTO; */ public class YamlConfigDescriptionParameterGroupDTO { + private static final Boolean ADVANCED_DEFAULT = Boolean.FALSE; + public String context; public Boolean advanced; public String label; @@ -42,10 +44,15 @@ public class YamlConfigDescriptionParameterGroupDTO { * Creates a new instance based on the specified {@link ConfigDescriptionParameterGroup}. * * @param parameterGroup the {@link ConfigDescriptionParameterGroup}. + * @param includeDefault whether boolean values with the default value should be included. */ - public YamlConfigDescriptionParameterGroupDTO(@NonNull ConfigDescriptionParameterGroup parameterGroup) { + public YamlConfigDescriptionParameterGroupDTO(@NonNull ConfigDescriptionParameterGroup parameterGroup, + boolean includeDefault) { this.context = parameterGroup.getContext(); this.advanced = parameterGroup.isAdvanced(); + if (!includeDefault && ADVANCED_DEFAULT.equals(this.advanced)) { + this.advanced = null; + } this.label = parameterGroup.getLabel(); this.description = parameterGroup.getDescription(); } diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/ModuleTypeAliases.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/ModuleTypeAliases.java index 4a0a41e15..2a1bdd91b 100644 --- a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/ModuleTypeAliases.java +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/ModuleTypeAliases.java @@ -26,6 +26,7 @@ import org.openhab.core.automation.internal.module.handler.DateTimeTriggerHandle import org.openhab.core.automation.internal.module.handler.DayOfWeekConditionHandler; import org.openhab.core.automation.internal.module.handler.EphemerisConditionHandler; import org.openhab.core.automation.internal.module.handler.GenericCronTriggerHandler; +import org.openhab.core.automation.internal.module.handler.GenericEventTriggerHandler; import org.openhab.core.automation.internal.module.handler.GroupCommandTriggerHandler; import org.openhab.core.automation.internal.module.handler.GroupStateTriggerHandler; import org.openhab.core.automation.internal.module.handler.IntervalConditionHandler; @@ -82,6 +83,7 @@ public class ModuleTypeAliases { { "T", "ChannelEvent", ChannelEventTriggerHandler.MODULE_TYPE_ID }, // { "T", "Cron", GenericCronTriggerHandler.MODULE_TYPE_ID }, // { "T", "DateTime", DateTimeTriggerHandler.MODULE_TYPE_ID }, // + { "T", "GenericEvent", GenericEventTriggerHandler.MODULE_TYPE_ID }, // { "T", "MemberReceivedCommand", GroupCommandTriggerHandler.MODULE_TYPE_ID }, // { "T", "MemberChanged", GroupStateTriggerHandler.CHANGE_MODULE_TYPE_ID }, // { "T", "MemberUpdated", GroupStateTriggerHandler.UPDATE_MODULE_TYPE_ID }, // diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlActionDTO.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlActionDTO.java index 9c34fd546..b7d62881c 100644 --- a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlActionDTO.java +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlActionDTO.java @@ -17,6 +17,7 @@ import java.util.Objects; import org.eclipse.jdt.annotation.NonNull; import org.openhab.core.automation.Action; +import org.openhab.core.automation.converter.RuleSerializer.RuleSerializationOption; /** * The {@link YamlActionDTO} is a data transfer object used to serialize an action in a YAML configuration file. @@ -31,8 +32,15 @@ public class YamlActionDTO extends YamlModuleDTO { } public YamlActionDTO(@NonNull Action action) { - super(action); + this(action, RuleSerializationOption.NORMAL); + } + + public YamlActionDTO(@NonNull Action action, RuleSerializationOption option) { + super(action, option); this.inputs = action.getInputs(); + if (option != RuleSerializationOption.INCLUDE_ALL && this.inputs.isEmpty()) { + this.inputs = null; + } } @Override @@ -62,23 +70,23 @@ public class YamlActionDTO extends YamlModuleDTO { public String toString() { StringBuilder builder = new StringBuilder(getClass().getSimpleName()); builder.append(" ["); - if (inputs != null) { - builder.append("inputs=").append(inputs).append(", "); - } if (id != null) { - builder.append("id=").append(id).append(", "); + builder.append("id=").append(id); + } + if (inputs != null) { + builder.append(", inputs=").append(inputs); } if (type != null) { - builder.append("type=").append(type).append(", "); + builder.append(", type=").append(type); } if (label != null) { - builder.append("label=").append(label).append(", "); + builder.append(", label=").append(label); } if (description != null) { - builder.append("description=").append(description).append(", "); + builder.append(", description=").append(description); } if (config != null) { - builder.append("config=").append(config); + builder.append(", config=").append(config); } builder.append("]"); return builder.toString(); diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlConditionDTO.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlConditionDTO.java index 48c58cf6c..229df6441 100644 --- a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlConditionDTO.java +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlConditionDTO.java @@ -17,6 +17,7 @@ import java.util.Objects; import org.eclipse.jdt.annotation.NonNull; import org.openhab.core.automation.Condition; +import org.openhab.core.automation.converter.RuleSerializer.RuleSerializationOption; /** * The {@link YamlConditionDTO} is a data transfer object used to serialize a condition in a YAML configuration file. @@ -31,8 +32,15 @@ public class YamlConditionDTO extends YamlModuleDTO { } public YamlConditionDTO(@NonNull Condition condition) { - super(condition); + this(condition, RuleSerializationOption.NORMAL); + } + + public YamlConditionDTO(@NonNull Condition condition, RuleSerializationOption option) { + super(condition, option); this.inputs = condition.getInputs(); + if (option != RuleSerializationOption.INCLUDE_ALL && this.inputs.isEmpty()) { + this.inputs = null; + } } @Override @@ -62,23 +70,23 @@ public class YamlConditionDTO extends YamlModuleDTO { public String toString() { StringBuilder builder = new StringBuilder(getClass().getSimpleName()); builder.append(" ["); - if (inputs != null) { - builder.append("inputs=").append(inputs).append(", "); - } if (id != null) { - builder.append("id=").append(id).append(", "); + builder.append("id=").append(id); + } + if (inputs != null) { + builder.append(", inputs=").append(inputs); } if (type != null) { - builder.append("type=").append(type).append(", "); + builder.append(", type=").append(type); } if (label != null) { - builder.append("label=").append(label).append(", "); + builder.append(", label=").append(label); } if (description != null) { - builder.append("description=").append(description).append(", "); + builder.append(", description=").append(description); } if (config != null) { - builder.append("config=").append(config); + builder.append(", config=").append(config); } builder.append("]"); return builder.toString(); diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlModuleDTO.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlModuleDTO.java index 19430f04d..1fdc05028 100644 --- a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlModuleDTO.java +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlModuleDTO.java @@ -15,9 +15,11 @@ package org.openhab.core.model.yaml.internal.rules; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; +import java.util.regex.Pattern; import org.eclipse.jdt.annotation.NonNull; import org.openhab.core.automation.Module; +import org.openhab.core.automation.converter.RuleSerializer.RuleSerializationOption; import com.fasterxml.jackson.annotation.JsonAlias; @@ -28,6 +30,8 @@ import com.fasterxml.jackson.annotation.JsonAlias; */ public class YamlModuleDTO { + private static final Pattern CONTEXT_COMMENT_PATTERN = Pattern.compile("^// context:.*$\\R", Pattern.MULTILINE); + public String id; public String label; public String description; @@ -39,6 +43,10 @@ public class YamlModuleDTO { } public YamlModuleDTO(@NonNull Module module) { + this(module, RuleSerializationOption.NORMAL); + } + + public YamlModuleDTO(@NonNull Module module, RuleSerializationOption option) { this.id = module.getId(); this.label = module.getLabel(); this.description = module.getDescription(); @@ -49,6 +57,16 @@ public class YamlModuleDTO { if (!type.equals(typeAlias)) { this.config.put("type", typeAlias); } + if (option != RuleSerializationOption.INCLUDE_ALL && "application/vnd.openhab.dsl.rule".equals(type)) { + if (this.config.get("script") instanceof String scriptContent) { + // Remove the "context comment" inserted into file-based DSL rules + this.config.put("script", CONTEXT_COMMENT_PATTERN.matcher(scriptContent).replaceFirst("")); + } + this.config.remove(Module.SHARED_CONTEXT); + } + } + if (option != RuleSerializationOption.INCLUDE_ALL && this.config.isEmpty()) { + this.config = null; } } @@ -76,19 +94,19 @@ public class YamlModuleDTO { StringBuilder builder = new StringBuilder(getClass().getSimpleName()); builder.append(" ["); if (id != null) { - builder.append("id=").append(id).append(", "); + builder.append("id=").append(id); } if (type != null) { - builder.append("type=").append(type).append(", "); + builder.append(", type=").append(type); } if (label != null) { - builder.append("label=").append(label).append(", "); + builder.append(", label=").append(label); } if (description != null) { - builder.append("description=").append(description).append(", "); + builder.append(", description=").append(description); } if (config != null) { - builder.append("config=").append(config); + builder.append(", config=").append(config); } builder.append("]"); return builder.toString(); diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlRuleDTO.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlRuleDTO.java index 76a4b8643..d3a3d56c1 100644 --- a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlRuleDTO.java +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlRuleDTO.java @@ -31,6 +31,7 @@ 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.converter.RuleSerializer.RuleSerializationOption; import org.openhab.core.automation.util.RuleUtil; import org.openhab.core.config.core.ConfigDescriptionParameter; import org.openhab.core.io.dto.ModularDTO; @@ -78,46 +79,73 @@ public class YamlRuleDTO implements ModularDTO tags = rule.getTags(); + this.tags = option != RuleSerializationOption.INCLUDE_ALL && tags.isEmpty() ? null : tags; this.description = rule.getDescription(); - this.visibility = rule.getVisibility(); - this.config = rule.getConfiguration().getProperties(); - List<@NonNull ConfigDescriptionParameter> configDescriptions = rule.getConfigurationDescriptions(); - if (!configDescriptions.isEmpty()) { - Map configDescriptionDtos = new LinkedHashMap<>( - configDescriptions.size()); - for (ConfigDescriptionParameter parameter : configDescriptions) { - configDescriptionDtos.put(parameter.getName(), new YamlConfigDescriptionParameterDTO(parameter)); + this.visibility = option == RuleSerializationOption.INCLUDE_ALL || rule.getVisibility() != Visibility.VISIBLE + ? rule.getVisibility() + : null; + if (option != RuleSerializationOption.STRIP_TEMPLATE) { + this.config = new LinkedHashMap<>(rule.getConfiguration().getProperties()); + if (option != RuleSerializationOption.INCLUDE_ALL) { + this.config.remove(Rule.SOURCE); + this.config.remove(Rule.SOURCE_TYPE); + if (this.config.isEmpty()) { + this.config = null; + } } - this.configDescriptions = configDescriptionDtos; } - List<@NonNull Action> actions = rule.getActions(); - if (!actions.isEmpty()) { - List actionDtos = new ArrayList<>(actions.size()); - for (Action action : actions) { - actionDtos.add(new YamlActionDTO(action)); + if (option == RuleSerializationOption.INCLUDE_ALL) { + List<@NonNull ConfigDescriptionParameter> configDescriptions = rule.getConfigurationDescriptions(); + if (!configDescriptions.isEmpty()) { + Map configDescriptionDtos = new LinkedHashMap<>( + configDescriptions.size()); + for (ConfigDescriptionParameter parameter : configDescriptions) { + configDescriptionDtos.put(parameter.getName(), + new YamlConfigDescriptionParameterDTO(parameter, true)); + } + this.configDescriptions = configDescriptionDtos; } - this.actions = actionDtos; } - List<@NonNull Condition> conditions = rule.getConditions(); - if (!conditions.isEmpty()) { - List conditionsDtos = new ArrayList<>(conditions.size()); - for (Condition condition : conditions) { - conditionsDtos.add(new YamlConditionDTO(condition)); + if (option != RuleSerializationOption.STUB_ONLY) { + List<@NonNull Action> actions = rule.getActions(); + if (!actions.isEmpty()) { + List actionDtos = new ArrayList<>(actions.size()); + for (Action action : actions) { + actionDtos.add(new YamlActionDTO(action, option)); + } + this.actions = actionDtos; } - this.conditions = conditionsDtos; - } - List<@NonNull Trigger> triggers = rule.getTriggers(); - if (!triggers.isEmpty()) { - List triggerDtos = new ArrayList<>(triggers.size()); - for (Trigger trigger : triggers) { - triggerDtos.add(new YamlModuleDTO(trigger)); + List<@NonNull Condition> conditions = rule.getConditions(); + if (!conditions.isEmpty()) { + List conditionsDtos = new ArrayList<>(conditions.size()); + for (Condition condition : conditions) { + conditionsDtos.add(new YamlConditionDTO(condition, option)); + } + this.conditions = conditionsDtos; + } + List<@NonNull Trigger> triggers = rule.getTriggers(); + if (!triggers.isEmpty()) { + List triggerDtos = new ArrayList<>(triggers.size()); + for (Trigger trigger : triggers) { + triggerDtos.add(new YamlModuleDTO(trigger, option)); + } + this.triggers = triggerDtos; } - this.triggers = triggerDtos; } } @@ -139,8 +167,10 @@ public class YamlRuleDTO implements ModularDTO tags; public String description; diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlRuleTemplateDTO.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlRuleTemplateDTO.java index 036cf484a..e5e099a29 100644 --- a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlRuleTemplateDTO.java +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlRuleTemplateDTO.java @@ -29,6 +29,7 @@ import org.openhab.core.automation.Action; import org.openhab.core.automation.Condition; import org.openhab.core.automation.Trigger; import org.openhab.core.automation.Visibility; +import org.openhab.core.automation.converter.RuleTemplateSerializer.RuleTemplateSerializationOption; import org.openhab.core.automation.template.RuleTemplate; import org.openhab.core.common.AbstractUID; import org.openhab.core.config.core.ConfigDescriptionParameter; @@ -75,19 +76,25 @@ public class YamlRuleTemplateDTO * Creates a new instance based on the specified {@link RuleTemplate}. * * @param template the {@link RuleTemplate}. + * @param option the {@link RuleTemplateSerializationOption} that decides how the to serialize the + * {@link RuleTemplate}. */ - public YamlRuleTemplateDTO(@NonNull RuleTemplate template) { + public YamlRuleTemplateDTO(@NonNull RuleTemplate template, RuleTemplateSerializationOption option) { this.uid = template.getUID(); this.label = template.getLabel(); - this.tags = template.getTags(); + Set<@NonNull String> tags = template.getTags(); + this.tags = option != RuleTemplateSerializationOption.INCLUDE_ALL && tags.isEmpty() ? null : tags; this.description = template.getDescription(); - this.visibility = template.getVisibility(); + this.visibility = option == RuleTemplateSerializationOption.INCLUDE_ALL + || template.getVisibility() != Visibility.VISIBLE ? template.getVisibility() : null; + List<@NonNull ConfigDescriptionParameter> configDescriptions = template.getConfigurationDescriptions(); if (!configDescriptions.isEmpty()) { Map configDescriptionDtos = new LinkedHashMap<>( configDescriptions.size()); for (ConfigDescriptionParameter parameter : configDescriptions) { - configDescriptionDtos.put(parameter.getName(), new YamlConfigDescriptionParameterDTO(parameter)); + configDescriptionDtos.put(parameter.getName(), new YamlConfigDescriptionParameterDTO(parameter, + option == RuleTemplateSerializationOption.INCLUDE_ALL)); } this.configDescriptions = configDescriptionDtos; } @@ -95,7 +102,7 @@ public class YamlRuleTemplateDTO if (!actions.isEmpty()) { List actionDtos = new ArrayList<>(actions.size()); for (Action action : actions) { - actionDtos.add(new YamlActionDTO(action)); + actionDtos.add(new YamlActionDTO(action, option.toRuleSerializationOption())); } this.actions = actionDtos; } @@ -103,7 +110,7 @@ public class YamlRuleTemplateDTO if (!conditions.isEmpty()) { List conditionsDtos = new ArrayList<>(conditions.size()); for (Condition condition : conditions) { - conditionsDtos.add(new YamlConditionDTO(condition)); + conditionsDtos.add(new YamlConditionDTO(condition, option.toRuleSerializationOption())); } this.conditions = conditionsDtos; } @@ -111,7 +118,7 @@ public class YamlRuleTemplateDTO if (!triggers.isEmpty()) { List triggerDtos = new ArrayList<>(triggers.size()); for (Trigger trigger : triggers) { - triggerDtos.add(new YamlModuleDTO(trigger)); + triggerDtos.add(new YamlModuleDTO(trigger, option.toRuleSerializationOption())); } this.triggers = triggerDtos; } diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/converter/YamlRuleConverter.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/converter/YamlRuleConverter.java new file mode 100644 index 000000000..5ab3ce139 --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/converter/YamlRuleConverter.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2010-2026 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.model.yaml.internal.rules.converter; + +import java.io.ByteArrayInputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +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.Module; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.converter.RuleParser; +import org.openhab.core.automation.converter.RuleSerializer; +import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRule; +import org.openhab.core.converter.SerializabilityResult; +import org.openhab.core.io.dto.SerializationException; +import org.openhab.core.model.yaml.YamlElement; +import org.openhab.core.model.yaml.YamlModelRepository; +import org.openhab.core.model.yaml.internal.rules.YamlRuleDTO; +import org.openhab.core.model.yaml.internal.rules.YamlRuleProvider; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * {@link YamlRuleConverter} is the YAML converter for {@link Rule} objects. + * + * @author Ravi Nadahar - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = { RuleSerializer.class, RuleParser.class }) +public class YamlRuleConverter implements RuleSerializer, RuleParser { + + private final YamlModelRepository modelRepository; + private final YamlRuleProvider ruleProvider; + + @Activate + public YamlRuleConverter(@Reference YamlModelRepository modelRepository, @Reference YamlRuleProvider ruleProvider) { + this.modelRepository = modelRepository; + this.ruleProvider = ruleProvider; + } + + @Override + public String getGeneratedFormat() { + return "YAML"; + } + + @Override + public List> checkSerializability(Collection rules) { + List> result = new ArrayList<>(rules.size()); + boolean failed; + for (Rule rule : rules) { + failed = false; + if (rule instanceof SimpleRule) { + result.add(new SerializabilityResult<>(rule.getUID(), false, + "Rule '" + rule.getUID() + "' is a SimpleRule with an inaccessible action.")); + continue; + } + + for (Action action : rule.getActions()) { + if (action.getConfiguration().get("type") instanceof String type + && "application/vnd.openhab.dsl.rule".equals(type) + && action.getConfiguration().get(Module.SHARED_CONTEXT) instanceof Boolean shared + && shared.booleanValue()) { + result.add(new SerializabilityResult<>(rule.getUID(), false, + "Rule '" + rule.getUID() + "': action '" + action.getId() + "' has shared context.")); + failed = true; + break; + } + } + + if (!failed) { + result.add(new SerializabilityResult<>(rule.getUID(), true, "")); + } + } + + return result; + } + + @Override + public void setRulesToBeSerialized(String id, List rules, RuleSerializationOption option) + throws SerializationException { + List errors = null; + List> checks = checkSerializability(rules); + for (SerializabilityResult check : checks) { + if (!check.ok()) { + if (errors == null) { + errors = new ArrayList<>(); + } + errors.add(check.failureReason()); + } + } + if (errors != null) { + throw new SerializationException( + "Rule serialization attempt failed with:\n " + String.join("\n ", errors)); + } + + Set handledRules = new HashSet<>(); + List elements = new ArrayList<>(rules.size()); + for (Rule rule : rules) { + if (handledRules.contains(rule)) { + continue; + } + try { + elements.add(new YamlRuleDTO(rule, option)); + } catch (RuntimeException e) { + if (errors == null) { + errors = new ArrayList<>(); + } + errors.add("Rule '" + rule.getUID() + "': " + e.getMessage()); + } + } + if (errors != null) { + throw new SerializationException( + "Rule serialization attempt failed with:\n " + String.join("\n ", errors)); + } + + modelRepository.addElementsToBeGenerated(id, elements); + } + + @Override + public void generateFormat(String id, OutputStream out) { + modelRepository.generateFileFormat(id, out); + } + + @Override + public String getParserFormat() { + return "YAML"; + } + + @Override + public @Nullable String startParsingFormat(String syntax, List errors, List warnings) { + ByteArrayInputStream inputStream = new ByteArrayInputStream(syntax.getBytes(StandardCharsets.UTF_8)); + return modelRepository.createIsolatedModel(inputStream, errors, warnings); + } + + @Override + public Collection getParsedObjects(String modelName) { + return ruleProvider.getAllFromModel(modelName); + } + + @Override + public void finishParsingFormat(String modelName) { + modelRepository.removeIsolatedModel(modelName); + } +} diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/converter/YamlRuleTemplateConverter.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/converter/YamlRuleTemplateConverter.java new file mode 100644 index 000000000..c3278c1a8 --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/converter/YamlRuleTemplateConverter.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2010-2026 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.model.yaml.internal.rules.converter; + +import java.io.ByteArrayInputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.converter.RuleTemplateParser; +import org.openhab.core.automation.converter.RuleTemplateSerializer; +import org.openhab.core.automation.template.RuleTemplate; +import org.openhab.core.converter.SerializabilityResult; +import org.openhab.core.io.dto.SerializationException; +import org.openhab.core.model.yaml.YamlElement; +import org.openhab.core.model.yaml.YamlModelRepository; +import org.openhab.core.model.yaml.internal.rules.YamlRuleTemplateDTO; +import org.openhab.core.model.yaml.internal.rules.YamlRuleTemplateProvider; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * {@link YamlRuleTemplateConverter} is the YAML converter for {@link RuleTemplate} objects. + * + * @author Ravi Nadahar - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = { RuleTemplateSerializer.class, RuleTemplateParser.class }) +public class YamlRuleTemplateConverter implements RuleTemplateSerializer, RuleTemplateParser { + + private final YamlModelRepository modelRepository; + private final YamlRuleTemplateProvider templateProvider; + + @Activate + public YamlRuleTemplateConverter(@Reference YamlModelRepository modelRepository, + @Reference YamlRuleTemplateProvider templateProvider) { + this.modelRepository = modelRepository; + this.templateProvider = templateProvider; + } + + @Override + public String getGeneratedFormat() { + return "YAML"; + } + + @Override + public List> checkSerializability(Collection templates) { + List> result = new ArrayList<>(templates.size()); + for (RuleTemplate template : templates) { + // There are no known circumstances under which a rule template can't be serialized to YAML + result.add(new SerializabilityResult<>(template.getUID(), true, "")); + } + return result; + } + + @Override + public void setTemplatesToBeSerialized(String id, List templates, + RuleTemplateSerializationOption option) throws SerializationException { + List errors = null; + List> checks = checkSerializability(templates); + for (SerializabilityResult check : checks) { + if (!check.ok()) { + if (errors == null) { + errors = new ArrayList<>(); + } + errors.add(check.failureReason()); + } + } + if (errors != null) { + throw new SerializationException( + "Rule template serialization attempt failed with:\n " + String.join("\n ", errors)); + } + + Set handledTemplates = new HashSet<>(); + List elements = new ArrayList<>(templates.size()); + for (RuleTemplate template : templates) { + if (handledTemplates.contains(template)) { + continue; + } + try { + elements.add(new YamlRuleTemplateDTO(template, option)); + } catch (RuntimeException e) { + if (errors == null) { + errors = new ArrayList<>(); + } + errors.add("Rule template '" + template.getUID() + "': " + e.getMessage()); + } + } + if (errors != null) { + throw new SerializationException( + "Rule template serialization attempt failed with:\n " + String.join("\n ", errors)); + } + + modelRepository.addElementsToBeGenerated(id, elements); + } + + @Override + public void generateFormat(String id, OutputStream out) { + modelRepository.generateFileFormat(id, out); + } + + @Override + public String getParserFormat() { + return "YAML"; + } + + @Override + public @Nullable String startParsingFormat(String syntax, List errors, List warnings) { + ByteArrayInputStream inputStream = new ByteArrayInputStream(syntax.getBytes(StandardCharsets.UTF_8)); + return modelRepository.createIsolatedModel(inputStream, errors, warnings); + } + + @Override + public Collection getParsedObjects(String modelName) { + return templateProvider.getAllFromModel(modelName); + } + + @Override + public void finishParsingFormat(String modelName) { + modelRepository.removeIsolatedModel(modelName); + } +} diff --git a/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlActionDTOTest.java b/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlActionDTOTest.java index 9076f23ba..b25df73dd 100644 --- a/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlActionDTOTest.java +++ b/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlActionDTOTest.java @@ -56,12 +56,11 @@ public class YamlActionDTOTest { Action a = ActionBuilder.create().withId("action1").withTypeUID("type1").build(); YamlActionDTO action1 = new YamlActionDTO(a); YamlActionDTO action2 = new YamlActionDTO(); - assertEquals("YamlActionDTO [inputs={}, id=action1, type=type1, config={}]", action1.toString()); + assertEquals("YamlActionDTO [id=action1, type=type1]", action1.toString()); assertEquals("YamlActionDTO []", action2.toString()); action1.label = "Label1"; action1.description = "Description1"; - assertEquals( - "YamlActionDTO [inputs={}, id=action1, type=type1, label=Label1, description=Description1, config={}]", + assertEquals("YamlActionDTO [id=action1, type=type1, label=Label1, description=Description1]", action1.toString()); } } diff --git a/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlConditionDTOTest.java b/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlConditionDTOTest.java index e9647f53d..c23adfd0f 100644 --- a/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlConditionDTOTest.java +++ b/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlConditionDTOTest.java @@ -56,12 +56,11 @@ public class YamlConditionDTOTest { Condition c = ConditionBuilder.create().withId("condition1").withTypeUID("type1").build(); YamlConditionDTO condition1 = new YamlConditionDTO(c); YamlConditionDTO condition2 = new YamlConditionDTO(); - assertEquals("YamlConditionDTO [inputs={}, id=condition1, type=type1, config={}]", condition1.toString()); + assertEquals("YamlConditionDTO [id=condition1, type=type1]", condition1.toString()); assertEquals("YamlConditionDTO []", condition2.toString()); condition1.label = "Label1"; condition1.description = "Description1"; - assertEquals( - "YamlConditionDTO [inputs={}, id=condition1, type=type1, label=Label1, description=Description1, config={}]", + assertEquals("YamlConditionDTO [id=condition1, type=type1, label=Label1, description=Description1]", condition1.toString()); } } diff --git a/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlModuleDTOTest.java b/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlModuleDTOTest.java index c95311cfd..3f6a6bdc7 100644 --- a/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlModuleDTOTest.java +++ b/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlModuleDTOTest.java @@ -56,11 +56,11 @@ public class YamlModuleDTOTest { Trigger t = TriggerBuilder.create().withId("trigger1").withTypeUID("type1").build(); YamlModuleDTO trigger1 = new YamlModuleDTO(t); YamlModuleDTO trigger2 = new YamlModuleDTO(); - assertEquals("YamlModuleDTO [id=trigger1, type=type1, config={}]", trigger1.toString()); + assertEquals("YamlModuleDTO [id=trigger1, type=type1]", trigger1.toString()); assertEquals("YamlModuleDTO []", trigger2.toString()); trigger1.label = "Label1"; trigger1.description = "Description1"; - assertEquals("YamlModuleDTO [id=trigger1, type=type1, label=Label1, description=Description1, config={}]", + assertEquals("YamlModuleDTO [id=trigger1, type=type1, label=Label1, description=Description1]", trigger1.toString()); } } diff --git a/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlRuleDTOTest.java b/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlRuleDTOTest.java index 41d71ea1b..c2b8eec42 100644 --- a/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlRuleDTOTest.java +++ b/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlRuleDTOTest.java @@ -29,6 +29,7 @@ import org.openhab.core.automation.Condition; import org.openhab.core.automation.Rule; import org.openhab.core.automation.Trigger; import org.openhab.core.automation.Visibility; +import org.openhab.core.automation.converter.RuleSerializer.RuleSerializationOption; import org.openhab.core.automation.util.ActionBuilder; import org.openhab.core.automation.util.ConditionBuilder; import org.openhab.core.automation.util.RuleBuilder; @@ -61,18 +62,32 @@ public class YamlRuleDTOTest { rule = RuleBuilder.create(rule).withConfigurationDescriptions( List.of(ConfigDescriptionParameterBuilder.create("number", Type.DECIMAL).build())).build(); - YamlRuleDTO ruleDTO = new YamlRuleDTO(rule); + YamlRuleDTO ruleDTO = new YamlRuleDTO(rule, RuleSerializationOption.INCLUDE_ALL); assertNotNull(ruleDTO); assertEquals( - "YamlRuleDTO [uid=rule1, templateState=no-template, label=Rule 1, tags=[], visibility=VISIBLE, config={}, configDescriptions={number=YamlConfigDescriptionParameterDTO [required=false, type=DECIMAL, readOnly=false, multiple=false, advanced=false, verify=false, limitToOptions=true, ]}, conditions=[YamlConditionDTO [inputs={}, id=condition1, type=type1, config={}]], actions=[YamlActionDTO [inputs={}, id=action1, type=type1, config={}]], triggers=[YamlModuleDTO [id=trigger1, type=type1, config={}]]]", + "YamlRuleDTO [uid=rule1, templateState=no-template, label=Rule 1, tags=[], visibility=VISIBLE, config={}, configDescriptions={number=YamlConfigDescriptionParameterDTO [required=false, type=DECIMAL, readOnly=false, multiple=false, advanced=false, verify=false, limitToOptions=true, ]}, conditions=[YamlConditionDTO [id=condition1, inputs={}, type=type1, config={}]], actions=[YamlActionDTO [id=action1, inputs={}, type=type1, config={}]], triggers=[YamlModuleDTO [id=trigger1, type=type1, config={}]]]", ruleDTO.toString()); rule = RuleBuilder.create(rule).withTemplateUID("templateUID").withActions(List.of()) .withDescription("Rule description").build(); - ruleDTO = new YamlRuleDTO(rule); + ruleDTO = new YamlRuleDTO(rule, RuleSerializationOption.INCLUDE_ALL); assertNotNull(ruleDTO); assertEquals( - "YamlRuleDTO [uid=rule1, template=templateUID, templateState=no-template, label=Rule 1, tags=[], description=Rule description, visibility=VISIBLE, config={}, configDescriptions={number=YamlConfigDescriptionParameterDTO [required=false, type=DECIMAL, readOnly=false, multiple=false, advanced=false, verify=false, limitToOptions=true, ]}, conditions=[YamlConditionDTO [inputs={}, id=condition1, type=type1, config={}]], triggers=[YamlModuleDTO [id=trigger1, type=type1, config={}]]]", + "YamlRuleDTO [uid=rule1, template=templateUID, templateState=no-template, label=Rule 1, tags=[], description=Rule description, visibility=VISIBLE, config={}, configDescriptions={number=YamlConfigDescriptionParameterDTO [required=false, type=DECIMAL, readOnly=false, multiple=false, advanced=false, verify=false, limitToOptions=true, ]}, conditions=[YamlConditionDTO [id=condition1, inputs={}, type=type1, config={}]], triggers=[YamlModuleDTO [id=trigger1, type=type1, config={}]]]", + ruleDTO.toString()); + ruleDTO = new YamlRuleDTO(rule, RuleSerializationOption.NORMAL); + assertNotNull(ruleDTO); + assertEquals( + "YamlRuleDTO [uid=rule1, template=templateUID, label=Rule 1, description=Rule description, conditions=[YamlConditionDTO [id=condition1, type=type1]], triggers=[YamlModuleDTO [id=trigger1, type=type1]]]", + ruleDTO.toString()); + ruleDTO = new YamlRuleDTO(rule, RuleSerializationOption.STRIP_TEMPLATE); + assertNotNull(ruleDTO); + assertEquals( + "YamlRuleDTO [uid=rule1, label=Rule 1, description=Rule description, conditions=[YamlConditionDTO [id=condition1, type=type1]], triggers=[YamlModuleDTO [id=trigger1, type=type1]]]", + ruleDTO.toString()); + ruleDTO = new YamlRuleDTO(rule, RuleSerializationOption.STUB_ONLY); + assertNotNull(ruleDTO); + assertEquals("YamlRuleDTO [uid=rule1, template=templateUID, label=Rule 1, description=Rule description, ]", ruleDTO.toString()); } diff --git a/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlRuleProviderTest.java b/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlRuleProviderTest.java index 8ac6a6c0f..e2f9b3dbd 100644 --- a/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlRuleProviderTest.java +++ b/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlRuleProviderTest.java @@ -217,7 +217,7 @@ public class YamlRuleProviderTest { assertThat(rule.getName(), is("Mode TV")); assertThat(rule.getDescription(), is(emptyOrNullString())); assertThat(rule.getTemplateUID(), is(emptyOrNullString())); - assertThat(rule.getTemplateState(), is(TemplateState.INSTANTIATED)); + assertThat(rule.getTemplateState(), is(TemplateState.NO_TEMPLATE)); assertThat(rule.getVisibility(), is(Visibility.VISIBLE)); Configuration config = rule.getConfiguration(); assertThat(config.getProperties(), is(aMapWithSize(1))); @@ -277,7 +277,7 @@ public class YamlRuleProviderTest { assertThat(rule.getDescription(), is("Creates timers to transition a state Item to a new state at defined times of day.")); assertThat(rule.getTemplateUID(), is("none")); - assertThat(rule.getTemplateState(), is(TemplateState.TEMPLATE_MISSING)); + assertThat(rule.getTemplateState(), is(TemplateState.PENDING)); assertThat(rule.getVisibility(), is(Visibility.VISIBLE)); config = rule.getConfiguration(); assertThat(config.getProperties(), is(aMapWithSize(3))); @@ -366,7 +366,7 @@ public class YamlRuleProviderTest { assertThat(rule.getDescription(), is( "This will monitor the power consumption of a washing machine and send an alert command when it gets below a threshold, meaning it has finished.")); assertThat(rule.getTemplateUID(), is(emptyOrNullString())); - assertThat(rule.getTemplateState(), is(TemplateState.TEMPLATE_MISSING)); + assertThat(rule.getTemplateState(), is(TemplateState.NO_TEMPLATE)); assertThat(rule.getVisibility(), is(Visibility.HIDDEN)); assertThat(rule.getConfiguration().getProperties(), is(anEmptyMap())); configDescriptions = rule.getConfigurationDescriptions(); @@ -451,7 +451,7 @@ public class YamlRuleProviderTest { assertThat(rule.getName(), is("Full Rule")); assertThat(rule.getDescription(), is("The description of the full rule")); assertThat(rule.getTemplateUID(), is("template:non-existing")); - assertThat(rule.getTemplateState(), is(TemplateState.TEMPLATE_MISSING)); + assertThat(rule.getTemplateState(), is(TemplateState.PENDING)); assertThat(rule.getVisibility(), is(Visibility.EXPERT)); Configuration config = rule.getConfiguration(); assertThat(config.getProperties(), is(aMapWithSize(4))); diff --git a/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlRuleTemplateDTOTest.java b/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlRuleTemplateDTOTest.java index 12879015d..a01347b85 100644 --- a/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlRuleTemplateDTOTest.java +++ b/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlRuleTemplateDTOTest.java @@ -28,6 +28,7 @@ import org.openhab.core.automation.Action; import org.openhab.core.automation.Condition; import org.openhab.core.automation.Trigger; import org.openhab.core.automation.Visibility; +import org.openhab.core.automation.converter.RuleTemplateSerializer.RuleTemplateSerializationOption; import org.openhab.core.automation.template.RuleTemplate; import org.openhab.core.automation.util.ActionBuilder; import org.openhab.core.automation.util.ConditionBuilder; @@ -49,25 +50,31 @@ public class YamlRuleTemplateDTOTest { Action action = ActionBuilder.create().withId("action1").withTypeUID("type1").build(); RuleTemplate template = new RuleTemplate("template1", "Foo Template", "Foo rule template", Set.of("test"), List.of(), List.of(), List.of(action), List.of(), Visibility.VISIBLE); - assertNotNull(new YamlRuleTemplateDTO(template)); + assertNotNull(new YamlRuleTemplateDTO(template, RuleTemplateSerializationOption.NORMAL)); Condition condition = ConditionBuilder.create().withId("condition1").withTypeUID("type1").build(); template = new RuleTemplate("template1", "Foo Template", "Foo rule template", Set.of("test"), List.of(), List.of(condition), List.of(action), List.of(), Visibility.VISIBLE); - assertNotNull(new YamlRuleTemplateDTO(template)); + assertNotNull(new YamlRuleTemplateDTO(template, RuleTemplateSerializationOption.NORMAL)); Trigger trigger = TriggerBuilder.create().withId("trigger1").withTypeUID("type1").build(); template = new RuleTemplate("template1", "Foo Template", "Foo rule template", Set.of("test"), List.of(trigger), List.of(condition), List.of(action), List.of(), Visibility.VISIBLE); - assertNotNull(new YamlRuleTemplateDTO(template)); + assertNotNull(new YamlRuleTemplateDTO(template, RuleTemplateSerializationOption.NORMAL)); template = new RuleTemplate("template1", "Foo Template", "Foo rule template", Set.of("test"), List.of(trigger), List.of(condition), List.of(action), List.of(ConfigDescriptionParameterBuilder.create("number", Type.DECIMAL).build()), Visibility.VISIBLE); - YamlRuleTemplateDTO templateDTO = new YamlRuleTemplateDTO(template); + YamlRuleTemplateDTO templateDTO = new YamlRuleTemplateDTO(template, + RuleTemplateSerializationOption.INCLUDE_ALL); assertNotNull(templateDTO); assertEquals( - "YamlRuleTemplateDTO [uid=template1, label=Foo Template, tags=[test], description=Foo rule template, visibility=VISIBLE, configDescriptions={number=YamlConfigDescriptionParameterDTO [required=false, type=DECIMAL, readOnly=false, multiple=false, advanced=false, verify=false, limitToOptions=true, ]}, conditions=[YamlConditionDTO [inputs={}, id=condition1, type=type1, config={}]], actions=[YamlActionDTO [inputs={}, id=action1, type=type1, config={}]], triggers=[YamlModuleDTO [id=trigger1, type=type1, config={}]]]", + "YamlRuleTemplateDTO [uid=template1, label=Foo Template, tags=[test], description=Foo rule template, visibility=VISIBLE, configDescriptions={number=YamlConfigDescriptionParameterDTO [required=false, type=DECIMAL, readOnly=false, multiple=false, advanced=false, verify=false, limitToOptions=true, ]}, conditions=[YamlConditionDTO [id=condition1, inputs={}, type=type1, config={}]], actions=[YamlActionDTO [id=action1, inputs={}, type=type1, config={}]], triggers=[YamlModuleDTO [id=trigger1, type=type1, config={}]]]", + templateDTO.toString()); + templateDTO = new YamlRuleTemplateDTO(template, RuleTemplateSerializationOption.NORMAL); + assertNotNull(templateDTO); + assertEquals( + "YamlRuleTemplateDTO [uid=template1, label=Foo Template, tags=[test], description=Foo rule template, configDescriptions={number=YamlConfigDescriptionParameterDTO [type=DECIMAL, ]}, conditions=[YamlConditionDTO [id=condition1, type=type1]], actions=[YamlActionDTO [id=action1, type=type1]], triggers=[YamlModuleDTO [id=trigger1, type=type1]]]", templateDTO.toString()); } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/converter/SerializabilityResult.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/converter/SerializabilityResult.java new file mode 100644 index 000000000..1a6aef627 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/converter/SerializabilityResult.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010-2026 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.converter; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * A container that holds the result of a serializability check. + * + * @author Nadahar - Initial contribution + */ +@NonNullByDefault +public record SerializabilityResult (T uid, boolean ok, String failureReason) { + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()).append(" [uid=").append(uid).append(", ok=").append(ok); + if (!ok) { + sb.append(", failureReason=").append(failureReason); + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml index 2a910cfe6..7aa54c387 100644 --- a/features/karaf/openhab-core/src/main/feature/feature.xml +++ b/features/karaf/openhab-core/src/main/feature/feature.xml @@ -429,6 +429,7 @@ openhab-core-base + openhab-core-automation-module-script-rulesupport openhab-core-ui mvn:org.openhab.core.bundles/org.openhab.core.model.yaml/${project.version} openhab.tp;filter:="(feature=jackson)"