Skip to content

Commit 6bc963f

Browse files
committed
[REST] New REST APIs to generate DSL syntax for items and things
Related to openhab#4509 Added REST APIs: - GET /inbox/{thingUID}/filesyntax to generate file syntax for the thing associated to the discovery result - GET /items/filesyntax to generate file syntax for all items - GET /items/{itemname}/filesyntax to generate file syntax for an item - GET /things/filesyntax to generate file syntax for all things - GET /things/{thingUID}/filesyntax to generate file syntax for a thing All these APIs have a parameter named "format" to request a particular output format. Of course, a syntax generator should be available for this format. Only "DSL" format is provided by this PR as this is currently our unique supported format for items and things in config files. So this parameter is set to "DSL" by default. In the future, new formats could be added and they will be automatically supported by these APIs. The API GET /things/filesyntax has another parameter named "preferPresentationAsTree" allowing to choose between a flat display or a display as a tree. Its default value is true for a display of things as tree. Signed-off-by: Laurent Garnier <[email protected]>
1 parent ce37425 commit 6bc963f

File tree

17 files changed

+1289
-21
lines changed

17 files changed

+1289
-21
lines changed

bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/discovery/InboxResource.java

+91-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
*/
1313
package org.openhab.core.io.rest.core.internal.discovery;
1414

15+
import static org.openhab.core.config.discovery.inbox.InboxPredicates.forThingUID;
16+
17+
import java.net.URI;
18+
import java.util.HashMap;
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.concurrent.ConcurrentHashMap;
1522
import java.util.stream.Stream;
1623

