Skip to content

Commit 2ce9669

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 2ce9669

File tree

17 files changed

+1255
-21
lines changed

17 files changed

+1255
-21
lines changed

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

+90-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;
@@ -96,10 +114,25 @@ public class InboxResource implements RESTResource {
96114
public static final String PATH_INBOX = "inbox";
97115

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

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

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

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

+115
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;
@@ -182,7 +188,9 @@ private static void respectForwarded(final UriBuilder uriBuilder, final @Context
182188
private final MetadataRegistry metadataRegistry;
183189
private final MetadataSelectorMatcher metadataSelectorMatcher;
184190
private final SemanticTagRegistry semanticTagRegistry;
191+
private final ItemChannelLinkRegistry itemChannelLinkRegistry;
185192
private final TimeZoneProvider timeZoneProvider;
193+
private final Map<String, ItemSyntaxGenerator> itemSyntaxGenerators = new ConcurrentHashMap<>();
186194

187195
private final RegistryChangedRunnableListener<Item> resetLastModifiedItemChangeListener = new RegistryChangedRunnableListener<>(
188196
() -> lastModified = null);
@@ -202,6 +210,7 @@ public ItemResource(//
202210
final @Reference MetadataRegistry metadataRegistry,
203211
final @Reference MetadataSelectorMatcher metadataSelectorMatcher,
204212
final @Reference SemanticTagRegistry semanticTagRegistry,
213+
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry,
205214
final @Reference TimeZoneProvider timeZoneProvider) {
206215
this.dtoMapper = dtoMapper;
207216
this.eventPublisher = eventPublisher;
@@ -212,6 +221,7 @@ public ItemResource(//
212221
this.metadataRegistry = metadataRegistry;
213222
this.metadataSelectorMatcher = metadataSelectorMatcher;
214223
this.semanticTagRegistry = semanticTagRegistry;
224+
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
215225
this.timeZoneProvider = timeZoneProvider;
216226

217227
this.itemRegistry.addRegistryChangeListener(resetLastModifiedItemChangeListener);
@@ -224,6 +234,15 @@ void deactivate() {
224234
this.metadataRegistry.removeRegistryChangeListener(resetLastModifiedMetadataChangeListener);
225235
}
226236

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

923+
@GET
924+
@RolesAllowed({ Role.ADMIN })
925+
@Path("/filesyntax")
926+
@Produces(MediaType.TEXT_PLAIN)
927+
@Operation(operationId = "generateSyntaxForAllItems", summary = "Generate file syntax for all items.", security = {
928+
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
929+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))),
930+
@ApiResponse(responseCode = "400", description = "Unsupported syntax format.") })
931+
public Response generateSyntaxForAllItems(
932+
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
933+
@DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) {
934+
ItemSyntaxGenerator generator = itemSyntaxGenerators.get(format);
935+
if (generator == null) {
936+
String message = "No syntax available for format " + format + "!";
937+
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
938+
}
939+
return Response.ok(generator.generateSyntax(sortItems(itemRegistry.getAll()), itemChannelLinkRegistry.getAll(),
940+
metadataRegistry.getAll())).build();
941+
}
942+
943+
@GET
944+
@RolesAllowed({ Role.ADMIN })
945+
@Path("/{itemname: [a-zA-Z_0-9]+}/filesyntax")
946+
@Produces(MediaType.TEXT_PLAIN)
947+
@Operation(operationId = "generateSyntaxForItem", summary = "Generate file syntax for an item.", security = {
948+
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
949+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))),
950+
@ApiResponse(responseCode = "400", description = "Unsupported syntax format."),
951+
@ApiResponse(responseCode = "404", description = "Item not found.") })
952+
public Response generateSyntaxForItem(
953+
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
954+
@PathParam("itemname") @Parameter(description = "item name") String itemname,
955+
@DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) {
956+
ItemSyntaxGenerator generator = itemSyntaxGenerators.get(format);
957+
if (generator == null) {
958+
String message = "No syntax available for format " + format + "!";
959+
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
960+
}
961+
962+
Item item = getItem(itemname);
963+
if (item == null) {
964+
String message = "Item " + itemname + " does not exist!";
965+
return Response.status(Response.Status.NOT_FOUND).entity(message).build();
966+
}
967+
968+
Set<ItemChannelLink> channelLinks = itemChannelLinkRegistry.getLinks(itemname);
969+
Set<Metadata> metadata = metadataRegistry.getAll().stream()
970+
.filter(md -> md.getUID().getItemName().equals(itemname)).collect(Collectors.toSet());
971+
972+
return Response.ok(generator.generateSyntax(List.of(item), channelLinks, metadata)).build();
973+
}
974+
904975
private JsonObject buildStatusObject(String itemName, String status, @Nullable String message) {
905976
JsonObject jo = new JsonObject();
906977
jo.addProperty("name", itemName);
@@ -1006,4 +1077,48 @@ private void addMetadata(EnrichedItemDTO dto, Set<String> namespaces, @Nullable
10061077
private boolean isEditable(String itemName) {
10071078
return managedItemProvider.get(itemName) != null;
10081079
}
1080+
1081+
private List<Item> sortItems(Collection<Item> items) {
1082+
return items.stream().sorted((item1, item2) -> {
1083+
if (item1.getName().equals(item2.getName())) {
1084+
return 0;
1085+
} else if (isAncestorGroupOf(item1, item2)) {
1086+
return -1;
1087+
} else if (isAncestorGroupOf(item2, item1)) {
1088+
return 1;
1089+
} else if (item1 instanceof GroupItem && !(item2 instanceof GroupItem)) {
1090+
return -1;
1091+
} else if (item2 instanceof GroupItem && !(item1 instanceof GroupItem)) {
1092+
return 1;
1093+
} else {
1094+
return item1.getName().compareTo(item2.getName());
1095+
}
1096+
}).collect(Collectors.toList());
1097+
}
1098+
1099+
private boolean isAncestorGroupOf(Item item1, Item item2) {
1100+
if (!item1.getName().equals(item2.getName()) && item1 instanceof GroupItem group) {
1101+
if (item2 instanceof GroupItem) {
1102+
List<Item> items = new ArrayList<>();
1103+
fillGroupTree(items, group);
1104+
return items.contains(item2);
1105+
} else {
1106+
return group.getAllMembers().contains(item2);
1107+
}
1108+
}
1109+
return false;
1110+
}
1111+
1112+
private void fillGroupTree(List<Item> items, Item item) {
1113+
if (!items.contains(item)) {
1114+
items.add(item);
1115+
if (item instanceof GroupItem group) {
1116+
for (Item member : group.getMembers()) {
1117+
if (member instanceof GroupItem groupMember) {
1118+
fillGroupTree(items, groupMember);
1119+
}
1120+
}
1121+
}
1122+
}
1123+
}
10091124
}

0 commit comments

Comments
 (0)