Skip to content

Commit 521b446

Browse files
committed
[REST] New REST APIs to generate DSL syntax for items and things
Related to openhab#4509 Signed-off-by: Laurent Garnier <[email protected]>
1 parent ce37425 commit 521b446

File tree

17 files changed

+1243
-21
lines changed

17 files changed

+1243
-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;
@@ -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,61 @@ 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+
Configuration config = buildThingConfiguration(thingType, result.getProperties());
248+
Thing thing = ThingFactory.createThing(thingType, result.getThingUID(), config, result.getBridgeUID(),
249+
configDescRegistry);
250+
251+
return Response.ok(generator.generateSyntax(List.of(thing), false)).build();
252+
}
253+
254+
private Configuration buildThingConfiguration(ThingType thingType, Map<String, Object> properties) {
255+
Map<String, Object> configParams = new HashMap<>();
256+
for (ConfigDescriptionParameter param : getConfigDescriptionParameters(thingType)) {
257+
Object value = properties.get(param.getName());
258+
if (value != null) {
259+
configParams.put(param.getName(), ConfigUtil.normalizeType(value, param));
260+
}
261+
}
262+
return new Configuration(configParams);
263+
}
264+
265+
private List<ConfigDescriptionParameter> getConfigDescriptionParameters(ThingType thingType) {
266+
URI descURI = thingType.getConfigDescriptionURI();
267+
if (descURI != null) {
268+
ConfigDescription desc = configDescRegistry.getConfigDescription(descURI);
269+
if (desc != null) {
270+
return desc.getParameters();
271+
}
272+
}
273+
return List.of();
274+
}
185275
}

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