1724
import javax.annotation.security.RolesAllowed;
@@ -33,6 +40,11 @@
3340
import org.eclipse.jdt.annotation.NonNullByDefault;
3441
import org.eclipse.jdt.annotation.Nullable;
3542
import org.openhab.core.auth.Role;
43+
import org.openhab.core.config.core.ConfigDescription;
44+
import org.openhab.core.config.core.ConfigDescriptionParameter;
45+
import org.openhab.core.config.core.ConfigDescriptionRegistry;
46+
import org.openhab.core.config.core.ConfigUtil;
47+
import org.openhab.core.config.core.Configuration;
3648
import org.openhab.core.config.discovery.DiscoveryResult;
3749
import org.openhab.core.config.discovery.DiscoveryResultFlag;
3850
import org.openhab.core.config.discovery.dto.DiscoveryResultDTO;
@@ -44,9 +56,15 @@
4456
import org.openhab.core.io.rest.Stream2JSONInputStream;
4557
import org.openhab.core.thing.Thing;
4658
import org.openhab.core.thing.ThingUID;
59+
import org.openhab.core.thing.binding.ThingFactory;
60+
import org.openhab.core.thing.syntaxgenerator.ThingSyntaxGenerator;
61+
import org.openhab.core.thing.type.ThingType;
62+
import org.openhab.core.thing.type.ThingTypeRegistry;
4763
import org.osgi.service.component.annotations.Activate;
4864
import org.osgi.service.component.annotations.Component;
4965
import org.osgi.service.component.annotations.Reference;
66+
import org.osgi.service.component.annotations.ReferenceCardinality;
67+
import org.osgi.service.component.annotations.ReferencePolicy;
5068
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
5169
import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired;
5270
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
@@ -78,6 +96,7 @@
7896
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
7997
* @author Wouter Born - Migrated to OpenAPI annotations
8098
* @author Laurent Garnier - Added optional parameter newThingId to approve API
99+
* @author Laurent Garnier - Added API to generate file syntax
81100
*/
82101
@Component(service = { RESTResource.class, InboxResource.class })
83102
@JaxrsResource
@@ -96,10 +115,25 @@ public class InboxResource implements RESTResource {
96115
public static final String PATH_INBOX = "inbox";
97116

98117
private final Inbox inbox;
118+
private final ThingTypeRegistry thingTypeRegistry;
119+
private final ConfigDescriptionRegistry configDescRegistry;
120+
private final Map<String, ThingSyntaxGenerator> thingSyntaxGenerators = new ConcurrentHashMap<>();
99121

100122
@Activate
101-
public InboxResource(final @Reference Inbox inbox) {
123+
public InboxResource(final @Reference Inbox inbox, final @Reference ThingTypeRegistry thingTypeRegistry,
124+
final @Reference ConfigDescriptionRegistry configDescRegistry) {
102125
this.inbox = inbox;
126+
this.thingTypeRegistry = thingTypeRegistry;
127+
this.configDescRegistry = configDescRegistry;
128+
}
129+
130+
@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE)
131+
protected void addThingSyntaxGenerator(ThingSyntaxGenerator thingSyntaxGenerator) {
132+
thingSyntaxGenerators.put(thingSyntaxGenerator.getFormat(), thingSyntaxGenerator);
133+
}
134+
135+
protected void removeThingSyntaxGenerator(ThingSyntaxGenerator thingSyntaxGenerator) {
136+
thingSyntaxGenerators.remove(thingSyntaxGenerator.getFormat());
103137
}
104138

105139
@POST
@@ -182,4 +216,60 @@ public Response unignore(@PathParam("thingUID") @Parameter(description = "thingU
182216
inbox.setFlag(new ThingUID(thingUID), DiscoveryResultFlag.NEW);
183217
return Response.ok(null, MediaType.TEXT_PLAIN).build();
184218
}
219+
220+
@GET
221+
@Path("/{thingUID}/filesyntax")
222+
@Produces(MediaType.TEXT_PLAIN)
223+
@Operation(operationId = "generateSyntaxForDiscoveryResult", summary = "Generate file syntax for the thing associated to the discovery result.", responses = {
224+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))),
225+
@ApiResponse(responseCode = "400", description = "Unsupported syntax format."),
226+
@ApiResponse(responseCode = "404", description = "Discovery result not found in the inbox or thing type not found.") })
227+
public Response generateSyntaxForDiscoveryResult(
228+
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
229+
@PathParam("thingUID") @Parameter(description = "thingUID") String thingUID,
230+
@DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) {
231+
ThingSyntaxGenerator generator = thingSyntaxGenerators.get(format);
232+
if (generator == null) {
233+
String message = "No syntax available for format " + format + "!";
234+
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
235+
}
236+
237+
List<DiscoveryResult> results = inbox.getAll().stream().filter(forThingUID(new ThingUID(thingUID))).toList();
238+
if (results.isEmpty()) {
239+
String message = "Discovery result for thing with UID " + thingUID + " not found in the inbox!";
240+
return Response.status(Response.Status.NOT_FOUND).entity(message).build();
241+
}
242+
DiscoveryResult result = results.get(0);
243+
ThingType thingType = thingTypeRegistry.getThingType(result.getThingTypeUID());
244+
if (thingType == null) {
245+
String message = "Thing type with UID " + result.getThingTypeUID() + " does not exist!";
246+
return Response.status(Response.Status.NOT_FOUND).entity(message).build();
247+
}
248+
249+
return Response.ok(generator.generateSyntax(List.of(simulateThing(result, thingType)), false)).build();
250+
}
251+
252+
/*
253+
* Create a thing from a discovery result without inserting it in the thing registry
254+
*/
255+
private Thing simulateThing(DiscoveryResult result, ThingType thingType) {
256+
Map<String, Object> configParams = new HashMap<>();
257+
List<ConfigDescriptionParameter> configDescriptionParameters = List.of();
258+
URI descURI = thingType.getConfigDescriptionURI();
259+
if (descURI != null) {
260+
ConfigDescription desc = configDescRegistry.getConfigDescription(descURI);
261+
if (desc != null) {
262+
configDescriptionParameters = desc.getParameters();
263+
}
264+
}
265+
for (ConfigDescriptionParameter param : configDescriptionParameters) {
266+
Object value = result.getProperties().get(param.getName());
267+
if (value != null) {
268+
configParams.put(param.getName(), ConfigUtil.normalizeType(value, param));
269+
}
270+
}
271+
Configuration config = new Configuration(configParams);
272+
return ThingFactory.createThing(thingType, result.getThingUID(), config, result.getBridgeUID(),
273+
configDescRegistry);
274+
}
185275
}

bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java

