Skip to content

Commit 3e7ee82

Browse files
committed
Sitemap YAML serialization and parsing
This PR builds on openhab#5459 and partially replaces openhab#4945 (covering YAML). It also makes further steps towards openhab#5007 Signed-off-by: Laurent Garnier <lg.hc@free.fr>
1 parent 5fb91ce commit 3e7ee82

File tree

15 files changed

+1773
-37
lines changed

15 files changed

+1773
-37
lines changed

bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/fileformat/FileFormatResource.java

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,19 @@ public class FileFormatResource implements RESTResource {
261261
}
262262
""";
263263

264+
private static final String YAML_SITEMAPS_EXAMPLE = """
265+
version: 1
266+
sitemaps:
267+
MySitemap:
268+
label: My Sitemap
269+
widgets:
270+
- type: Frame
271+
widgets:
272+
- type: Input
273+
item: MyItem
274+
label: My Input
275+
""";
276+
264277
private static final String GEN_ID_PATTERN = "gen_file_format_%d";
265278

266279
private final Logger logger = LoggerFactory.getLogger(FileFormatResource.class);
@@ -463,11 +476,12 @@ public Response createFileFormatForThings(final @Context HttpHeaders httpHeaders
463476
@RolesAllowed({ Role.ADMIN })
464477
@Path("/sitemaps")
465478
@Consumes(MediaType.APPLICATION_JSON)
466-
@Produces({ "text/vnd.openhab.dsl.sitemap" })
479+
@Produces({ "text/vnd.openhab.dsl.sitemap", "application/yaml" })
467480
@Operation(operationId = "createFileFormatForSitemaps", summary = "Create file format for a list of sitemaps in registry.", security = {
468481
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
469482
@ApiResponse(responseCode = "200", description = "OK", content = {
470-
@Content(mediaType = "text/vnd.openhab.dsl.sitemap", schema = @Schema(example = DSL_SITEMAPS_EXAMPLE)) }),
483+
@Content(mediaType = "text/vnd.openhab.dsl.sitemap", schema = @Schema(example = DSL_SITEMAPS_EXAMPLE)),
484+
@Content(mediaType = "application/yaml", schema = @Schema(example = YAML_SITEMAPS_EXAMPLE)) }),
471485
@ApiResponse(responseCode = "400", description = "Payload invalid."),
472486
@ApiResponse(responseCode = "404", description = "One or more sitemaps not found in registry."),
473487
@ApiResponse(responseCode = "415", description = "Unsupported media type.") })
@@ -594,10 +608,15 @@ public Response create(final @Context HttpHeaders httpHeaders,
594608
itemSerializer.setItemsToBeSerialized(genId, items,
595609
hideChannelLinksAndMetadata ? List.of() : metadata, stateFormatters, hideDefaultParameters);
596610
}
611+
if (sitemapSerializer != null) {
612+
sitemapSerializer.setSitemapsToBeSerialized(genId, sitemaps);
613+
}
597614
if (thingSerializer != null) {
598615
thingSerializer.generateFormat(genId, outputStream);
599616
} else if (itemSerializer != null) {
600617
itemSerializer.generateFormat(genId, outputStream);
618+
} else if (sitemapSerializer != null) {
619+
sitemapSerializer.generateFormat(genId, outputStream);
601620
}
602621
break;
603622
default:
@@ -641,6 +660,7 @@ public Response parse(final @Context HttpHeaders httpHeaders,
641660
SitemapParser sitemapParser = getSitemapParser(contentTypeHeader);
642661
String modelName = null;
643662
String modelName2 = null;
663+
String modelName3 = null;
644664
switch (contentTypeHeader) {
645665
case "text/vnd.openhab.dsl.thing":
646666
if (thingParser == null) {
@@ -684,13 +704,13 @@ public Response parse(final @Context HttpHeaders httpHeaders,
684704
return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE)
685705
.entity("Unsupported content type '" + contentTypeHeader + "'!").build();
686706
}
687-
modelName2 = sitemapParser.startParsingFormat(input, errors, warnings);
688-
if (modelName2 == null) {
707+
modelName3 = sitemapParser.startParsingFormat(input, errors, warnings);
708+
if (modelName3 == null) {
689709
return Response.status(Response.Status.BAD_REQUEST).entity(String.join("\n", errors)).build();
690710
}
691-
sitemaps = sitemapParser.getParsedObjects(modelName2);
711+
sitemaps = sitemapParser.getParsedObjects(modelName3);
692712
if (sitemaps.isEmpty()) {
693-
sitemapParser.finishParsingFormat(modelName2);
713+
sitemapParser.finishParsingFormat(modelName3);
694714
return Response.status(Response.Status.BAD_REQUEST).entity("No sitemap loaded from input").build();
695715
}
696716
break;
@@ -717,6 +737,19 @@ public Response parse(final @Context HttpHeaders httpHeaders,
717737
metadata = itemParser.getParsedMetadata(modelNameToUse);
718738
stateFormatters = itemParser.getParsedStateFormatters(modelNameToUse);
719739
}
740+
if (sitemapParser != null) {
741+
// Avoid parsing the input a second time
742+
if (modelName == null && modelName2 == null) {
743+
modelName3 = sitemapParser.startParsingFormat(input, errors, warnings);
744+
if (modelName3 == null) {
745+
return Response.status(Response.Status.BAD_REQUEST).entity(String.join("\n", errors))
746+
.build();
747+
}
748+
}
749+
String modelNameToUse = modelName != null ? modelName
750+
: (modelName2 != null ? modelName2 : Objects.requireNonNull(modelName3));
751+
sitemaps = sitemapParser.getParsedObjects(modelNameToUse);
752+
}
720753
break;
721754
default:
722755
return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE)
@@ -730,8 +763,8 @@ public Response parse(final @Context HttpHeaders httpHeaders,
730763
if (modelName2 != null && itemParser != null) {
731764
itemParser.finishParsingFormat(modelName2);
732765
}
733-
if (modelName2 != null && sitemapParser != null) {
734-
sitemapParser.finishParsingFormat(modelName2);
766+
if (modelName3 != null && sitemapParser != null) {
767+
sitemapParser.finishParsingFormat(modelName3);
735768
}
736769
return Response.ok(result).build();
737770
}
@@ -903,6 +936,7 @@ private Thing simulateThing(DiscoveryResult result, ThingType thingType) {
903936
private @Nullable SitemapSerializer getSitemapSerializer(String mediaType) {
904937
return switch (mediaType) {
905938
case "text/vnd.openhab.dsl.sitemap" -> sitemapSerializers.get("DSL");
939+
case "application/yaml" -> sitemapSerializers.get("YAML");
906940
default -> null;
907941
};
908942
}
@@ -927,6 +961,7 @@ private Thing simulateThing(DiscoveryResult result, ThingType thingType) {
927961
private @Nullable SitemapParser getSitemapParser(String contentType) {
928962
return switch (contentType) {
929963
case "text/vnd.openhab.dsl.sitemap" -> sitemapParsers.get("DSL");
964+
case "application/yaml" -> sitemapParsers.get("YAML");
930965
default -> null;
931966
};
932967
}

bundles/org.openhab.core.model.yaml/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,10 @@
4040
<artifactId>org.openhab.core.automation</artifactId>
4141
<version>${project.version}</version>
4242
</dependency>
43+
<dependency>
44+
<groupId>org.openhab.core.bundles</groupId>
45+
<artifactId>org.openhab.core.sitemap</artifactId>
46+
<version>5.2.0-SNAPSHOT</version>
47+
</dependency>
4348
</dependencies>
4449
</project>

bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/YamlModelRepositoryImpl.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.openhab.core.model.yaml.internal.rules.YamlRuleDTO;
4949
import org.openhab.core.model.yaml.internal.rules.YamlRuleTemplateDTO;
5050
import org.openhab.core.model.yaml.internal.semantics.YamlSemanticTagDTO;
51+
import org.openhab.core.model.yaml.internal.sitemaps.YamlSitemapDTO;
5152
import org.openhab.core.model.yaml.internal.things.YamlThingDTO;
5253
import org.openhab.core.service.WatchService;
5354
import org.openhab.core.service.WatchService.Kind;
@@ -97,14 +98,15 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
9798
getElementName(YamlRuleTemplateDTO.class), // "ruleTemplates"
9899
getElementName(YamlSemanticTagDTO.class), // "tags"
99100
getElementName(YamlThingDTO.class), // "things"
100-
getElementName(YamlItemDTO.class) // "items"
101+
getElementName(YamlItemDTO.class), // "items"
102+
getElementName(YamlSitemapDTO.class) // "sitemaps"
101103
);
102104

103105
private static final String UNWANTED_EXCEPTION_TEXT = "at [Source: UNKNOWN; byte offset: #UNKNOWN] ";
104106
private static final String UNWANTED_EXCEPTION_TEXT2 = "\\n \\(through reference chain: .*";
105107

106-
private static final List<Path> WATCHED_PATHS = Stream.of("things", "items", "tags", "rules", "yaml").map(Path::of)
107-
.toList();
108+
private static final List<Path> WATCHED_PATHS = Stream.of("things", "items", "tags", "sitemaps", "rules", "yaml")
109+
.map(Path::of).toList();
108110

109111
private final Logger logger = LoggerFactory.getLogger(YamlModelRepositoryImpl.class);
110112

bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/items/YamlItemDTO.java

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,11 @@ public boolean isValid(@Nullable List<@NonNull String> errors, @Nullable List<@N
133133
"item \"%s\": \"dimension\" field ignored as type is not Number".formatted(name, dimension));
134134
}
135135
}
136-
if (icon != null) {
137-
subErrors.clear();
138-
ok &= isValidIcon(icon, subErrors);
139-
subErrors.forEach(error -> {
140-
addToList(errors, "invalid item \"%s\": %s".formatted(name, error));
141-
});
136+
if (icon != null && !YamlElementUtils.isValidIcon(icon)) {
137+
addToList(errors,
138+
"invalid item \"%s\": invalid value \"%s\" for \"icon\" field; it must contain a maximum of 3 segments separated by a colon, each segment matching pattern [a-zA-Z0-9_][a-zA-Z0-9_-]*"
139+
.formatted(name, icon));
140+
ok = false;
142141
}
143142
if (groups != null) {
144143
for (String gr : groups) {
@@ -206,26 +205,6 @@ public boolean isValid(@Nullable List<@NonNull String> errors, @Nullable List<@N
206205
return ok;
207206
}
208207

209-
private boolean isValidIcon(String icon, List<@NonNull String> errors) {
210-
boolean ok = true;
211-
String[] segments = icon.split(AbstractUID.SEPARATOR);
212-
int nb = segments.length;
213-
if (nb > 3) {
214-
errors.add("too many segments in value \"%s\" for \"icon\" field; maximum 3 is expected".formatted(icon));
215-
ok = false;
216-
nb = 3;
217-
}
218-
for (int i = 0; i < nb; i++) {
219-
String segment = segments[i];
220-
if (!ICON_SEGMENT_PATTERN.matcher(segment).matches()) {
221-
errors.add("segment \"%s\" in \"icon\" field not matching the expected syntax %s".formatted(segment,
222-
ICON_SEGMENT_PATTERN.pattern()));
223-
ok = false;
224-
}
225-
}
226-
return ok;
227-
}
228-
229208
private boolean isValidChannel(String channelUID, @Nullable Map<@NonNull String, @NonNull Object> configuration,
230209
List<@NonNull String> errors) {
231210
boolean ok = true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright (c) 2010-2026 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.core.model.yaml.internal.sitemaps;
14+
15+
import java.util.List;
16+
import java.util.Objects;
17+
18+
import org.eclipse.jdt.annotation.NonNull;
19+
import org.eclipse.jdt.annotation.Nullable;
20+
import org.openhab.core.model.yaml.internal.util.YamlElementUtils;
21+
22+
/**
23+
* This is a data transfer object that is used to serialize button definitions.
24+
*
25+
* @author Laurent Garnier - Initial contribution
26+
*/
27+
public class YamlButtonDefinitionDTO {
28+
29+
public Integer row;
30+
public Integer column;
31+
public String command;
32+
public String label;
33+
public String icon;
34+
35+
public YamlButtonDefinitionDTO() {
36+
}
37+
38+
public boolean isValid(@NonNull List<@NonNull String> errors, @NonNull List<@NonNull String> warnings) {
39+
boolean ok = true;
40+
if (row == null) {
41+
addToList(errors, "\"row\" field missing while mandatory in buttons definition");
42+
ok = false;
43+
} else if (row < 0) {
44+
addToList(errors,
45+
"invalid value \"%d\" for \"row\" field; value must be greater than 0 in buttons definition"
46+
.formatted(row));
47+
ok = false;
48+
} else if (row == 0) {
49+
addToList(warnings,
50+
"invalid value \"%d\" for \"row\" field; value must be greater than 0 in buttons definition"
51+
.formatted(row));
52+
}
53+
if (column == null) {
54+
addToList(errors, "\"column\" field missing while mandatory in buttons definition");
55+
ok = false;
56+
} else if (column < 0) {
57+
addToList(errors,
58+
"invalid value \"%d\" for \"column\" field; value must be greater than 0 in buttons definition"
59+
.formatted(column));
60+
ok = false;
61+
} else if (column == 0) {
62+
addToList(warnings,
63+
"invalid value \"%d\" for \"column\" field; value must be greater than 0 in buttons definition"
64+
.formatted(column));
65+
}
66+
if (command == null) {
67+
addToList(errors, "\"command\" field missing while mandatory in buttons definition");
68+
ok = false;
69+
}
70+
if (label == null) {
71+
addToList(errors, "\"label\" field missing while mandatory in buttons definition");
72+
ok = false;
73+
}
74+
if (icon != null && !YamlElementUtils.isValidIcon(icon)) {
75+
addToList(errors,
76+
"invalid value \"%s\" for \"icon\" field in buttons definition; it must contain a maximum of 3 segments separated by a colon, each segment matching pattern [a-zA-Z0-9_][a-zA-Z0-9_-]*"
77+
.formatted(icon));
78+
ok = false;
79+
}
80+
return ok;
81+
}
82+
83+
private void addToList(@Nullable List<@NonNull String> list, String value) {
84+
if (list != null) {
85+
list.add(value);
86+
}
87+
}
88+
89+
@Override
90+
public int hashCode() {
91+
return Objects.hash(row, column, command, label, icon);
92+
}
93+
94+
@Override
95+
public boolean equals(@Nullable Object obj) {
96+
if (this == obj) {
97+
return true;
98+
} else if (obj == null || getClass() != obj.getClass()) {
99+
return false;
100+
}
101+
YamlButtonDefinitionDTO other = (YamlButtonDefinitionDTO) obj;
102+
return Objects.equals(row, other.row) && Objects.equals(column, other.column)
103+
&& Objects.equals(command, other.command) && Objects.equals(label, other.label)
104+
&& Objects.equals(icon, other.icon);
105+
}
106+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright (c) 2010-2026 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.core.model.yaml.internal.sitemaps;
14+
15+
import java.util.List;
16+
import java.util.Objects;
17+
18+
import org.eclipse.jdt.annotation.NonNull;
19+
import org.eclipse.jdt.annotation.Nullable;
20+
21+
/**
22+
* This is a data transfer object that is used to serialize color rules.
23+
*
24+
* @author Laurent Garnier - Initial contribution
25+
*/
26+
public class YamlColorRuleDTO {
27+
28+
public List<YamlConditionDTO> conditions;
29+
public String color;
30+
31+
public YamlColorRuleDTO() {
32+
}
33+
34+
public boolean isValid(@NonNull List<@NonNull String> errors, @NonNull List<@NonNull String> warnings) {
35+
boolean ok = true;
36+
if (conditions != null && !conditions.isEmpty()) {
37+
for (YamlConditionDTO condition : conditions) {
38+
ok &= condition.isValid(errors, warnings);
39+
}
40+
}
41+
if (color == null) {
42+
addToList(errors, "\"color\" field missing while mandatory");
43+
ok = false;
44+
}
45+
return ok;
46+
}
47+
48+
private void addToList(@Nullable List<@NonNull String> list, String value) {
49+
if (list != null) {
50+
list.add(value);
51+
}
52+
}
53+
54+
@Override
55+
public int hashCode() {
56+
return Objects.hash(conditions, color);
57+
}
58+
59+
@Override
60+
public boolean equals(@Nullable Object obj) {
61+
if (this == obj) {
62+
return true;
63+
} else if (obj == null || getClass() != obj.getClass()) {
64+
return false;
65+
}
66+
YamlColorRuleDTO other = (YamlColorRuleDTO) obj;
67+
return Objects.equals(conditions, other.conditions) && Objects.equals(color, other.color);
68+
}
69+
}

0 commit comments

Comments
 (0)