Rule and RuleTemplate file format conversion (#5572)

* Create ObjectParser and ObjectSerializer interfaces for rules and rule templates

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Embed DSL rule source with the Rule

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Detect shared context DSL rules

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Update YAML DTOs to support serialization

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Create rule converters

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Create rule template YAML converter

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Implement rule and rule template conversion in the REST API

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Handle DSL conditions

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Create check serializability endpoint

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Don't deserialize TemplateState - deduce it from templateUID

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Add GenericEventTrigger to ModuleTypeAliases

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Fix JavaDoc errors

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Allow triggerless DSL rules

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Fix negative indentation issue during DSL formatting

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Add rule configuration to "rule summary"

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Fix rule template REST API schema

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Address review feedback

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Address review feedback

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Handle single digit hour specification during DSL serialization

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Address review comments

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Address review comments

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Apply review suggestion

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Remove seemingly needless import

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Apply review derived fixes

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Review fixes

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Address review formatter feedback

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

* Fix YAML bundle feature dependency

Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>

---------

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