+105
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,16 @@
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.syntaxgenerator.ItemSyntaxGenerator;
9294
import org.openhab.core.types.Command;
9395
import org.openhab.core.types.State;
9496
import org.openhab.core.types.TypeParser;
9597
import org.osgi.service.component.annotations.Activate;
9698
import org.osgi.service.component.annotations.Component;
9799
import org.osgi.service.component.annotations.Deactivate;
98100
import org.osgi.service.component.annotations.Reference;
101+
import org.osgi.service.component.annotations.ReferenceCardinality;
102+
import org.osgi.service.component.annotations.ReferencePolicy;
99103
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
100104
import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired;
101105
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
@@ -183,6 +187,7 @@ private static void respectForwarded(final UriBuilder uriBuilder, final @Context
183187
private final MetadataSelectorMatcher metadataSelectorMatcher;
184188
private final SemanticTagRegistry semanticTagRegistry;
185189
private final TimeZoneProvider timeZoneProvider;
190+
private final Map<String, ItemSyntaxGenerator> itemSyntaxGenerators = new ConcurrentHashMap<>();
186191

187192
private final RegistryChangedRunnableListener<Item> resetLastModifiedItemChangeListener = new RegistryChangedRunnableListener<>(
188193
() -> lastModified = null);
@@ -224,6 +229,15 @@ void deactivate() {
224229
this.metadataRegistry.removeRegistryChangeListener(resetLastModifiedMetadataChangeListener);
225230
}
226231

232+
@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE)
233+
protected void addItemSyntaxGenerator(ItemSyntaxGenerator itemSyntaxGenerator) {
234+
itemSyntaxGenerators.put(itemSyntaxGenerator.getFormat(), itemSyntaxGenerator);
235+
}
236+
237+
protected void removeItemSyntaxGenerator(ItemSyntaxGenerator itemSyntaxGenerator) {
238+
itemSyntaxGenerators.remove(itemSyntaxGenerator.getFormat());
239+
}
240+
227241
private UriBuilder uriBuilder(final UriInfo uriInfo, final HttpHeaders httpHeaders) {
228242
final UriBuilder uriBuilder = uriInfo.getBaseUriBuilder().path(PATH_ITEMS).path("{itemName}");
229243
respectForwarded(uriBuilder, httpHeaders);
@@ -901,6 +915,53 @@ public Response getSemanticItem(final @Context UriInfo uriInfo, final @Context H
901915
return JSONResponse.createResponse(Status.OK, dto, null);
902916
}
903917

918+
@GET
919+
@RolesAllowed({ Role.ADMIN })
920+
@Path("/filesyntax")
921+
@Produces(MediaType.TEXT_PLAIN)
922+
@Operation(operationId = "generateSyntaxForAllItems", summary = "Generate file syntax for all items.", security = {
923+
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
924+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))),
925+
@ApiResponse(responseCode = "400", description = "Unsupported syntax format.") })
926+
public Response generateSyntaxForAllItems(
927+
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
928+
@DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) {
929+
ItemSyntaxGenerator generator = itemSyntaxGenerators.get(format);
930+
if (generator == null) {
931+
String message = "No syntax available for format " + format + "!";
932+
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
933+
}
934+
return Response.ok(generator.generateSyntax(sortItems(itemRegistry.getAll()))).build();
935+
}
936+
937+
@GET
938+
@RolesAllowed({ Role.ADMIN })
939+
@Path("/{itemname: [a-zA-Z_0-9]+}/filesyntax")
940+
@Produces(MediaType.TEXT_PLAIN)
941+
@Operation(operationId = "generateSyntaxForItem", summary = "Generate file syntax for an item.", security = {
942+
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
943+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))),
944+
@ApiResponse(responseCode = "400", description = "Unsupported syntax format."),
945+
@ApiResponse(responseCode = "404", description = "Item not found.") })
946+
public Response generateSyntaxForItem(
947+
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
948+
@PathParam("itemname") @Parameter(description = "item name") String itemname,
949+
@DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) {
950+
ItemSyntaxGenerator generator = itemSyntaxGenerators.get(format);
951+
if (generator == null) {
952+
String message = "No syntax available for format " + format + "!";
953+
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
954+
}
955+
956+
Item item = getItem(itemname);
957+
if (item == null) {
958+
String message = "Item " + itemname + " does not exist!";
959+
return Response.status(Response.Status.NOT_FOUND).entity(message).build();
960+
}
961+
962+
return Response.ok(generator.generateSyntax(List.of(item))).build();
963+
}
964+
904965
private JsonObject buildStatusObject(String itemName, String status, @Nullable String message) {
905966
JsonObject jo = new JsonObject();
906967
jo.addProperty("name", itemName);
@@ -1006,4 +1067,48 @@ private void addMetadata(EnrichedItemDTO dto, Set<String> namespaces, @Nullable
10061067
private boolean isEditable(String itemName) {
10071068
return managedItemProvider.get(itemName) != null;
10081069
}
1070+
1071+
private List<Item> sortItems(Collection<Item> items) {
1072+
return items.stream().sorted((item1, item2) -> {
1073+
if (item1.getName().equals(item2.getName())) {
1074+
return 0;
1075+
} else if (isAncestorGroupOf(item1, item2)) {
1076+
return -1;
1077+
} else if (isAncestorGroupOf(item2, item1)) {
1078+
return 1;
1079+
} else if (item1 instanceof GroupItem && !(item2 instanceof GroupItem)) {
1080+
return -1;
1081+
} else if (item2 instanceof GroupItem && !(item1 instanceof GroupItem)) {
1082+
return 1;
1083+
} else {
1084+
return item1.getName().compareTo(item2.getName());
1085+
}
1086+
}).collect(Collectors.toList());
1087+
}
1088+
1089+
private boolean isAncestorGroupOf(Item item1, Item item2) {
1090+
if (!item1.getName().equals(item2.getName()) && item1 instanceof GroupItem group) {
1091+
if (item2 instanceof GroupItem) {
1092+
List<Item> items = new ArrayList<>();
1093+
fillGroupTree(items, group);
1094+
return items.contains(item2);
1095+
} else {
1096+
return group.getAllMembers().contains(item2);
1097+
}
1098+
}
1099+
return false;
1100+
}
1101+
1102+
private void fillGroupTree(List<Item> items, Item item) {
1103+
if (!items.contains(item)) {
1104+
items.add(item);
1105+
if (item instanceof GroupItem group) {
1106+
for (Item member : group.getMembers()) {
1107+
if (member instanceof GroupItem groupMember) {
1108+
fillGroupTree(items, groupMember);
1109+
}
1110+
}
1111+
}
1112+
}
1113+
}
10091114
}

0 commit comments

Comments
 (0)