From 369889495b0c531bfb70474f207e2cbc294b69f6 Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Mon, 16 Feb 2026 20:08:06 +0100 Subject: [PATCH 01/20] Create ObjectParser and ObjectSerializer interfaces for rules and rule templates Signed-off-by: Ravi Nadahar --- .../core/automation/converter/RuleParser.java | 51 +++++++++ .../automation/converter/RuleSerializer.java | 97 ++++++++++++++++ .../converter/RuleTemplateParser.java | 51 +++++++++ .../converter/RuleTemplateSerializer.java | 105 ++++++++++++++++++ .../core/converter/SerializabilityResult.java | 35 ++++++ 5 files changed, 339 insertions(+) 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/src/main/java/org/openhab/core/converter/SerializabilityResult.java 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 00000000000..1c85cfd954a --- /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 00000000000..b1f956071af --- /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 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 00000000000..e8d4772c6d7 --- /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 00000000000..a65e820060c --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/converter/RuleTemplateSerializer.java @@ -0,0 +1,105 @@ +/* + * 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.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 Rule} 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 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; + } + + 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; + } + + 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/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 00000000000..1a6aef6275e --- /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(); + } +} From 7b1486e1dff1f534bea6290e70bc2b0161055847 Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Fri, 27 Feb 2026 05:53:57 +0100 Subject: [PATCH 02/20] Embed DSL rule source with the Rule Signed-off-by: Ravi Nadahar --- .../java/org/openhab/core/automation/Rule.java | 14 ++++++++++++++ .../rule/runtime/internal/DSLRuleProvider.java | 8 ++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) 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 f0ae3dce108..4c139ddee4c 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 @@ default TemplateState getTemplateState() { 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.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 2b6a6959b02..ae3a100e48e 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 @@ -114,7 +114,7 @@ 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<>(); @@ -360,8 +360,12 @@ private Rule toRule(String modelName, org.openhab.core.model.rule.rules.Rule rul List actions = List.of(ActionBuilder.create().withId("script").withTypeUID(ScriptActionHandler.TYPE_ID) .withConfiguration(cfg).build()); + Configuration ruleCfg = new Configuration(); + ruleCfg.put(Rule.SOURCE, + NodeModelUtils.findActualNodeFor(rule).getParent().getText().replaceFirst("^\\R+", "")); + 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 String removeIndentation(String script) { From 7a2521b1fdafb0ea1d2a299a07b1199cfa70dd23 Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Wed, 13 May 2026 17:18:12 +0200 Subject: [PATCH 03/20] Detect shared context DSL rules Signed-off-by: Ravi Nadahar --- .../org/openhab/core/automation/Module.java | 7 ++++ .../runtime/internal/DSLRuleProvider.java | 42 ++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) 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 c423410603e..a4bc9e4d158 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.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 ae3a100e48e..f4e171d91c9 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.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.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; @@ -357,17 +361,51 @@ private Rule toRule(String modelName, org.openhab.core.model.rule.rules.Rule rul 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(); - ruleCfg.put(Rule.SOURCE, - NodeModelUtils.findActualNodeFor(rule).getParent().getText().replaceFirst("^\\R+", "")); + 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).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) { String s = script; // first let's remove empty lines at the beginning and add an empty line at the end to beautify the yaml style. From ca3c82c3b25157192860a2f3db8b38caa4fc2416 Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Mon, 30 Mar 2026 15:35:20 +0200 Subject: [PATCH 04/20] Update YAML DTOs to support serialization Signed-off-by: Ravi Nadahar --- .../config/YamlConfigDescriptionDTO.java | 8 +- .../YamlConfigDescriptionParameterDTO.java | 36 +++++++- ...amlConfigDescriptionParameterGroupDTO.java | 9 +- .../yaml/internal/rules/YamlActionDTO.java | 26 ++++-- .../yaml/internal/rules/YamlConditionDTO.java | 26 ++++-- .../yaml/internal/rules/YamlModuleDTO.java | 28 +++++- .../yaml/internal/rules/YamlRuleDTO.java | 91 ++++++++++++------- .../internal/rules/YamlRuleTemplateDTO.java | 23 +++-- .../internal/rules/YamlActionDTOTest.java | 5 +- .../internal/rules/YamlConditionDTOTest.java | 5 +- .../internal/rules/YamlModuleDTOTest.java | 4 +- .../yaml/internal/rules/YamlRuleDTOTest.java | 23 ++++- .../rules/YamlRuleTemplateDTOTest.java | 17 +++- 13 files changed, 215 insertions(+), 86 deletions(-) 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 6efeaa6689a..7813617e301 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 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 YamlConfigDescriptionDTO(@NonNull ConfigDescription configDescription) { 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 46188340c99..18fe3ff4ecd 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 @@ */ 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 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 YamlConfigDescriptionParameterDTO(@NonNull ConfigDescriptionParameter par } 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 boolean equals(Object obj) { && 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 String toString() { 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 d2ccdf1f7cb..123f71eec2a 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 @@ */ 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 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/YamlActionDTO.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/YamlActionDTO.java index 9c34fd5460b..b7d62881c4e 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 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 YamlActionDTO() { } 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 boolean equals(Object obj) { 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 48c58cf6cc5..229df6441bc 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 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 YamlConditionDTO() { } 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 boolean equals(Object obj) { 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 19430f04dbe..1fdc0502890 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 @@ 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 @@ */ 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 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 YamlModuleDTO(@NonNull Module module) { 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 String toString() { 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 76a4b8643dd..5e84e3317d9 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.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 YamlRuleDTO() { * @param rule the {@link Rule}. */ public YamlRuleDTO(@NonNull Rule rule) { + this(rule, RuleSerializationOption.NORMAL); + } + + /** + * Creates a new instance based on the specified {@link Rule}. + * + * @param rule the {@link Rule}. + * @param option the {@link RuleSerializationOption} that decides how the to serialize the {@link Rule}. + */ + public YamlRuleDTO(@NonNull Rule rule, RuleSerializationOption option) { this.uid = rule.getUID(); - this.template = rule.getTemplateUID(); - this.templateState = rule.getTemplateState(); + this.template = option == RuleSerializationOption.STRIP_TEMPLATE ? null : rule.getTemplateUID(); + this.templateState = option == RuleSerializationOption.INCLUDE_ALL ? rule.getTemplateState() : null; this.label = rule.getName(); - this.tags = rule.getTags(); + Set<@NonNull String> 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; } } @@ -409,6 +437,7 @@ protected static class YamlPartialRuleDTO { @JsonAlias({ "templateUid", "templateUID" }) public String template; public String templateState; + @JsonAlias({ "name" }) public String label; public Set<@NonNull String> 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 036cf484a87..f58e47987f2 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.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,27 @@ public 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}. + * @throws IllegalArgumentException If the specified {@code option} isn't supported. */ - public YamlRuleTemplateDTO(@NonNull RuleTemplate template) { + public YamlRuleTemplateDTO(@NonNull RuleTemplate template, RuleTemplateSerializationOption option) + throws IllegalArgumentException { 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 +104,7 @@ public YamlRuleTemplateDTO(@NonNull RuleTemplate template) { 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 +112,7 @@ public YamlRuleTemplateDTO(@NonNull RuleTemplate template) { 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 +120,7 @@ public YamlRuleTemplateDTO(@NonNull RuleTemplate template) { 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/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 9076f23ba56..b25df73ddca 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 void testToString() { 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 e9647f53d80..c23adfd0f45 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 void testToString() { 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 c95311cfdaa..3f6a6bdc7fe 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 void testToString() { 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 41d71ea1b72..c2b8eec42ec 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.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 void testConstructor() { 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/YamlRuleTemplateDTOTest.java b/bundles/org.openhab.core.model.yaml/src/test/java/org/openhab/core/model/yaml/internal/rules/YamlRuleTemplateDTOTest.java index 12879015dbb..a01347b85fd 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.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 void testConstructor() { 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()); } From 2f876164e219d1e5328c1fa2efa35c0eb365a7a6 Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Mon, 23 Feb 2026 22:51:29 +0100 Subject: [PATCH 05/20] Create rule converters Signed-off-by: Ravi Nadahar --- .../internal/converter/DslRuleConverter.java | 657 ++++++++++++++++++ bundles/org.openhab.core.model.rule/bnd.bnd | 2 + .../core/model/rule/RulesRuntimeModule.xtend | 6 + .../rule/formatting/RulesFormatter.xtend | 74 +- .../rules/converter/YamlRuleConverter.java | 163 +++++ .../openhab-core/src/main/feature/feature.xml | 1 + 6 files changed, 894 insertions(+), 9 deletions(-) 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.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/converter/YamlRuleConverter.java 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 00000000000..c97d6522a61 --- /dev/null +++ b/bundles/org.openhab.core.model.rule.runtime/src/org/openhab/core/model/rule/runtime/internal/converter/DslRuleConverter.java @@ -0,0 +1,657 @@ +/* + * 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.NonNull; +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.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.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.ItemCommandTriggerHandler; +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.ThingStatusTriggerHandler; +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.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.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.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.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 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 @NonNull 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"); + } + List triggers = rule.getTriggers(); + if (triggers.isEmpty()) { + errors.add("has no triggers"); + } else { + for (Trigger trigger : triggers) { + try { + buildModelTrigger(trigger); + } catch (SerializationException e) { + errors.add("trigger '" + trigger.getId() + "': " + e.getMessage()); + } + } + } + + if (!rule.getConditions().isEmpty()) { + errors.add("has conditions"); + } + 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.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.warn("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()); + String result = modelRepository.createIsolatedModel("rules", inputStream, errors, warnings); + return result; + } + + @Override + public @NonNull Collection getParsedObjects(String modelName) { + List result = new ArrayList<>(); + RuleBuilder builder; + List actions = new ArrayList<>(); + ActionBuilder aBuilder; + LinkedHashMap props; + Set usedUids = new HashSet<>(); + String strippedModeName = modelName.replace(".rules", ""); + String uid; + for (Rule rule : ruleProvider.getAllFromModel(modelName)) { + if ((uid = rule.getUID()).startsWith(strippedModeName) || 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 (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(); + } else { + SystemStartlevelTrigger result = factory.createSystemStartlevelTrigger(); + result.setLevel(level); + return result; + } + } else { + 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; + } else { + 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; + } else { + 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; + } else { + 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; + } else { + 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; + } else { + 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; + } else { + 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; + } else { + 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(); + if ("12:00".equals(str)) { + result.setTime("noon"); + } else if ("00:00".equals(str)) { + result.setTime("midnight"); + } else { + result.setTime(str); + } + return result; + } else { + 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 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 f43eae7b46c..0b6521a1412 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,\ @@ -34,6 +35,7 @@ Import-Package: \ org.openhab.core.thing.binding,\ org.openhab.core.thing.events,\ org.openhab.core.types,\ + org.openhab.core.types.util,\ com.google.common.base;version="14",\ javax.measure,\ javax.measure.quantity,\ 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 abec2525b2a..98a99216242 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/RulesFormatter.xtend b/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/formatting/RulesFormatter.xtend index 71ec9479f02..ef1a2844451 100644 --- 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 @@ -15,10 +15,12 @@ */ package org.openhab.core.model.rule.formatting +import com.google.inject.Inject import org.eclipse.xtext.formatting.impl.AbstractDeclarativeFormatter +import org.eclipse.xtext.formatting.impl.AbstractFormattingConfig import org.eclipse.xtext.formatting.impl.FormattingConfig -// import com.google.inject.Inject; -// import org.openhab.core.model.rule.services.RulesGrammarAccess +import org.eclipse.xtext.xtext.XtextFormatter +import org.openhab.core.model.rule.services.RulesGrammarAccess /** * This class contains custom formatting description. @@ -26,17 +28,71 @@ import org.eclipse.xtext.formatting.impl.FormattingConfig * 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 + * Also see {@link XtextFormatter} as an example */ class RulesFormatter extends AbstractDeclarativeFormatter { -// @Inject extension RulesGrammarAccess + @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) + + c.setLinewrap(1, 1, 2).before(ruleModelRule) + c.setLinewrap(1, 1, 2).after(ruleModelRule) + c.setLinewrap(1, 1, 2).after(XImportDeclarationRule) + c.setLinewrap(1, 1, 2).after(XFunctionTypeRefRule) + c.setLinewrap(1, 1, 2).after(XBlockExpressionRule) + c.setLinewrap(1, 2, 2).before(getRuleAccess.ruleKeyword_0) + c.setLinewrap(1, 1, 2).after(getRuleAccess.orKeyword_6_0) + c.setLinewrap(1, 1, 2).before(getRuleAccess.whenKeyword_4) + c.setLinewrap(1, 1, 2).after(getRuleAccess.whenKeyword_4) + c.setLinewrap(1, 1, 2).before(getRuleAccess.thenKeyword_8) + c.setLinewrap(1, 1, 2).after(getRuleAccess.thenKeyword_8) + c.setLinewrap(1, 1, 2).before(getRuleAccess.endKeyword_10) + + c.setIndentationIncrement.after("{") + c.setIndentationDecrement.before("}") + c.setIndentationIncrement.after(getRuleAccess.whenKeyword_4) + c.setIndentationDecrement.before(getRuleAccess.thenKeyword_8) + c.setIndentationIncrement.after(getRuleAccess.thenKeyword_8) + c.setIndentationDecrement.before(getRuleAccess.endKeyword_10) + + c.setLinewrap().before("}") + + c.setNoSpace().withinKeywordPairs("(", ")") + c.setNoSpace().withinKeywordPairs("[", "]") + c.setNoSpace().around("=") + c.setNoSpace().around(".") + c.setNoSpace().before(",") + + c.autoLinewrap = 120 + + c.setLinewrap(0, 1, 2).before(SL_COMMENTRule) + c.setLinewrap(0, 1, 2).before(ML_COMMENTRule) + c.setLinewrap(0, 1, 1).after(ML_COMMENTRule) } + + def withinKeywordPairs(FormattingConfig.NoSpaceLocator locator, String leftKW, String rightKW) { + for (pair : findKeywordPairs(leftKW, rightKW)) { + locator.after(pair.first) + locator.before(pair.second) + } + } + + def around(AbstractFormattingConfig.ElementLocator locator, String ... listKW) { + for (keyword : findKeywords(listKW)) { + locator.around(keyword) + } + } + + def after(AbstractFormattingConfig.ElementLocator locator, String ... listKW) { + for (keyword : findKeywords(listKW)) { + locator.after(keyword) + } + } + + def before(AbstractFormattingConfig.ElementLocator locator, String ... listKW) { + for (keyword : findKeywords(listKW)) { + locator.before(keyword) + } + } } 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 00000000000..f3a862dafd1 --- /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.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()); + 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/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml index 2a910cfe6b7..8810420bb2d 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-model-script openhab-core-ui mvn:org.openhab.core.bundles/org.openhab.core.model.yaml/${project.version} openhab.tp;filter:="(feature=jackson)" From db0d37f32205d097e1d0e5ad4aba435695f01067 Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Sat, 4 Apr 2026 20:12:34 +0200 Subject: [PATCH 06/20] Create rule template YAML converter Signed-off-by: Ravi Nadahar --- .../converter/YamlRuleTemplateConverter.java | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/rules/converter/YamlRuleTemplateConverter.java 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 00000000000..f796c143807 --- /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.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()); + 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); + } +} From 0accc5a13f59f4a0ee122ee0117e1b06665d35f3 Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Sat, 14 Mar 2026 15:28:58 +0100 Subject: [PATCH 07/20] Implement rule and rule template conversion in the REST API Signed-off-by: Ravi Nadahar --- bundles/org.openhab.core.io.rest.core/pom.xml | 5 + .../rest/core/fileformat/FileFormatDTO.java | 8 +- .../fileformat/FileFormatResource.java | 476 +++++++++++++++++- 3 files changed, 473 insertions(+), 16 deletions(-) diff --git a/bundles/org.openhab.core.io.rest.core/pom.xml b/bundles/org.openhab.core.io.rest.core/pom.xml index 4a53460b693..d5843f378fc 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 23021b068ce..ad44057beef 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 @@ 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 @@ /** * 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 d98386cabc5..853aa12f1c4 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 @@ -23,9 +23,11 @@ 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 +43,26 @@ 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,6 +71,8 @@ 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; @@ -215,6 +235,69 @@ 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 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 +380,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,6 +436,24 @@ 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 SemanticTagRegistry semanticTagRegistry; @@ -322,6 +466,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 +476,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 +497,8 @@ public FileFormatResource(// 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 +511,8 @@ public FileFormatResource(// this.thingTypeRegistry = thingTypeRegistry; this.channelTypeRegistry = channelTypeRegistry; this.configDescRegistry = configDescRegistry; + this.ruleRegistry = ruleRegistry; + this.templateRegistry = templateRegistry; this.sitemapFactory = sitemapFactory; this.sitemapRegistry = sitemapRegistry; } @@ -421,6 +575,42 @@ protected void removeThingParser(ThingParser thingParser) { 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 +719,111 @@ public Response createFileFormatForThings(final @Context HttpHeaders httpHeaders 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 ID '" + 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(new String(outputStream.toByteArray(), StandardCharsets.UTF_8)).build(); + } + + @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("createFileFormatForRules: mediaType = {}, ruleUIDs = {}", 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 ID '" + 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(new String(outputStream.toByteArray(), StandardCharsets.UTF_8)).build(); + } + @POST @RolesAllowed({ Role.ADMIN }) @Path("/sitemaps") @@ -633,21 +928,24 @@ public Response createFileFormatForSemanticTags(final @Context HttpHeaders httpH @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 +955,20 @@ public Response create(final @Context HttpHeaders httpHeaders, 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 +994,20 @@ public Response create(final @Context HttpHeaders httpHeaders, 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 + "'"); + } else 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 +1029,23 @@ public Response create(final @Context HttpHeaders httpHeaders, 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 +1055,10 @@ public Response create(final @Context HttpHeaders httpHeaders, 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 +1073,8 @@ public Response create(final @Context HttpHeaders httpHeaders, @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 +1086,7 @@ public Response parse(final @Context HttpHeaders httpHeaders, @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 +1099,15 @@ public Response parse(final @Context HttpHeaders httpHeaders, 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 +1151,22 @@ public Response parse(final @Context HttpHeaders httpHeaders, 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 +1194,7 @@ public Response parse(final @Context HttpHeaders httpHeaders, 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 +1207,32 @@ public Response parse(final @Context HttpHeaders httpHeaders, 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 +1244,7 @@ public Response parse(final @Context HttpHeaders httpHeaders, 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 +1261,7 @@ public Response parse(final @Context HttpHeaders httpHeaders, .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 +1269,7 @@ public Response parse(final @Context HttpHeaders httpHeaders, } private String newIdForSerialization() { - return GEN_ID_PATTERN.formatted(++counter); + return String.format(Locale.ROOT, GEN_ID_PATTERN, counter.incrementAndGet()); } /* @@ -1056,6 +1439,26 @@ private Thing simulateThing(DiscoveryResult result, ThingType thingType) { }; } + 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 +1491,26 @@ private Thing simulateThing(DiscoveryResult result, ThingType thingType) { }; } + 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 +1541,8 @@ private List getThingsOrDiscoveryResult(List thingUIDs) { } 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 +1625,16 @@ private boolean convertFromFileFormatDTO(FileFormatDTO data, List t } } } + 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 +1672,8 @@ private boolean convertFromFileFormatDTO(FileFormatDTO data, List t 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 +1720,18 @@ private ExtendedFileFormatDTO convertToFileFormatDTO(Collection tag 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 -> { From ebf1b0a89c7a87700bff2273765de04a980cb033 Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Sun, 12 Apr 2026 18:21:03 +0200 Subject: [PATCH 08/20] Handle DSL conditions Signed-off-by: Ravi Nadahar --- .../internal/converter/DslRuleConverter.java | 213 +++++++++++++++++- .../rule/formatting/RulesFormatter.xtend | 5 + 2 files changed, 215 insertions(+), 3 deletions(-) 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 index c97d6522a61..769a265b7bf 100644 --- 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 @@ -41,6 +41,7 @@ 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; @@ -49,13 +50,19 @@ 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; @@ -67,21 +74,29 @@ 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; @@ -183,14 +198,25 @@ public List> checkSerializability(Collection } } - if (!rule.getConditions().isEmpty()) { - errors.add("has conditions"); + 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.getConfiguration().get("type") instanceof String type) { + 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"); @@ -407,6 +433,10 @@ private org.openhab.core.model.rule.rules.Rule buildModelRule(Rule rule, tags.add(tag); } + for (Condition condition : rule.getConditions()) { + model.getConditions().add(buildModelCondition(condition)); + } + for (Trigger trigger : rule.getTriggers()) { model.getEventtrigger().add(buildModelTrigger(trigger)); } @@ -616,6 +646,183 @@ private EventTrigger buildModelTrigger(Trigger trigger) throws SerializationExce } } + 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) { + todCond.setStart(start); + } + value = condition.getConfiguration().get(TimeOfDayConditionHandler.CFG_END_TIME); + if (value instanceof String end) { + todCond.setEnd(end); + } + 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()) { 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 index ef1a2844451..d917c02e6c6 100644 --- 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 @@ -43,8 +43,11 @@ class RulesFormatter extends AbstractDeclarativeFormatter { c.setLinewrap(1, 1, 2).after(XBlockExpressionRule) c.setLinewrap(1, 2, 2).before(getRuleAccess.ruleKeyword_0) c.setLinewrap(1, 1, 2).after(getRuleAccess.orKeyword_6_0) + c.setLinewrap(1, 1, 2).after(getRuleAccess.andKeyword_7_4_0) c.setLinewrap(1, 1, 2).before(getRuleAccess.whenKeyword_4) c.setLinewrap(1, 1, 2).after(getRuleAccess.whenKeyword_4) + c.setLinewrap(1, 1, 2).before(getRuleAccess.butKeyword_7_0) + c.setLinewrap(1, 1, 2).after(getRuleAccess.ifKeyword_7_2) c.setLinewrap(1, 1, 2).before(getRuleAccess.thenKeyword_8) c.setLinewrap(1, 1, 2).after(getRuleAccess.thenKeyword_8) c.setLinewrap(1, 1, 2).before(getRuleAccess.endKeyword_10) @@ -52,6 +55,8 @@ class RulesFormatter extends AbstractDeclarativeFormatter { c.setIndentationIncrement.after("{") c.setIndentationDecrement.before("}") c.setIndentationIncrement.after(getRuleAccess.whenKeyword_4) + c.setIndentationDecrement.before(getRuleAccess.butKeyword_7_0) + c.setIndentationIncrement.after(getRuleAccess.ifKeyword_7_2) c.setIndentationDecrement.before(getRuleAccess.thenKeyword_8) c.setIndentationIncrement.after(getRuleAccess.thenKeyword_8) c.setIndentationDecrement.before(getRuleAccess.endKeyword_10) From 58b51d8908d7cda0fda31444213207b3ab6628a3 Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Wed, 13 May 2026 17:29:03 +0200 Subject: [PATCH 09/20] Create check serializability endpoint Signed-off-by: Ravi Nadahar --- .../fileformat/FileFormatResource.java | 143 +++++++++++++++++- .../rule/SerializabilityResultsDTO.java | 34 +++++ 2 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/rule/SerializabilityResultsDTO.java 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 853aa12f1c4..0af80e43b25 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 @@ 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; @@ -79,6 +80,7 @@ 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; @@ -88,6 +90,7 @@ 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; @@ -137,9 +140,15 @@ 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; @@ -264,6 +273,74 @@ public class FileFormatResource implements RESTResource { 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: @@ -456,6 +533,7 @@ public Family getFamily() { 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; @@ -756,7 +834,7 @@ public Response createFileFormatForRules(@Context HttpHeaders httpHeaders, Rule rule = ruleRegistry.get(ruleUID); if (rule == null) { return JSONResponse.createErrorResponse(Response.Status.NOT_FOUND, - "Rule with ID '" + ruleUID + "' not found in the rule registry!"); + "Rule with UID '" + ruleUID + "' not found in the rule registry!"); } rules.add(rule); } @@ -772,6 +850,64 @@ public Response createFileFormatForRules(@Context HttpHeaders httpHeaders, return Response.ok(new String(outputStream.toByteArray(), 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.") }) + 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") @@ -808,7 +944,7 @@ public Response createFileFormatForRuleTemplates(@Context HttpHeaders httpHeader RuleTemplate template = templateRegistry.get(templateUID); if (template == null) { return JSONResponse.createErrorResponse(Response.Status.NOT_FOUND, - "Rule template with ID '" + templateUID + "' not found in the registry!"); + "Rule template with UID '" + templateUID + "' not found in the registry!"); } templates.add(template); } @@ -1846,4 +1982,7 @@ private URI getConfigDescriptionURI(ChannelUID channelUID) { 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 00000000000..c5a0cebc039 --- /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; + } +} From 3af3bf79920f4950e0dcf807d5999818509c325b Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Sun, 26 Apr 2026 05:02:33 +0200 Subject: [PATCH 10/20] Don't deserialize TemplateState - deduce it from templateUID Signed-off-by: Ravi Nadahar --- .../org/openhab/core/automation/util/RuleBuilder.java | 10 ++++++---- .../core/model/yaml/internal/rules/YamlRuleDTO.java | 6 ++++-- .../yaml/internal/rules/YamlRuleProviderTest.java | 8 ++++---- 3 files changed, 14 insertions(+), 10 deletions(-) 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 d12a316f06b..ce6c82b7874 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 @@ protected RuleBuilder(Rule rule) { 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 static RuleBuilder create(String ruleUid) { * @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 static RuleBuilder create(String ruleUid, Rule r) { /** * 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.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 5e84e3317d9..d3a3d56c18f 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 @@ -167,8 +167,10 @@ public void setId(@NonNull String id) { try { partial = mapper.treeToValue(node, YamlPartialRuleDTO.class); result.uid = partial.uid; - result.template = partial.template; - result.templateState = TemplateState.typeOf(partial.templateState); + String templateUID = partial.template; + result.template = templateUID; + result.templateState = templateUID == null || templateUID.isBlank() ? TemplateState.NO_TEMPLATE + : TemplateState.PENDING; result.label = partial.label; result.tags = partial.tags; result.description = partial.description; 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 8ac6a6c0f03..e2f9b3dbdec 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 void mixedRulesTest() throws IOException { 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 void mixedRulesTest() throws IOException { 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 void mixedRulesTest() throws IOException { 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 void fullRuleTest() throws IOException { 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))); From 41583c5796f823b44615b633d24ffce69425e793 Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Wed, 6 May 2026 16:43:01 +0200 Subject: [PATCH 11/20] Add GenericEventTrigger to ModuleTypeAliases Signed-off-by: Ravi Nadahar --- .../core/model/yaml/internal/rules/ModuleTypeAliases.java | 2 ++ 1 file changed, 2 insertions(+) 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 4a0a41e1535..2a1bdd91bc1 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.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 }, // From a96e2cb34517f18e14afab3239f37137a60b8a4a Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Wed, 13 May 2026 19:44:54 +0200 Subject: [PATCH 12/20] Fix JavaDoc errors Signed-off-by: Ravi Nadahar --- .../org/openhab/core/automation/converter/RuleSerializer.java | 2 +- .../core/automation/converter/RuleTemplateSerializer.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index b1f956071af..d8a8f73813a 100644 --- 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 @@ -45,7 +45,7 @@ public interface RuleSerializer extends ObjectSerializer { * * @param id the identifier of the {@link Rule} format generation. * @param rules the {@link List} of {@link Rule}s to serialize. - * @param the option that determines how to serialize the {@link Rule}s. + * @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) 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 index a65e820060c..854d51fc346 100644 --- 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 @@ -47,7 +47,7 @@ public interface RuleTemplateSerializer extends ObjectSerializer { * * @param id the identifier of the {@link RuleTemplate} format generation. * @param templates the {@link List} of {@link RuleTemplate}s to serialize. - * @param the option that determines how to serialize the {@link RuleTemplate}s. + * @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) From 7f3e04e59e58e4c21c1937398a0ae1078ff64ac2 Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Thu, 14 May 2026 06:51:35 +0200 Subject: [PATCH 13/20] Allow triggerless DSL rules Signed-off-by: Ravi Nadahar --- .../internal/converter/DslRuleConverter.java | 15 +++------ .../rule/formatting/RulesFormatter.xtend | 32 +++++++++---------- 2 files changed, 21 insertions(+), 26 deletions(-) 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 index 769a265b7bf..79d199fc833 100644 --- 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 @@ -185,16 +185,11 @@ public List> checkSerializability(Collection if ((s = rule.getDescription()) != null && !s.isBlank()) { errors.add("has a description"); } - List triggers = rule.getTriggers(); - if (triggers.isEmpty()) { - errors.add("has no triggers"); - } else { - for (Trigger trigger : triggers) { - try { - buildModelTrigger(trigger); - } catch (SerializationException e) { - errors.add("trigger '" + trigger.getId() + "': " + e.getMessage()); - } + for (Trigger trigger : rule.getTriggers()) { + try { + buildModelTrigger(trigger); + } catch (SerializationException e) { + errors.add("trigger '" + trigger.getId() + "': " + e.getMessage()); } } 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 index d917c02e6c6..5bc32f7fcfd 100644 --- 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 @@ -41,25 +41,25 @@ class RulesFormatter extends AbstractDeclarativeFormatter { c.setLinewrap(1, 1, 2).after(XImportDeclarationRule) c.setLinewrap(1, 1, 2).after(XFunctionTypeRefRule) c.setLinewrap(1, 1, 2).after(XBlockExpressionRule) - c.setLinewrap(1, 2, 2).before(getRuleAccess.ruleKeyword_0) - c.setLinewrap(1, 1, 2).after(getRuleAccess.orKeyword_6_0) - c.setLinewrap(1, 1, 2).after(getRuleAccess.andKeyword_7_4_0) - c.setLinewrap(1, 1, 2).before(getRuleAccess.whenKeyword_4) - c.setLinewrap(1, 1, 2).after(getRuleAccess.whenKeyword_4) - c.setLinewrap(1, 1, 2).before(getRuleAccess.butKeyword_7_0) - c.setLinewrap(1, 1, 2).after(getRuleAccess.ifKeyword_7_2) - c.setLinewrap(1, 1, 2).before(getRuleAccess.thenKeyword_8) - c.setLinewrap(1, 1, 2).after(getRuleAccess.thenKeyword_8) - c.setLinewrap(1, 1, 2).before(getRuleAccess.endKeyword_10) + c.setLinewrap(1, 2, 2).before(getRuleAccess.getRuleKeyword_0()) + c.setLinewrap(1, 1, 2).after(getRuleAccess.getOrKeyword_4_2_0()) + c.setLinewrap(1, 1, 2).after(getRuleAccess.getAndKeyword_5_4_0()) + c.setLinewrap(1, 1, 2).before(getRuleAccess.getWhenKeyword_4_0()) + c.setLinewrap(1, 1, 2).after(getRuleAccess.getWhenKeyword_4_0()) + c.setLinewrap(1, 1, 2).before(getRuleAccess.getButKeyword_5_0()) + c.setLinewrap(1, 1, 2).after(getRuleAccess.getIfKeyword_5_2()) + c.setLinewrap(1, 1, 2).before(getRuleAccess.getThenKeyword_6()) + c.setLinewrap(1, 1, 2).after(getRuleAccess.getThenKeyword_6()) + c.setLinewrap(1, 1, 2).before(getRuleAccess.getEndKeyword_8()) c.setIndentationIncrement.after("{") c.setIndentationDecrement.before("}") - c.setIndentationIncrement.after(getRuleAccess.whenKeyword_4) - c.setIndentationDecrement.before(getRuleAccess.butKeyword_7_0) - c.setIndentationIncrement.after(getRuleAccess.ifKeyword_7_2) - c.setIndentationDecrement.before(getRuleAccess.thenKeyword_8) - c.setIndentationIncrement.after(getRuleAccess.thenKeyword_8) - c.setIndentationDecrement.before(getRuleAccess.endKeyword_10) + c.setIndentationIncrement.after(getRuleAccess.getWhenKeyword_4_0()) + c.setIndentationDecrement.before(getRuleAccess.getButKeyword_5_0()) + c.setIndentationIncrement.after(getRuleAccess.getIfKeyword_5_2()) + c.setIndentationDecrement.before(getRuleAccess.getThenKeyword_6()) + c.setIndentationIncrement.after(getRuleAccess.getThenKeyword_6()) + c.setIndentationDecrement.before(getRuleAccess.getEndKeyword_8()) c.setLinewrap().before("}") From 913d1fee7c3a5eb538f519b64a3df6e9d2bf6c9b Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Thu, 14 May 2026 15:07:00 +0200 Subject: [PATCH 14/20] Fix negative indentation issue during DSL formatting Signed-off-by: Ravi Nadahar --- .../core/model/rule/GenerateRules.mwe2 | 3 + .../RuleFormattingConfigBasedStream.java | 51 +++++++ .../model/rule/formatting/RulesFormatter.java | 142 ++++++++++++++++++ .../rule/formatting/RulesFormatter.xtend | 103 ------------- 4 files changed, 196 insertions(+), 103 deletions(-) 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 diff --git a/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/GenerateRules.mwe2 b/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/GenerateRules.mwe2 index 3506d858839..71024fe961c 100644 --- a/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/GenerateRules.mwe2 +++ b/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/GenerateRules.mwe2 @@ -57,6 +57,9 @@ Workflow { serializer = { generateStub = false } + formatter = { + generateXtendStub = false + } validator = { generateXtendStub = false } 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 00000000000..dd98c5cfbae --- /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 00000000000..c61055b585f --- /dev/null +++ b/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/formatting/RulesFormatter.java @@ -0,0 +1,142 @@ +/* + * 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(), "(", ")"); + withinKeywordPairs(c.setNoSpace(), "[", "]"); + around(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 5bc32f7fcfd..00000000000 --- a/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/formatting/RulesFormatter.xtend +++ /dev/null @@ -1,103 +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 com.google.inject.Inject -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.xtext.XtextFormatter -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 XtextFormatter} as an example - */ -class RulesFormatter extends AbstractDeclarativeFormatter { - - @Inject extension RulesGrammarAccess - - override protected void configureFormatting(FormattingConfig c) { - - c.setLinewrap(1, 1, 2).before(ruleModelRule) - c.setLinewrap(1, 1, 2).after(ruleModelRule) - c.setLinewrap(1, 1, 2).after(XImportDeclarationRule) - c.setLinewrap(1, 1, 2).after(XFunctionTypeRefRule) - c.setLinewrap(1, 1, 2).after(XBlockExpressionRule) - c.setLinewrap(1, 2, 2).before(getRuleAccess.getRuleKeyword_0()) - c.setLinewrap(1, 1, 2).after(getRuleAccess.getOrKeyword_4_2_0()) - c.setLinewrap(1, 1, 2).after(getRuleAccess.getAndKeyword_5_4_0()) - c.setLinewrap(1, 1, 2).before(getRuleAccess.getWhenKeyword_4_0()) - c.setLinewrap(1, 1, 2).after(getRuleAccess.getWhenKeyword_4_0()) - c.setLinewrap(1, 1, 2).before(getRuleAccess.getButKeyword_5_0()) - c.setLinewrap(1, 1, 2).after(getRuleAccess.getIfKeyword_5_2()) - c.setLinewrap(1, 1, 2).before(getRuleAccess.getThenKeyword_6()) - c.setLinewrap(1, 1, 2).after(getRuleAccess.getThenKeyword_6()) - c.setLinewrap(1, 1, 2).before(getRuleAccess.getEndKeyword_8()) - - c.setIndentationIncrement.after("{") - c.setIndentationDecrement.before("}") - c.setIndentationIncrement.after(getRuleAccess.getWhenKeyword_4_0()) - c.setIndentationDecrement.before(getRuleAccess.getButKeyword_5_0()) - c.setIndentationIncrement.after(getRuleAccess.getIfKeyword_5_2()) - c.setIndentationDecrement.before(getRuleAccess.getThenKeyword_6()) - c.setIndentationIncrement.after(getRuleAccess.getThenKeyword_6()) - c.setIndentationDecrement.before(getRuleAccess.getEndKeyword_8()) - - c.setLinewrap().before("}") - - c.setNoSpace().withinKeywordPairs("(", ")") - c.setNoSpace().withinKeywordPairs("[", "]") - c.setNoSpace().around("=") - c.setNoSpace().around(".") - c.setNoSpace().before(",") - - c.autoLinewrap = 120 - - c.setLinewrap(0, 1, 2).before(SL_COMMENTRule) - c.setLinewrap(0, 1, 2).before(ML_COMMENTRule) - c.setLinewrap(0, 1, 1).after(ML_COMMENTRule) - } - - def withinKeywordPairs(FormattingConfig.NoSpaceLocator locator, String leftKW, String rightKW) { - for (pair : findKeywordPairs(leftKW, rightKW)) { - locator.after(pair.first) - locator.before(pair.second) - } - } - - def around(AbstractFormattingConfig.ElementLocator locator, String ... listKW) { - for (keyword : findKeywords(listKW)) { - locator.around(keyword) - } - } - - def after(AbstractFormattingConfig.ElementLocator locator, String ... listKW) { - for (keyword : findKeywords(listKW)) { - locator.after(keyword) - } - } - - def before(AbstractFormattingConfig.ElementLocator locator, String ... listKW) { - for (keyword : findKeywords(listKW)) { - locator.before(keyword) - } - } -} From 856542bcda9b1d6b18b497925b9a3b7db3187e9d Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Fri, 15 May 2026 00:50:23 +0200 Subject: [PATCH 15/20] Add rule configuration to "rule summary" Signed-off-by: Ravi Nadahar --- .../org/openhab/core/automation/rest/internal/RuleResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 44b31b0eb41..8d856319428 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 Response get(@Context SecurityContext securityContext, @Context Request r .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(); From f64dea15f391ede0a8c04892e22d03b383e25142 Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Fri, 15 May 2026 18:44:32 +0200 Subject: [PATCH 16/20] Fix rule template REST API schema Signed-off-by: Ravi Nadahar --- .../automation/rest/internal/TemplateResource.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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 04673cf1a20..df9cf2b1b04 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.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 TemplateResource( // @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 Response getAll( @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, From 1131c5856869a6c510d164b08e7f8182f9afea85 Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Sun, 17 May 2026 05:23:55 +0200 Subject: [PATCH 17/20] Address review feedback Signed-off-by: Ravi Nadahar --- .../internal/converter/DslRuleConverter.java | 27 +++++++------------ .../core/model/rule/GenerateRules.mwe2 | 3 --- 2 files changed, 9 insertions(+), 21 deletions(-) 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 index 79d199fc833..552370dd131 100644 --- 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 @@ -459,9 +459,8 @@ private EventTrigger buildModelTrigger(Trigger trigger) throws SerializationExce result.setLevel(level); return result; } - } else { - throw new SerializationException("Invalid trigger: " + trigger); } + throw new SerializationException("Invalid trigger: " + trigger); case ItemCommandTriggerHandler.MODULE_TYPE_ID: value = trigger.getConfiguration().get(ItemCommandTriggerHandler.CFG_ITEMNAME); if (value instanceof String str) { @@ -472,9 +471,8 @@ private EventTrigger buildModelTrigger(Trigger trigger) throws SerializationExce result.setCommand(createValidCommand(command)); } return result; - } else { - throw new SerializationException("Invalid trigger: " + trigger); } + throw new SerializationException("Invalid trigger: " + trigger); case GroupCommandTriggerHandler.MODULE_TYPE_ID: value = trigger.getConfiguration().get(GroupCommandTriggerHandler.CFG_GROUPNAME); if (value instanceof String str) { @@ -485,9 +483,8 @@ private EventTrigger buildModelTrigger(Trigger trigger) throws SerializationExce result.setCommand(createValidCommand(command)); } return result; - } else { - throw new SerializationException("Invalid trigger: " + trigger); } + throw new SerializationException("Invalid trigger: " + trigger); case ItemStateTriggerHandler.UPDATE_MODULE_TYPE_ID: value = trigger.getConfiguration().get(ItemStateTriggerHandler.CFG_ITEMNAME); if (value instanceof String str) { @@ -498,9 +495,8 @@ private EventTrigger buildModelTrigger(Trigger trigger) throws SerializationExce result.setState(createValidState(state)); } return result; - } else { - throw new SerializationException("Invalid trigger: " + trigger); } + throw new SerializationException("Invalid trigger: " + trigger); case ItemStateTriggerHandler.CHANGE_MODULE_TYPE_ID: value = trigger.getConfiguration().get(ItemStateTriggerHandler.CFG_ITEMNAME); if (value instanceof String str) { @@ -515,9 +511,8 @@ private EventTrigger buildModelTrigger(Trigger trigger) throws SerializationExce result.setOldState(createValidState(state)); } return result; - } else { - throw new SerializationException("Invalid trigger: " + trigger); } + throw new SerializationException("Invalid trigger: " + trigger); case GroupStateTriggerHandler.UPDATE_MODULE_TYPE_ID: value = trigger.getConfiguration().get(GroupStateTriggerHandler.CFG_GROUPNAME); if (value instanceof String str) { @@ -528,9 +523,8 @@ private EventTrigger buildModelTrigger(Trigger trigger) throws SerializationExce result.setState(createValidState(state)); } return result; - } else { - throw new SerializationException("Invalid trigger: " + trigger); } + throw new SerializationException("Invalid trigger: " + trigger); case GroupStateTriggerHandler.CHANGE_MODULE_TYPE_ID: value = trigger.getConfiguration().get(GroupStateTriggerHandler.CFG_GROUPNAME); if (value instanceof String str) { @@ -545,9 +539,8 @@ private EventTrigger buildModelTrigger(Trigger trigger) throws SerializationExce result.setOldState(createValidState(state)); } return result; - } else { - throw new SerializationException("Invalid trigger: " + trigger); } + throw new SerializationException("Invalid trigger: " + trigger); case GenericCronTriggerHandler.MODULE_TYPE_ID: value = trigger.getConfiguration().get(GenericCronTriggerHandler.CFG_CRON_EXPRESSION); if (value instanceof String str) { @@ -560,9 +553,8 @@ private EventTrigger buildModelTrigger(Trigger trigger) throws SerializationExce result.setCron(str); } return result; - } else { - throw new SerializationException("Invalid trigger: " + trigger); } + throw new SerializationException("Invalid trigger: " + trigger); case TimeOfDayTriggerHandler.MODULE_TYPE_ID: value = trigger.getConfiguration().get(TimeOfDayTriggerHandler.CFG_TIME); if (value instanceof String str) { @@ -575,9 +567,8 @@ private EventTrigger buildModelTrigger(Trigger trigger) throws SerializationExce result.setTime(str); } return result; - } else { - throw new SerializationException("Invalid trigger: " + trigger); } + throw new SerializationException("Invalid trigger: " + trigger); case DateTimeTriggerHandler.MODULE_TYPE_ID: value = trigger.getConfiguration().get(DateTimeTriggerHandler.CONFIG_ITEM_NAME); if (value instanceof String str) { diff --git a/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/GenerateRules.mwe2 b/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/GenerateRules.mwe2 index 71024fe961c..3506d858839 100644 --- a/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/GenerateRules.mwe2 +++ b/bundles/org.openhab.core.model.rule/src/org/openhab/core/model/rule/GenerateRules.mwe2 @@ -57,9 +57,6 @@ Workflow { serializer = { generateStub = false } - formatter = { - generateXtendStub = false - } validator = { generateXtendStub = false } From b4f2c902a695395399f14b57df5385f107a62be5 Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Sun, 17 May 2026 14:38:15 +0200 Subject: [PATCH 18/20] Address review feedback Signed-off-by: Ravi Nadahar --- .../rule/runtime/internal/converter/DslRuleConverter.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 index 552370dd131..64b2b12e29a 100644 --- 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 @@ -454,11 +454,10 @@ private EventTrigger buildModelTrigger(Trigger trigger) throws SerializationExce int level = num.intValue(); if (level == 40) { return factory.createSystemOnStartupTrigger(); - } else { - SystemStartlevelTrigger result = factory.createSystemStartlevelTrigger(); - result.setLevel(level); - return result; } + SystemStartlevelTrigger result = factory.createSystemStartlevelTrigger(); + result.setLevel(level); + return result; } throw new SerializationException("Invalid trigger: " + trigger); case ItemCommandTriggerHandler.MODULE_TYPE_ID: From 75012d3362cabd8ff88c6b35fd44ca3890d1be93 Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Thu, 21 May 2026 04:38:24 +0200 Subject: [PATCH 19/20] Handle single digit hour specification during DSL serialization Signed-off-by: Ravi Nadahar --- .../internal/converter/DslRuleConverter.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) 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 index 64b2b12e29a..c1401f24a34 100644 --- 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 @@ -123,6 +123,7 @@ public class DslRuleConverter implements RuleSerializer, RuleParser { 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-6]\\d)$"); private static final Pattern INDEX_PATTERN = Pattern.compile("-(?\\d+)$"); private final Set enumStates; private final Set enumCommands; @@ -558,12 +559,14 @@ private EventTrigger buildModelTrigger(Trigger trigger) throws SerializationExce value = trigger.getConfiguration().get(TimeOfDayTriggerHandler.CFG_TIME); if (value instanceof String str) { TimerTrigger result = factory.createTimerTrigger(); - if ("12:00".equals(str)) { + 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(str)) { + } else if ("00:00".equals(time)) { result.setTime("midnight"); } else { - result.setTime(str); + result.setTime(time); } return result; } @@ -643,11 +646,15 @@ private org.openhab.core.model.rule.rules.Condition buildModelCondition(Conditio TimeOfDayCondition todCond = factory.createTimeOfDayCondition(); value = condition.getConfiguration().get(TimeOfDayConditionHandler.CFG_START_TIME); if (value instanceof String start) { - todCond.setStart(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) { - todCond.setEnd(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: From de035ca46767d6cbce6b1bf0d58e809a803adaff Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Tue, 2 Jun 2026 18:23:49 +0200 Subject: [PATCH 20/20] Address review comments Signed-off-by: Ravi Nadahar --- .../core/automation/converter/RuleTemplateSerializer.java | 3 +-- .../rest/core/internal/fileformat/FileFormatResource.java | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) 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 index 854d51fc346..69a6d7e6e44 100644 --- 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 @@ -18,7 +18,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.automation.Rule; import org.openhab.core.automation.converter.RuleSerializer.RuleSerializationOption; import org.openhab.core.automation.template.RuleTemplate; import org.openhab.core.converter.ObjectSerializer; @@ -26,7 +25,7 @@ import org.openhab.core.io.dto.SerializationException; /** - * {@link RuleTemplateSerializer} is the interface to implement by any file generator for {@link Rule} object. + * {@link RuleTemplateSerializer} is the interface to implement by any file generator for {@link RuleTemplate} object. * * @author Ravi Nadahar - Initial contribution */ 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 0af80e43b25..1b744e4e5a2 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 @@ -847,7 +847,7 @@ public Response createFileFormatForRules(@Context HttpHeaders httpHeaders, return JSONResponse.createErrorResponse(UNPROCESSABLE_ENTITY, e.getMessage()); } serializer.generateFormat(genId, outputStream); - return Response.ok(new String(outputStream.toByteArray(), StandardCharsets.UTF_8)).build(); + return Response.ok(outputStream.toString(StandardCharsets.UTF_8)).build(); } @POST @@ -924,7 +924,7 @@ public Response createFileFormatForRuleTemplates(@Context HttpHeaders httpHeader @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("createFileFormatForRules: mediaType = {}, ruleUIDs = {}", acceptHeader, templateUIDs); + logger.debug("createFileFormatForRuleTemplates: mediaType = {}, templateUIDs = {}", acceptHeader, templateUIDs); RuleTemplateSerializer serializer = getRuleTemplateSerializer(acceptHeader); if (serializer == null) { return JSONResponse.createErrorResponse(Response.Status.UNSUPPORTED_MEDIA_TYPE, @@ -957,7 +957,7 @@ public Response createFileFormatForRuleTemplates(@Context HttpHeaders httpHeader return JSONResponse.createErrorResponse(UNPROCESSABLE_ENTITY, e.getMessage()); } serializer.generateFormat(genId, outputStream); - return Response.ok(new String(outputStream.toByteArray(), StandardCharsets.UTF_8)).build(); + return Response.ok(outputStream.toString(StandardCharsets.UTF_8)).build(); } @POST