+133
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.Objects;
2828
import java.util.Optional;
2929
import java.util.Set;
30+
import java.util.concurrent.ConcurrentHashMap;
3031
import java.util.function.Predicate;
3132
import java.util.stream.Collectors;
3233
import java.util.stream.Stream;
@@ -89,13 +90,18 @@
8990
import org.openhab.core.library.types.UpDownType;
9091
import org.openhab.core.semantics.SemanticTagRegistry;
9192
import org.openhab.core.semantics.SemanticsPredicates;
93+
import org.openhab.core.thing.link.ItemChannelLink;
94+
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
95+
import org.openhab.core.thing.syntaxgenerator.ItemSyntaxGenerator;
9296
import org.openhab.core.types.Command;
9397
import org.openhab.core.types.State;
9498
import org.openhab.core.types.TypeParser;
9599
import org.osgi.service.component.annotations.Activate;
96100
import org.osgi.service.component.annotations.Component;
97101
import org.osgi.service.component.annotations.Deactivate;
98102
import org.osgi.service.component.annotations.Reference;
103+
import org.osgi.service.component.annotations.ReferenceCardinality;
104+
import org.osgi.service.component.annotations.ReferencePolicy;
99105
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
100106
import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired;
101107
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
@@ -137,6 +143,7 @@
137143
* @author Stefan Triller - Added bulk item add method
138144
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
139145
* @author Wouter Born - Migrated to OpenAPI annotations
146+
* @author Laurent Garnier - Added API to generate file syntax
140147
*/
141148
@Component
142149
@JaxrsResource
@@ -182,7 +189,9 @@ private static void respectForwarded(final UriBuilder uriBuilder, final @Context
182189
private final MetadataRegistry metadataRegistry;
183190
private final MetadataSelectorMatcher metadataSelectorMatcher;
184191
private final SemanticTagRegistry semanticTagRegistry;
192+
private final ItemChannelLinkRegistry itemChannelLinkRegistry;
185193
private final TimeZoneProvider timeZoneProvider;
194+
private final Map<String, ItemSyntaxGenerator> itemSyntaxGenerators = new ConcurrentHashMap<>();
186195

187196
private final RegistryChangedRunnableListener<Item> resetLastModifiedItemChangeListener = new RegistryChangedRunnableListener<>(
188197
() -> lastModified = null);
@@ -202,6 +211,7 @@ public ItemResource(//
202211
final @Reference MetadataRegistry metadataRegistry,
203212
final @Reference MetadataSelectorMatcher metadataSelectorMatcher,
204213
final @Reference SemanticTagRegistry semanticTagRegistry,
214+
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry,
205215
final @Reference TimeZoneProvider timeZoneProvider) {
206216
this.dtoMapper = dtoMapper;
207217
this.eventPublisher = eventPublisher;
@@ -212,6 +222,7 @@ public ItemResource(//
212222
this.metadataRegistry = metadataRegistry;
213223
this.metadataSelectorMatcher = metadataSelectorMatcher;
214224
this.semanticTagRegistry = semanticTagRegistry;
225+
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
215226
this.timeZoneProvider = timeZoneProvider;
216227

217228
this.itemRegistry.addRegistryChangeListener(resetLastModifiedItemChangeListener);
@@ -224,6 +235,15 @@ void deactivate() {
224235
this.metadataRegistry.removeRegistryChangeListener(resetLastModifiedMetadataChangeListener);
225236
}
226237

238+
@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE)
239+
protected void addItemSyntaxGenerator(ItemSyntaxGenerator itemSyntaxGenerator) {
240+
itemSyntaxGenerators.put(itemSyntaxGenerator.getFormat(), itemSyntaxGenerator);
241+
}
242+
243+
protected void removeItemSyntaxGenerator(ItemSyntaxGenerator itemSyntaxGenerator) {
244+
itemSyntaxGenerators.remove(itemSyntaxGenerator.getFormat());
245+
}
246+
227247
private UriBuilder uriBuilder(final UriInfo uriInfo, final HttpHeaders httpHeaders) {
228248
final UriBuilder uriBuilder = uriInfo.getBaseUriBuilder().path(PATH_ITEMS).path("{itemName}");
229249
respectForwarded(uriBuilder, httpHeaders);
@@ -901,6 +921,58 @@ public Response getSemanticItem(final @Context UriInfo uriInfo, final @Context H
901921
return JSONResponse.createResponse(Status.OK, dto, null);
902922
}
903923

924+
@GET
925+
@RolesAllowed({ Role.ADMIN })
926+
@Path("/filesyntax")
927+
@Produces(MediaType.TEXT_PLAIN)
928+
@Operation(operationId = "generateSyntaxForAllItems", summary = "Generate file syntax for all items.", security = {
929+
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
930+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))),
931+
@ApiResponse(responseCode = "400", description = "Unsupported syntax format.") })
932+
public Response generateSyntaxForAllItems(
933+
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
934+
@DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) {
935+
ItemSyntaxGenerator generator = itemSyntaxGenerators.get(format);
936+
if (generator == null) {
937+
String message = "No syntax available for format " + format + "!";
938+
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
939+
}
940+
return Response.ok(generator.generateSyntax(sortItems(itemRegistry.getAll()), itemChannelLinkRegistry.getAll(),
941+
metadataRegistry.getAll())).build();
942+
}
943+
944+
@GET
945+
@RolesAllowed({ Role.ADMIN })
946+
@Path("/{itemname: [a-zA-Z_0-9]+}/filesyntax")
947+
@Produces(MediaType.TEXT_PLAIN)
948+
@Operation(operationId = "generateSyntaxForItem", summary = "Generate file syntax for an item.", security = {
949+
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
950+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))),
951+
@ApiResponse(responseCode = "400", description = "Unsupported syntax format."),
952+
@ApiResponse(responseCode = "404", description = "Item not found.") })
953+
public Response generateSyntaxForItem(
954+
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
955+
@PathParam("itemname") @Parameter(description = "item name") String itemname,
956+
@DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) {
957+
ItemSyntaxGenerator generator = itemSyntaxGenerators.get(format);
958+
if (generator == null) {
959+
String message = "No syntax available for format " + format + "!";
960+
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
961+
}
962+
963+
Item item = getItem(itemname);
964+
if (item == null) {
965+
String message = "Item " + itemname + " does not exist!";
966+
return Response.status(Response.Status.NOT_FOUND).entity(message).build();
967+
}
968+
969+
Set<ItemChannelLink> channelLinks = itemChannelLinkRegistry.getLinks(itemname);
970+
Set<Metadata> metadata = metadataRegistry.getAll().stream()
971+
.filter(md -> md.getUID().getItemName().equals(itemname)).collect(Collectors.toSet());
972+
973+
return Response.ok(generator.generateSyntax(List.of(item), channelLinks, metadata)).build();
974+
}
975+
904976
private JsonObject buildStatusObject(String itemName, String status, @Nullable String message) {
905977
JsonObject jo = new JsonObject();
906978
jo.addProperty("name", itemName);
@@ -1006,4 +1078,65 @@ private void addMetadata(EnrichedItemDTO dto, Set<String> namespaces, @Nullable
10061078
private boolean isEditable(String itemName) {
10071079
return managedItemProvider.get(itemName) != null;
10081080
}
1081+
1082+
/*
1083+
* Sort the items in such a way:
1084+
* - group items are before non group items
1085+
* - group items are sorted to have as much as possible ancestors before their children
1086+
* - items not linked to a channel are before items linked to a channel
1087+
* - items linked to a channel are grouped by thing UID
1088+
* - items linked to the same thing UID are sorted by item name
1089+
*/
1090+
private List<Item> sortItems(Collection<Item> items) {
1091+
List<Item> groups = items.stream().filter(item -> item instanceof GroupItem).sorted((item1, item2) -> {
1092+
return item1.getName().compareTo(item2.getName());
1093+
}).collect(Collectors.toList());
1094+
1095+
List<Item> topGroups = groups.stream().filter(group -> group.getGroupNames().isEmpty())
1096+
.sorted((group1, group2) -> {
1097+
return group1.getName().compareTo(group2.getName());
1098+
}).collect(Collectors.toList());
1099+
1100+
List<Item> groupTree = new ArrayList<>();
1101+
for (Item group : topGroups) {
1102+
fillGroupTree(groupTree, group);
1103+
}
1104+
1105+
if (groupTree.size() != groups.size()) {
1106+
logger.warn("Something want wrong when sorting groups; failback to a sort by name.");
1107+
groupTree = groups;
1108+
}
1109+
1110+
List<Item> nonGroups = items.stream().filter(item -> !(item instanceof GroupItem)).sorted((item1, item2) -> {
1111+
Set<ItemChannelLink> channelLinks1 = itemChannelLinkRegistry.getLinks(item1.getName());
1112+
String thingUID1 = channelLinks1.isEmpty() ? null
1113+
: channelLinks1.iterator().next().getLinkedUID().getThingUID().getAsString();
1114+
Set<ItemChannelLink> channelLinks2 = itemChannelLinkRegistry.getLinks(item2.getName());
1115+
String thingUID2 = channelLinks2.isEmpty() ? null
1116+
: channelLinks2.iterator().next().getLinkedUID().getThingUID().getAsString();
1117+
1118+
if (thingUID1 == null && thingUID2 != null) {
1119+
return -1;
1120+
} else if (thingUID1 != null && thingUID2 == null) {
1121+
return 1;
1122+
} else if (thingUID1 != null && thingUID2 != null && !thingUID1.equals(thingUID2)) {
1123+
return thingUID1.compareTo(thingUID2);
1124+
}
1125+
return item1.getName().compareTo(item2.getName());
1126+
}).collect(Collectors.toList());
1127+
1128+
return Stream.of(groupTree, nonGroups).flatMap(List::stream).collect(Collectors.toList());
1129+
}
1130+
1131+
private void fillGroupTree(List<Item> groups, Item item) {
1132+
if (item instanceof GroupItem group && !groups.contains(group)) {
1133+
groups.add(group);
1134+
List<Item> members = group.getMembers().stream().sorted((member1, member2) -> {
1135+
return member1.getName().compareTo(member2.getName());
1136+
}).collect(Collectors.toList());
1137+
for (Item member : members) {
1138+
fillGroupTree(groups, member);
1139+
}
1140+
}
1141+
}
10091142
}

0 commit comments

Comments
 (0)