From cff0a29c8444828b2b760769c7f9bede09196ccb Mon Sep 17 00:00:00 2001 From: Laurent Garnier Date: Sun, 19 Jan 2025 23:45:37 +0100 Subject: [PATCH] [REST] New REST APIs to generate DSL syntax for items and things Related to #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 --- .../internal/discovery/InboxResource.java | 92 +++++++- .../rest/core/internal/item/ItemResource.java | 116 ++++++++++ .../core/internal/thing/ThingResource.java | 101 +++++++++ .../core/model/core/ModelRepository.java | 10 + .../core/internal/ModelRepositoryImpl.java | 21 ++ .../core/model/ItemsRuntimeModule.xtend | 9 +- .../model/formatting/ItemsFormatter.xtend | 24 +- bundles/org.openhab.core.model.thing/bnd.bnd | 3 + .../core/model/thing/ThingRuntimeModule.xtend | 10 +- .../thing/formatting/ThingFormatter.xtend | 56 ++++- .../ItemDslSyntaxGenerator.java | 171 +++++++++++++++ .../ThingDslSyntaxGenerator.java | 162 ++++++++++++++ .../AbstractItemSyntaxGenerator.java | 205 ++++++++++++++++++ .../AbstractThingSyntaxGenerator.java | 205 ++++++++++++++++++ .../syntaxgenerator/ItemSyntaxGenerator.java | 47 ++++ .../syntaxgenerator/ThingSyntaxGenerator.java | 43 ++++ .../discovery/InboxResourceOSGITest.java | 6 +- 17 files changed, 1260 insertions(+), 21 deletions(-) create mode 100644 bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/syntaxgenerator/ItemDslSyntaxGenerator.java create mode 100644 bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/syntaxgenerator/ThingDslSyntaxGenerator.java create mode 100644 bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/AbstractItemSyntaxGenerator.java create mode 100644 bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/AbstractThingSyntaxGenerator.java create mode 100644 bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/ItemSyntaxGenerator.java create mode 100644 bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/ThingSyntaxGenerator.java diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/discovery/InboxResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/discovery/InboxResource.java index 984174aecbc..17b37de5596 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/discovery/InboxResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/discovery/InboxResource.java @@ -12,6 +12,13 @@ */ package org.openhab.core.io.rest.core.internal.discovery; +import static org.openhab.core.config.discovery.inbox.InboxPredicates.forThingUID; + +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; import javax.annotation.security.RolesAllowed; @@ -33,6 +40,11 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.auth.Role; +import org.openhab.core.config.core.ConfigDescription; +import org.openhab.core.config.core.ConfigDescriptionParameter; +import org.openhab.core.config.core.ConfigDescriptionRegistry; +import org.openhab.core.config.core.ConfigUtil; +import org.openhab.core.config.core.Configuration; import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResultFlag; import org.openhab.core.config.discovery.dto.DiscoveryResultDTO; @@ -44,9 +56,15 @@ import org.openhab.core.io.rest.Stream2JSONInputStream; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingFactory; +import org.openhab.core.thing.syntaxgenerator.ThingSyntaxGenerator; +import org.openhab.core.thing.type.ThingType; +import org.openhab.core.thing.type.ThingTypeRegistry; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants; import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired; import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect; @@ -78,6 +96,7 @@ * @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification * @author Wouter Born - Migrated to OpenAPI annotations * @author Laurent Garnier - Added optional parameter newThingId to approve API + * @author Laurent Garnier - Added API to generate file syntax */ @Component(service = { RESTResource.class, InboxResource.class }) @JaxrsResource @@ -96,10 +115,25 @@ public class InboxResource implements RESTResource { public static final String PATH_INBOX = "inbox"; private final Inbox inbox; + private final ThingTypeRegistry thingTypeRegistry; + private final ConfigDescriptionRegistry configDescRegistry; + private final Map thingSyntaxGenerators = new ConcurrentHashMap<>(); @Activate - public InboxResource(final @Reference Inbox inbox) { + public InboxResource(final @Reference Inbox inbox, final @Reference ThingTypeRegistry thingTypeRegistry, + final @Reference ConfigDescriptionRegistry configDescRegistry) { this.inbox = inbox; + this.thingTypeRegistry = thingTypeRegistry; + this.configDescRegistry = configDescRegistry; + } + + @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE) + protected void addThingSyntaxGenerator(ThingSyntaxGenerator thingSyntaxGenerator) { + thingSyntaxGenerators.put(thingSyntaxGenerator.getFormat(), thingSyntaxGenerator); + } + + protected void removeThingSyntaxGenerator(ThingSyntaxGenerator thingSyntaxGenerator) { + thingSyntaxGenerators.remove(thingSyntaxGenerator.getFormat()); } @POST @@ -182,4 +216,60 @@ public Response unignore(@PathParam("thingUID") @Parameter(description = "thingU inbox.setFlag(new ThingUID(thingUID), DiscoveryResultFlag.NEW); return Response.ok(null, MediaType.TEXT_PLAIN).build(); } + + @GET + @Path("/{thingUID}/filesyntax") + @Produces(MediaType.TEXT_PLAIN) + @Operation(operationId = "generateSyntaxForDiscoveryResult", summary = "Generate file syntax for the thing associated to the discovery result.", responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))), + @ApiResponse(responseCode = "400", description = "Unsupported syntax format."), + @ApiResponse(responseCode = "404", description = "Discovery result not found in the inbox or thing type not found.") }) + public Response generateSyntaxForDiscoveryResult( + @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, + @PathParam("thingUID") @Parameter(description = "thingUID") String thingUID, + @DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) { + ThingSyntaxGenerator generator = thingSyntaxGenerators.get(format); + if (generator == null) { + String message = "No syntax available for format " + format + "!"; + return Response.status(Response.Status.BAD_REQUEST).entity(message).build(); + } + + List results = inbox.getAll().stream().filter(forThingUID(new ThingUID(thingUID))).toList(); + if (results.isEmpty()) { + String message = "Discovery result for thing with UID " + thingUID + " not found in the inbox!"; + return Response.status(Response.Status.NOT_FOUND).entity(message).build(); + } + DiscoveryResult result = results.get(0); + ThingType thingType = thingTypeRegistry.getThingType(result.getThingTypeUID()); + if (thingType == null) { + String message = "Thing type with UID " + result.getThingTypeUID() + " does not exist!"; + return Response.status(Response.Status.NOT_FOUND).entity(message).build(); + } + + return Response.ok(generator.generateSyntax(List.of(simulateThing(result, thingType)), false)).build(); + } + + /* + * Create a thing from a discovery result without inserting it in the thing registry + */ + private Thing simulateThing(DiscoveryResult result, ThingType thingType) { + Map configParams = new HashMap<>(); + List configDescriptionParameters = List.of(); + URI descURI = thingType.getConfigDescriptionURI(); + if (descURI != null) { + ConfigDescription desc = configDescRegistry.getConfigDescription(descURI); + if (desc != null) { + configDescriptionParameters = desc.getParameters(); + } + } + for (ConfigDescriptionParameter param : configDescriptionParameters) { + Object value = result.getProperties().get(param.getName()); + if (value != null) { + configParams.put(param.getName(), ConfigUtil.normalizeType(value, param)); + } + } + Configuration config = new Configuration(configParams); + return ThingFactory.createThing(thingType, result.getThingUID(), config, result.getBridgeUID(), + configDescRegistry); + } } diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java index 8d317e47633..0279e7baa5c 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java @@ -27,6 +27,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -89,6 +90,9 @@ import org.openhab.core.library.types.UpDownType; import org.openhab.core.semantics.SemanticTagRegistry; import org.openhab.core.semantics.SemanticsPredicates; +import org.openhab.core.thing.link.ItemChannelLink; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.thing.syntaxgenerator.ItemSyntaxGenerator; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.core.types.TypeParser; @@ -96,6 +100,8 @@ import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants; import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired; import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect; @@ -137,6 +143,7 @@ * @author Stefan Triller - Added bulk item add method * @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification * @author Wouter Born - Migrated to OpenAPI annotations + * @author Laurent Garnier - Added API to generate file syntax */ @Component @JaxrsResource @@ -182,7 +189,9 @@ private static void respectForwarded(final UriBuilder uriBuilder, final @Context private final MetadataRegistry metadataRegistry; private final MetadataSelectorMatcher metadataSelectorMatcher; private final SemanticTagRegistry semanticTagRegistry; + private final ItemChannelLinkRegistry itemChannelLinkRegistry; private final TimeZoneProvider timeZoneProvider; + private final Map itemSyntaxGenerators = new ConcurrentHashMap<>(); private final RegistryChangedRunnableListener resetLastModifiedItemChangeListener = new RegistryChangedRunnableListener<>( () -> lastModified = null); @@ -202,6 +211,7 @@ public ItemResource(// final @Reference MetadataRegistry metadataRegistry, final @Reference MetadataSelectorMatcher metadataSelectorMatcher, final @Reference SemanticTagRegistry semanticTagRegistry, + final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, final @Reference TimeZoneProvider timeZoneProvider) { this.dtoMapper = dtoMapper; this.eventPublisher = eventPublisher; @@ -212,6 +222,7 @@ public ItemResource(// this.metadataRegistry = metadataRegistry; this.metadataSelectorMatcher = metadataSelectorMatcher; this.semanticTagRegistry = semanticTagRegistry; + this.itemChannelLinkRegistry = itemChannelLinkRegistry; this.timeZoneProvider = timeZoneProvider; this.itemRegistry.addRegistryChangeListener(resetLastModifiedItemChangeListener); @@ -224,6 +235,15 @@ void deactivate() { this.metadataRegistry.removeRegistryChangeListener(resetLastModifiedMetadataChangeListener); } + @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE) + protected void addItemSyntaxGenerator(ItemSyntaxGenerator itemSyntaxGenerator) { + itemSyntaxGenerators.put(itemSyntaxGenerator.getFormat(), itemSyntaxGenerator); + } + + protected void removeItemSyntaxGenerator(ItemSyntaxGenerator itemSyntaxGenerator) { + itemSyntaxGenerators.remove(itemSyntaxGenerator.getFormat()); + } + private UriBuilder uriBuilder(final UriInfo uriInfo, final HttpHeaders httpHeaders) { final UriBuilder uriBuilder = uriInfo.getBaseUriBuilder().path(PATH_ITEMS).path("{itemName}"); respectForwarded(uriBuilder, httpHeaders); @@ -901,6 +921,58 @@ public Response getSemanticItem(final @Context UriInfo uriInfo, final @Context H return JSONResponse.createResponse(Status.OK, dto, null); } + @GET + @RolesAllowed({ Role.ADMIN }) + @Path("/filesyntax") + @Produces(MediaType.TEXT_PLAIN) + @Operation(operationId = "generateSyntaxForAllItems", summary = "Generate file syntax for all items.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))), + @ApiResponse(responseCode = "400", description = "Unsupported syntax format.") }) + public Response generateSyntaxForAllItems( + @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, + @DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) { + ItemSyntaxGenerator generator = itemSyntaxGenerators.get(format); + if (generator == null) { + String message = "No syntax available for format " + format + "!"; + return Response.status(Response.Status.BAD_REQUEST).entity(message).build(); + } + return Response.ok(generator.generateSyntax(sortItems(itemRegistry.getAll()), itemChannelLinkRegistry.getAll(), + metadataRegistry.getAll())).build(); + } + + @GET + @RolesAllowed({ Role.ADMIN }) + @Path("/{itemname: [a-zA-Z_0-9]+}/filesyntax") + @Produces(MediaType.TEXT_PLAIN) + @Operation(operationId = "generateSyntaxForItem", summary = "Generate file syntax for an item.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))), + @ApiResponse(responseCode = "400", description = "Unsupported syntax format."), + @ApiResponse(responseCode = "404", description = "Item not found.") }) + public Response generateSyntaxForItem( + @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, + @PathParam("itemname") @Parameter(description = "item name") String itemname, + @DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) { + ItemSyntaxGenerator generator = itemSyntaxGenerators.get(format); + if (generator == null) { + String message = "No syntax available for format " + format + "!"; + return Response.status(Response.Status.BAD_REQUEST).entity(message).build(); + } + + Item item = getItem(itemname); + if (item == null) { + String message = "Item " + itemname + " does not exist!"; + return Response.status(Response.Status.NOT_FOUND).entity(message).build(); + } + + Set channelLinks = itemChannelLinkRegistry.getLinks(itemname); + Set metadata = metadataRegistry.getAll().stream() + .filter(md -> md.getUID().getItemName().equals(itemname)).collect(Collectors.toSet()); + + return Response.ok(generator.generateSyntax(List.of(item), channelLinks, metadata)).build(); + } + private JsonObject buildStatusObject(String itemName, String status, @Nullable String message) { JsonObject jo = new JsonObject(); jo.addProperty("name", itemName); @@ -1006,4 +1078,48 @@ private void addMetadata(EnrichedItemDTO dto, Set namespaces, @Nullable private boolean isEditable(String itemName) { return managedItemProvider.get(itemName) != null; } + + private List sortItems(Collection items) { + return items.stream().sorted((item1, item2) -> { + if (item1.getName().equals(item2.getName())) { + return 0; + } else if (isAncestorGroupOf(item1, item2)) { + return -1; + } else if (isAncestorGroupOf(item2, item1)) { + return 1; + } else if (item1 instanceof GroupItem && !(item2 instanceof GroupItem)) { + return -1; + } else if (item2 instanceof GroupItem && !(item1 instanceof GroupItem)) { + return 1; + } else { + return item1.getName().compareTo(item2.getName()); + } + }).collect(Collectors.toList()); + } + + private boolean isAncestorGroupOf(Item item1, Item item2) { + if (!item1.getName().equals(item2.getName()) && item1 instanceof GroupItem group) { + if (item2 instanceof GroupItem) { + List items = new ArrayList<>(); + fillGroupTree(items, group); + return items.contains(item2); + } else { + return group.getAllMembers().contains(item2); + } + } + return false; + } + + private void fillGroupTree(List items, Item item) { + if (!items.contains(item)) { + items.add(item); + if (item instanceof GroupItem group) { + for (Item member : group.getMembers()) { + if (member instanceof GroupItem groupMember) { + fillGroupTree(items, groupMember); + } + } + } + } + } } diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/thing/ThingResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/thing/ThingResource.java index 99464a6d785..ab8215f38dc 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/thing/ThingResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/thing/ThingResource.java @@ -26,6 +26,8 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.security.RolesAllowed; @@ -72,6 +74,7 @@ import org.openhab.core.items.ItemFactory; import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.ManagedItemProvider; +import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ManagedThingProvider; @@ -95,6 +98,7 @@ import org.openhab.core.thing.i18n.ThingStatusInfoI18nLocalizationService; import org.openhab.core.thing.link.ItemChannelLinkRegistry; import org.openhab.core.thing.link.ManagedItemChannelLinkProvider; +import org.openhab.core.thing.syntaxgenerator.ThingSyntaxGenerator; import org.openhab.core.thing.type.BridgeType; import org.openhab.core.thing.type.ChannelType; import org.openhab.core.thing.type.ChannelTypeRegistry; @@ -106,6 +110,8 @@ import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants; import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired; import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect; @@ -140,6 +146,7 @@ * @author Dimitar Ivanov - replaced Firmware UID with thing UID and firmware version * @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification * @author Wouter Born - Migrated to OpenAPI annotations + * @author Laurent Garnier - Added API to generate file syntax */ @Component @JaxrsResource @@ -171,6 +178,7 @@ public class ThingResource implements RESTResource { private final ThingTypeRegistry thingTypeRegistry; private final RegistryChangedRunnableListener resetLastModifiedChangeListener = new RegistryChangedRunnableListener<>( () -> lastModified = null); + private final Map thingSyntaxGenerators = new ConcurrentHashMap<>(); private @Context @NonNullByDefault({}) UriInfo uriInfo; private @Nullable Date lastModified = null; @@ -215,6 +223,15 @@ void deactivate() { this.thingRegistry.removeRegistryChangeListener(resetLastModifiedChangeListener); } + @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE) + protected void addThingSyntaxGenerator(ThingSyntaxGenerator thingSyntaxGenerator) { + thingSyntaxGenerators.put(thingSyntaxGenerator.getFormat(), thingSyntaxGenerator); + } + + protected void removeThingSyntaxGenerator(ThingSyntaxGenerator thingSyntaxGenerator) { + thingSyntaxGenerators.remove(thingSyntaxGenerator.getFormat()); + } + /** * create a new Thing * @@ -720,6 +737,56 @@ public Response getFirmwares(@PathParam("thingUID") @Parameter(description = "th return Response.ok().entity(new Stream2JSONInputStream(firmwareStream)).build(); } + @GET + @RolesAllowed({ Role.ADMIN }) + @Path("/filesyntax") + @Produces(MediaType.TEXT_PLAIN) + @Operation(operationId = "generateSyntaxForAllThings", summary = "Generate file syntax for all things.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))), + @ApiResponse(responseCode = "400", description = "Unsupported syntax format.") }) + public Response generateSyntaxForAllThings( + @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, + @DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format, + @DefaultValue("true") @QueryParam("preferPresentationAsTree") @Parameter(description = "prefer a presentation as a tree if supported by the generator") boolean preferPresentationAsTree) { + ThingSyntaxGenerator generator = thingSyntaxGenerators.get(format); + if (generator == null) { + String message = "No syntax available for format " + format + "!"; + return Response.status(Response.Status.BAD_REQUEST).entity(message).build(); + } + return Response.ok(generator.generateSyntax(sortThings(thingRegistry.getAll()), preferPresentationAsTree)) + .build(); + } + + @GET + @RolesAllowed({ Role.ADMIN }) + @Path("/{thingUID}/filesyntax") + @Produces(MediaType.TEXT_PLAIN) + @Operation(operationId = "generateSyntaxForThing", summary = "Generate file syntax for a thing.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))), + @ApiResponse(responseCode = "400", description = "Unsupported syntax format."), + @ApiResponse(responseCode = "404", description = "Thing not found.") }) + public Response generateSyntaxForThing( + @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, + @PathParam("thingUID") @Parameter(description = "thingUID") String thingUID, + @DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) { + ThingSyntaxGenerator generator = thingSyntaxGenerators.get(format); + if (generator == null) { + String message = "No syntax available for format " + format + "!"; + return Response.status(Response.Status.BAD_REQUEST).entity(message).build(); + } + + ThingUID aThingUID = new ThingUID(thingUID); + Thing thing = thingRegistry.get(aThingUID); + if (thing == null) { + String message = "Thing " + thingUID + " does not exist!"; + return Response.status(Response.Status.NOT_FOUND).entity(message).build(); + } + + return Response.ok(generator.generateSyntax(List.of(thing), false)).build(); + } + private FirmwareDTO convertToFirmwareDTO(Firmware firmware) { return new FirmwareDTO(firmware.getThingTypeUID().getAsString(), firmware.getVendor(), firmware.getModel(), firmware.isModelRestricted(), firmware.getDescription(), firmware.getVersion(), @@ -886,4 +953,38 @@ private URI getConfigDescriptionURI(ChannelUID channelUID) { throw new BadRequestException("Invalid URI syntax: " + uriString); } } + + /* + * Sort the things first by binding name. + * For things of the same binding, sort the things by UID with the exception that a bridge thing is always + * before any sub-thing. + */ + private List sortThings(Collection things) { + return things.stream().sorted((thing1, thing2) -> { + if (thing1.getUID().equals(thing2.getUID())) { + return 0; + } else if (!thing1.getUID().getBindingId().equals(thing2.getUID().getBindingId())) { + return thing1.getUID().getBindingId().compareTo(thing2.getUID().getBindingId()); + } else { + if (isAncestorOf(thing1, thing2)) { + return -1; + } else if (isAncestorOf(thing2, thing1)) { + return 1; + } else { + return thing1.getUID().getAsString().compareTo(thing2.getUID().getAsString()); + } + } + }).collect(Collectors.toList()); + } + + private boolean isAncestorOf(Thing thing1, Thing thing2) { + if (!thing1.getUID().equals(thing2.getUID())) { + ThingUID bridgeUidThing2 = thing2.getBridgeUID(); + Thing bridgeThing2 = bridgeUidThing2 == null ? null : thingRegistry.get(bridgeUidThing2); + if (thing1 instanceof Bridge && bridgeThing2 != null) { + return thing1.getUID().equals(bridgeThing2.getUID()) ? true : isAncestorOf(thing1, bridgeThing2); + } + } + return false; + } } diff --git a/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/ModelRepository.java b/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/ModelRepository.java index 2f216b626b3..15a8e758d16 100644 --- a/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/ModelRepository.java +++ b/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/ModelRepository.java @@ -26,6 +26,7 @@ * come from. * * @author Kai Kreuzer - Initial contribution + * @author Laurent Garnier - Added method generateSyntaxFromModelContent */ @NonNullByDefault public interface ModelRepository { @@ -92,4 +93,13 @@ public interface ModelRepository { * @param listener the listener to remove */ void removeModelRepositoryChangeListener(ModelRepositoryChangeListener listener); + + /** + * Generate the syntax from a provided model content. + * + * @param extension the kind of model ("items", "things", ...) + * @param content the content of the model + * @return the corresponding syntax + */ + String generateSyntaxFromModelContent(String extension, EObject content); } diff --git a/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/internal/ModelRepositoryImpl.java b/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/internal/ModelRepositoryImpl.java index 736ac28fdad..4d5ddd45bba 100644 --- a/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/internal/ModelRepositoryImpl.java +++ b/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/internal/ModelRepositoryImpl.java @@ -13,6 +13,7 @@ package org.openhab.core.model.core.internal; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -50,6 +51,7 @@ * @author Kai Kreuzer - Initial contribution * @author Oliver Libutzki - Added reloadAllModelsOfType method * @author Simon Kaufmann - added validation of models before loading them + * @author Laurent Garnier - Added method generateSyntaxFromModelContent */ @Component(immediate = true) @NonNullByDefault @@ -227,6 +229,25 @@ public void removeModelRepositoryChangeListener(ModelRepositoryChangeListener li listeners.remove(listener); } + @Override + public String generateSyntaxFromModelContent(String extension, EObject content) { + String result = ""; + synchronized (resourceSet) { + Resource resource = resourceSet.createResource(URI.createURI("tmp_generated_syntax." + extension)); + try { + resource.getContents().add(content); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + resource.save(outputStream, Map.of(XtextResource.OPTION_ENCODING, StandardCharsets.UTF_8.name())); + result = new String(outputStream.toByteArray()); + } catch (IOException e) { + logger.warn("Exception when saving the model {}", resource.getURI().lastSegment()); + } finally { + resourceSet.getResources().remove(resource); + } + } + return result; + } + private @Nullable Resource getResource(String name) { return resourceSet.getResource(URI.createURI(name), false); } diff --git a/bundles/org.openhab.core.model.item/src/org/openhab/core/model/ItemsRuntimeModule.xtend b/bundles/org.openhab.core.model.item/src/org/openhab/core/model/ItemsRuntimeModule.xtend index fc01add6fcb..90ce09fee57 100644 --- a/bundles/org.openhab.core.model.item/src/org/openhab/core/model/ItemsRuntimeModule.xtend +++ b/bundles/org.openhab.core.model.item/src/org/openhab/core/model/ItemsRuntimeModule.xtend @@ -18,19 +18,24 @@ org.openhab.core.model import com.google.inject.Binder import com.google.inject.name.Names +import org.openhab.core.model.formatting.ItemsFormatter import org.openhab.core.model.internal.valueconverter.ItemValueConverters import org.eclipse.xtext.conversion.IValueConverterService +import org.eclipse.xtext.formatting.IFormatter import org.eclipse.xtext.linking.lazy.LazyURIEncoder /** * Use this class to register components to be used at runtime / without the Equinox extension registry. */ class ItemsRuntimeModule extends AbstractItemsRuntimeModule { - override Class bindIValueConverterService() { return ItemValueConverters } - + + override Class bindIFormatter() { + return ItemsFormatter + } + override void configureUseIndexFragmentsForLazyLinking(Binder binder) { binder.bind(Boolean.TYPE).annotatedWith(Names.named(LazyURIEncoder.USE_INDEXED_FRAGMENTS_BINDING)).toInstance( Boolean.FALSE) diff --git a/bundles/org.openhab.core.model.item/src/org/openhab/core/model/formatting/ItemsFormatter.xtend b/bundles/org.openhab.core.model.item/src/org/openhab/core/model/formatting/ItemsFormatter.xtend index 7f45bc9aada..28cea5338d9 100644 --- a/bundles/org.openhab.core.model.item/src/org/openhab/core/model/formatting/ItemsFormatter.xtend +++ b/bundles/org.openhab.core.model.item/src/org/openhab/core/model/formatting/ItemsFormatter.xtend @@ -26,17 +26,17 @@ class ItemsFormatter extends AbstractDeclarativeFormatter { override protected void configureFormatting(FormattingConfig c) { c.setLinewrap(1, 1, 2).before(modelGroupItemRule) - c.setLinewrap(1, 1, 2).before(modelItemTypeRule) + c.setLinewrap(1, 1, 2).before(modelNormalItemRule) c.setNoSpace().withinKeywordPairs("<", ">") c.setNoSpace().withinKeywordPairs("(", ")") + c.setNoSpace().withinKeywordPairs("[", "]") - c.setIndentationIncrement.after(modelItemTypeRule) - c.setIndentationDecrement.before(modelItemTypeRule) - c.setIndentationIncrement.after(modelGroupItemRule) - c.setIndentationDecrement.before(modelGroupItemRule) + c.setNoSpace().around(":", "=") + c.setNoSpace().before(",") + + c.autoLinewrap = 250 - c.autoLinewrap = 160 c.setLinewrap(0, 1, 2).before(SL_COMMENTRule) c.setLinewrap(0, 1, 2).before(ML_COMMENTRule) c.setLinewrap(0, 1, 1).after(ML_COMMENTRule) @@ -48,4 +48,16 @@ class ItemsFormatter extends AbstractDeclarativeFormatter { locator.before(pair.second) } } + + def around(FormattingConfig.ElementLocator locator, String ... listKW) { + for (keyword : findKeywords(listKW)) { + locator.around(keyword) + } + } + + def before(FormattingConfig.ElementLocator locator, String ... listKW) { + for (keyword : findKeywords(listKW)) { + locator.before(keyword) + } + } } diff --git a/bundles/org.openhab.core.model.thing/bnd.bnd b/bundles/org.openhab.core.model.thing/bnd.bnd index c1bfc49cfd8..084ceb712de 100644 --- a/bundles/org.openhab.core.model.thing/bnd.bnd +++ b/bundles/org.openhab.core.model.thing/bnd.bnd @@ -17,13 +17,16 @@ Import-Package: org.apache.log4j,\ org.openhab.core.common.registry,\ org.openhab.core.i18n,\ org.openhab.core.items,\ + org.openhab.core.model.items,\ org.openhab.core.service,\ org.openhab.core.thing,\ org.openhab.core.thing.binding,\ org.openhab.core.thing.binding.builder,\ org.openhab.core.thing.link,\ + org.openhab.core.thing.syntaxgenerator,\ org.openhab.core.thing.type,\ org.openhab.core.thing.util,\ + org.openhab.core.types,\ org.openhab.core.types.util,\ org.openhab.core.util,\ org.openhab.core.model.core,\ diff --git a/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/ThingRuntimeModule.xtend b/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/ThingRuntimeModule.xtend index 1a7b7fcabae..7b033310b55 100644 --- a/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/ThingRuntimeModule.xtend +++ b/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/ThingRuntimeModule.xtend @@ -12,11 +12,13 @@ */ package org.openhab.core.model.thing +import com.google.inject.Binder +import com.google.inject.name.Names +import org.openhab.core.model.thing.formatting.ThingFormatter import org.openhab.core.model.thing.valueconverter.ThingValueConverters import org.eclipse.xtext.conversion.IValueConverterService +import org.eclipse.xtext.formatting.IFormatter import org.eclipse.xtext.linking.lazy.LazyURIEncoder -import com.google.inject.Binder -import com.google.inject.name.Names /** * Use this class to register components to be used at runtime / without the Equinox extension registry. @@ -30,6 +32,10 @@ import com.google.inject.name.Names return org.openhab.core.model.thing.serializer.ThingSyntacticSequencerExtension } + override Class bindIFormatter() { + return ThingFormatter + } + override void configureUseIndexFragmentsForLazyLinking(Binder binder) { binder.bind(Boolean.TYPE).annotatedWith(Names.named(LazyURIEncoder.USE_INDEXED_FRAGMENTS_BINDING)).toInstance( Boolean.FALSE) diff --git a/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/formatting/ThingFormatter.xtend b/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/formatting/ThingFormatter.xtend index 45d855a59ef..f9d4f8d6272 100644 --- a/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/formatting/ThingFormatter.xtend +++ b/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/formatting/ThingFormatter.xtend @@ -17,8 +17,8 @@ package org.openhab.core.model.thing.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.thing.services.ThingGrammarAccess +import com.google.inject.Inject; +import org.openhab.core.model.thing.services.ThingGrammarAccess /** * This class contains custom formatting description. @@ -30,13 +30,51 @@ import org.eclipse.xtext.formatting.impl.FormattingConfig */ class ThingFormatter extends AbstractDeclarativeFormatter { -// @Inject extension ThingGrammarAccess - + @Inject extension ThingGrammarAccess + 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) + c.setLinewrap(1, 1, 2).before("Bridge") + c.setLinewrap(1, 1, 2).before(modelThingRule) + c.setLinewrap(1, 1, 2).before(modelChannelRule) + + c.setIndentationIncrement.after("{") + c.setIndentationDecrement.before("}") + c.setLinewrap().before("}") + + c.setNoSpace().withinKeywordPairs("(", ")") + c.setNoSpace().withinKeywordPairs("[", "]") + c.setNoSpace().around("=") + c.setNoSpace().before(",") + + c.autoLinewrap = 250 + + c.setLinewrap(0, 1, 2).before(SL_COMMENTRule) + c.setLinewrap(0, 1, 2).before(ML_COMMENTRule) + c.setLinewrap(0, 1, 1).after(ML_COMMENTRule) + } + + def withinKeywordPairs(FormattingConfig.NoSpaceLocator locator, String leftKW, String rightKW) { + for (pair : findKeywordPairs(leftKW, rightKW)) { + locator.after(pair.first) + locator.before(pair.second) + } + } + + def around(FormattingConfig.ElementLocator locator, String ... listKW) { + for (keyword : findKeywords(listKW)) { + locator.around(keyword) + } + } + + def after(FormattingConfig.ElementLocator locator, String ... listKW) { + for (keyword : findKeywords(listKW)) { + locator.after(keyword) + } + } + + def before(FormattingConfig.ElementLocator locator, String ... listKW) { + for (keyword : findKeywords(listKW)) { + locator.before(keyword) + } } } diff --git a/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/syntaxgenerator/ItemDslSyntaxGenerator.java b/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/syntaxgenerator/ItemDslSyntaxGenerator.java new file mode 100644 index 00000000000..3ab385755a5 --- /dev/null +++ b/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/syntaxgenerator/ItemDslSyntaxGenerator.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2010-2025 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.thing.internal.syntaxgenerator; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.config.core.ConfigDescriptionRegistry; +import org.openhab.core.items.GroupFunction; +import org.openhab.core.items.GroupItem; +import org.openhab.core.items.Item; +import org.openhab.core.items.Metadata; +import org.openhab.core.model.core.ModelRepository; +import org.openhab.core.model.items.ItemModel; +import org.openhab.core.model.items.ItemsFactory; +import org.openhab.core.model.items.ModelBinding; +import org.openhab.core.model.items.ModelGroupFunction; +import org.openhab.core.model.items.ModelGroupItem; +import org.openhab.core.model.items.ModelItem; +import org.openhab.core.model.items.ModelProperty; +import org.openhab.core.thing.link.ItemChannelLink; +import org.openhab.core.thing.syntaxgenerator.AbstractItemSyntaxGenerator; +import org.openhab.core.thing.syntaxgenerator.ItemSyntaxGenerator; +import org.openhab.core.types.State; +import org.openhab.core.types.StateDescription; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * {@link ItemDslSyntaxGenerator} is the DSL syntax generator for {@link Item} object. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = ItemSyntaxGenerator.class) +public class ItemDslSyntaxGenerator extends AbstractItemSyntaxGenerator { + + private final ModelRepository modelRepository; + + @Activate + public ItemDslSyntaxGenerator(final @Reference ModelRepository modelRepository, + final @Reference ConfigDescriptionRegistry configDescRegistry) { + super(configDescRegistry); + this.modelRepository = modelRepository; + } + + @Override + public String getFormat() { + return "DSL"; + } + + @Override + public synchronized String generateSyntax(List items, Collection channelLinks, + Collection metadata) { + ItemModel model = ItemsFactory.eINSTANCE.createItemModel(); + for (Item item : items) { + model.getItems().add(buildModelItem(item, getChannelLinks(channelLinks, item.getName()), + getMetadata(metadata, item.getName()))); + } + return modelRepository.generateSyntaxFromModelContent("items", model); + } + + private ModelItem buildModelItem(Item item, List channelLinks, List metadata) { + ModelItem model; + if (item instanceof GroupItem groupItem) { + ModelGroupItem modelGroup = ItemsFactory.eINSTANCE.createModelGroupItem(); + model = modelGroup; + Item baseItem = groupItem.getBaseItem(); + if (baseItem != null) { + modelGroup.setType(baseItem.getType()); + GroupFunction function = groupItem.getFunction(); + if (function != null) { + ModelGroupFunction modelFunction = ModelGroupFunction + .getByName(function.getClass().getSimpleName().toUpperCase()); + modelGroup.setFunction(modelFunction); + State[] parameters = function.getParameters(); + for (int i = 0; i < parameters.length; i++) { + modelGroup.getArgs().add(parameters[i].toString()); + } + } + } + } else { + model = ItemsFactory.eINSTANCE.createModelNormalItem(); + model.setType(item.getType()); + } + + model.setName(item.getName()); + String label = item.getLabel(); + boolean patternInjected = false; + String defaultPattern = getDefaultStatePattern(item); + if (label != null && !label.isEmpty()) { + StateDescription stateDescr = item.getStateDescription(); + String statePattern = stateDescr == null ? null : stateDescr.getPattern(); + String patterToInject = statePattern != null && !statePattern.equals(defaultPattern) ? statePattern : null; + if (patterToInject != null) { + // Inject the pattern in the label + patternInjected = true; + model.setLabel("%s [%s]".formatted(label, patterToInject)); + } else { + model.setLabel(label); + } + } + + String category = item.getCategory(); + if (category != null && !category.isEmpty()) { + model.setIcon(category); + } + for (String group : item.getGroupNames()) { + model.getGroups().add(group); + } + for (String tag : item.getTags().stream().sorted().collect(Collectors.toList())) { + model.getTags().add(tag); + } + + for (ItemChannelLink channelLink : channelLinks) { + ModelBinding binding = ItemsFactory.eINSTANCE.createModelBinding(); + binding.setType("channel"); + binding.setConfiguration(channelLink.getLinkedUID().getAsString()); + for (ConfigParameter param : getConfigurationParameters(channelLink)) { + binding.getProperties().add(buildModelProperty(param.name(), param.value())); + } + model.getBindings().add(binding); + } + + for (Metadata md : metadata) { + String namespace = md.getUID().getNamespace(); + ModelBinding binding = ItemsFactory.eINSTANCE.createModelBinding(); + binding.setType(namespace); + binding.setConfiguration(md.getValue()); + String statePattern = null; + for (ConfigParameter param : getConfigurationParameters(md)) { + binding.getProperties().add(buildModelProperty(param.name(), param.value())); + if ("stateDescription".equals(namespace) && "pattern".equals(param.name())) { + statePattern = param.value().toString(); + } + } + // Ignore state description in case it contains only a state pattern and state pattern was injected + // in the item label or is the default pattern + if (!(statePattern != null && binding.getProperties().size() == 1 + && (patternInjected || statePattern.equals(defaultPattern)))) { + model.getBindings().add(binding); + } + } + + return model; + } + + private ModelProperty buildModelProperty(String key, Object value) { + ModelProperty property = ItemsFactory.eINSTANCE.createModelProperty(); + property.setKey(key); + if (value instanceof List list) { + property.getValue().addAll(list); + } else { + property.getValue().add(value); + } + return property; + } +} diff --git a/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/syntaxgenerator/ThingDslSyntaxGenerator.java b/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/syntaxgenerator/ThingDslSyntaxGenerator.java new file mode 100644 index 00000000000..9cc9cbe626f --- /dev/null +++ b/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/syntaxgenerator/ThingDslSyntaxGenerator.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2010-2025 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.thing.internal.syntaxgenerator; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.config.core.ConfigDescriptionRegistry; +import org.openhab.core.model.core.ModelRepository; +import org.openhab.core.model.thing.thing.ModelBridge; +import org.openhab.core.model.thing.thing.ModelChannel; +import org.openhab.core.model.thing.thing.ModelProperty; +import org.openhab.core.model.thing.thing.ModelThing; +import org.openhab.core.model.thing.thing.ThingFactory; +import org.openhab.core.model.thing.thing.ThingModel; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.syntaxgenerator.AbstractThingSyntaxGenerator; +import org.openhab.core.thing.syntaxgenerator.ThingSyntaxGenerator; +import org.openhab.core.thing.type.ChannelKind; +import org.openhab.core.thing.type.ChannelTypeRegistry; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.thing.type.ThingTypeRegistry; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * {@link ThingDslSyntaxGenerator} is the DSL syntax generator for {@link Thing} object. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = ThingSyntaxGenerator.class) +public class ThingDslSyntaxGenerator extends AbstractThingSyntaxGenerator { + + private final ModelRepository modelRepository; + + @Activate + public ThingDslSyntaxGenerator(final @Reference ModelRepository modelRepository, + final @Reference ThingTypeRegistry thingTypeRegistry, + final @Reference ChannelTypeRegistry channelTypeRegistry, + final @Reference ConfigDescriptionRegistry configDescRegistry) { + super(thingTypeRegistry, channelTypeRegistry, configDescRegistry); + this.modelRepository = modelRepository; + } + + @Override + public String getFormat() { + return "DSL"; + } + + @Override + public synchronized String generateSyntax(List things, boolean preferPresentationAsTree) { + ThingModel model = ThingFactory.eINSTANCE.createThingModel(); + Set handledThings = new HashSet<>(); + for (Thing thing : things) { + if (handledThings.contains(thing)) { + continue; + } + model.getThings().add(buildModelThing(thing, preferPresentationAsTree, true, things, handledThings)); + } + String result = modelRepository.generateSyntaxFromModelContent("things", model); + // Double quotes are unexpectedly generated in thing UID when the segment contains a -. + // Fix that by removing these double quotes. + return result.replaceAll(":\"([a-zA-Z0-9_][a-zA-Z0-9_-]*)\"", ":$1"); + } + + private ModelThing buildModelThing(Thing thing, boolean preferPresentationAsTree, boolean topLevel, + List onlyThings, Set handledThings) { + ModelThing model; + ModelBridge modelBridge; + if (preferPresentationAsTree && thing instanceof Bridge) { + modelBridge = ThingFactory.eINSTANCE.createModelBridge(); + modelBridge.setBridge(true); + model = modelBridge; + } else { + modelBridge = null; + model = ThingFactory.eINSTANCE.createModelThing(); + } + if (!preferPresentationAsTree || topLevel) { + model.setId(thing.getUID().getAsString()); + ThingUID bridgeUID = thing.getBridgeUID(); + if (bridgeUID != null && modelBridge == null) { + model.setBridgeUID(bridgeUID.getAsString()); + } + } else { + model.setThingTypeId(thing.getThingTypeUID().getId()); + model.setThingId(thing.getUID().getId()); + } + if (thing.getLabel() != null) { + model.setLabel(thing.getLabel()); + } + if (thing.getLocation() != null) { + model.setLocation(thing.getLocation()); + } + + for (ConfigParameter param : getConfigurationParameters(thing)) { + model.getProperties().add(buildModelProperty(param.name(), param.value())); + } + + if (preferPresentationAsTree && modelBridge != null) { + for (Thing child : getChildThings(thing)) { + if (onlyThings.contains(child) && !handledThings.contains(child)) { + modelBridge.getThings().add(buildModelThing(child, true, false, onlyThings, handledThings)); + } + } + } + + for (Channel channel : getNonDefaultChannels(thing)) { + model.getChannels().add(buildModelChannel(channel)); + } + + handledThings.add(thing); + + return model; + } + + private ModelChannel buildModelChannel(Channel channel) { + ModelChannel modelChannel = ThingFactory.eINSTANCE.createModelChannel(); + ChannelTypeUID channelTypeUID = channel.getChannelTypeUID(); + if (channelTypeUID != null) { + modelChannel.setChannelType(channelTypeUID.getId()); + } else { + modelChannel.setChannelKind(channel.getKind() == ChannelKind.STATE ? "State" : "Trigger"); + modelChannel.setType(channel.getAcceptedItemType()); + } + modelChannel.setId(channel.getUID().getId()); + if (channel.getLabel() != null) { + modelChannel.setLabel(channel.getLabel()); + } + for (ConfigParameter param : getConfigurationParameters(channel)) { + modelChannel.getProperties().add(buildModelProperty(param.name(), param.value())); + } + return modelChannel; + } + + private ModelProperty buildModelProperty(String key, Object value) { + ModelProperty property = ThingFactory.eINSTANCE.createModelProperty(); + property.setKey(key); + if (value instanceof List list) { + property.getValue().addAll(list); + } else { + property.getValue().add(value); + } + return property; + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/AbstractItemSyntaxGenerator.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/AbstractItemSyntaxGenerator.java new file mode 100644 index 00000000000..4e22b68c6fa --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/AbstractItemSyntaxGenerator.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2010-2025 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.thing.syntaxgenerator; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.config.core.ConfigDescription; +import org.openhab.core.config.core.ConfigDescriptionParameter; +import org.openhab.core.config.core.ConfigDescriptionRegistry; +import org.openhab.core.config.core.ConfigUtil; +import org.openhab.core.items.GroupItem; +import org.openhab.core.items.Item; +import org.openhab.core.items.Metadata; +import org.openhab.core.library.CoreItemFactory; +import org.openhab.core.thing.link.ItemChannelLink; + +/** + * {@link AbstractItemSyntaxGenerator} is the base class for any {@link Item} syntax generator. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractItemSyntaxGenerator implements ItemSyntaxGenerator { + + private final ConfigDescriptionRegistry configDescRegistry; + + public AbstractItemSyntaxGenerator(ConfigDescriptionRegistry configDescRegistry) { + this.configDescRegistry = configDescRegistry; + } + + /** + * {@link ConfigParameter} is a container for any configuration parameter defined by a name and a value. + */ + protected record ConfigParameter(String name, Object value) { + } + + /** + * Get the list of available channel links for an item, sorted by natural order of their UID. + * + * @param channelLinks a collection of channel links + * @param itemName the item name + * @return the sorted list of channel links for this item + */ + protected List getChannelLinks(Collection channelLinks, String itemName) { + return channelLinks.stream().filter(link -> link.getItemName().equals(itemName)).sorted((link1, link2) -> { + return link1.getLinkedUID().getAsString().compareTo(link2.getLinkedUID().getAsString()); + }).collect(Collectors.toList()); + } + + /** + * Get the list of configuration parameters for a channel link. + * + * If a profile is set and a configuration description is found for this profile, the parameters are provided + * in the same order as in this configuration description, and any parameter having the default value is ignored. + * If no profile is set, the parameters are provided sorted by natural order of their names. + * + * @param channelLink the channel link + * @return a sorted list of configuration parameters for the channel link + */ + protected List getConfigurationParameters(ItemChannelLink channelLink) { + List parameters = new ArrayList<>(); + Map configParameters = channelLink.getConfiguration().getProperties(); + Set handledNames = new HashSet<>(); + Object profile = configParameters.get("profile"); + List configDescriptionParameter = List.of(); + if (profile instanceof String profileStr) { + parameters.add(new ConfigParameter("profile", profileStr)); + handledNames.add("profile"); + try { + ConfigDescription configDesc = configDescRegistry + .getConfigDescription(new URI("profile:" + profileStr)); + if (configDesc != null) { + configDescriptionParameter = configDesc.getParameters(); + } + } catch (URISyntaxException e) { + // Ignored; in practice this will never be thrown + } + } + for (ConfigDescriptionParameter param : configDescriptionParameter) { + String paramName = param.getName(); + Object value = configParameters.get(paramName); + Object defaultValue = ConfigUtil.getDefaultValueAsCorrectType(param); + if (value != null && !value.equals(defaultValue)) { + parameters.add(new ConfigParameter(paramName, value)); + } + handledNames.add(paramName); + } + for (String paramName : configParameters.keySet().stream().sorted().collect(Collectors.toList())) { + Object value = configParameters.get(paramName); + if (!handledNames.contains(paramName) && value != null) { + parameters.add(new ConfigParameter(paramName, value)); + } + } + return parameters; + } + + /** + * Get the list of available metadata for an item, sorted by natural order of their namespaces. + * The "semantics" namespace is ignored. + * + * @param metadata a collection of metadata + * @param itemName the item name + * @return the sorted list of metadata for this item + */ + protected List getMetadata(Collection metadata, String itemName) { + return metadata.stream().filter( + md -> !"semantics".equals(md.getUID().getNamespace()) && md.getUID().getItemName().equals(itemName)) + .sorted((md1, md2) -> { + return md1.getUID().getNamespace().compareTo(md2.getUID().getNamespace()); + }).collect(Collectors.toList()); + } + + /** + * Get the list of configuration parameters for a metadata, sorted by natural order of their names + * with the exception of the "stateDescription" namespace where "min", "max" and "step" parameters + * are provided at first in this order. + * + * @param metadata the metadata + * @return a sorted list of configuration parameters for the metadata + */ + protected List getConfigurationParameters(Metadata metadata) { + String namespace = metadata.getUID().getNamespace(); + Map configParams = metadata.getConfiguration(); + List paramNames = configParams.keySet().stream().sorted((key1, key2) -> { + if ("stateDescription".equals(namespace)) { + if ("min".equals(key1)) { + return -1; + } else if ("min".equals(key2)) { + return 1; + } else if ("max".equals(key1)) { + return -1; + } else if ("max".equals(key2)) { + return 1; + } else if ("step".equals(key1)) { + return -1; + } else if ("step".equals(key2)) { + return 1; + } + } + return key1.compareTo(key2); + }).collect(Collectors.toList()); + + List parameters = new ArrayList<>(); + for (String paramName : paramNames) { + Object value = configParams.get(paramName); + if (value != null) { + parameters.add(new ConfigParameter(paramName, value)); + } + } + return parameters; + } + + /** + * Get the default state pattern for an item. + * + * @param item the item + * @return the default state pattern of null if no default + */ + protected @Nullable String getDefaultStatePattern(Item item) { + String pattern = null; + if (item instanceof GroupItem group) { + Item baseItem = group.getBaseItem(); + if (baseItem != null) { + pattern = getDefaultStatePattern(baseItem); + } + } else if (item.getType().startsWith(CoreItemFactory.NUMBER + ":")) { + pattern = "%.0f %unit%"; + } else { + switch (item.getType()) { + case CoreItemFactory.STRING: + pattern = "%s"; + break; + case CoreItemFactory.DATETIME: + pattern = "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS"; + break; + case CoreItemFactory.NUMBER: + pattern = "%.0f"; + break; + default: + break; + } + } + return pattern; + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/AbstractThingSyntaxGenerator.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/AbstractThingSyntaxGenerator.java new file mode 100644 index 00000000000..f1daa5ea548 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/AbstractThingSyntaxGenerator.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2010-2025 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.thing.syntaxgenerator; + +import java.net.URI; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.config.core.ConfigDescription; +import org.openhab.core.config.core.ConfigDescriptionParameter; +import org.openhab.core.config.core.ConfigDescriptionRegistry; +import org.openhab.core.config.core.ConfigUtil; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.type.ChannelType; +import org.openhab.core.thing.type.ChannelTypeRegistry; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.thing.type.ThingType; +import org.openhab.core.thing.type.ThingTypeRegistry; +import org.osgi.service.component.annotations.Activate; + +/** + * {@link AbstractThingSyntaxGenerator} is the base class for any {@link Thing} syntax generator. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractThingSyntaxGenerator implements ThingSyntaxGenerator { + + private final ThingTypeRegistry thingTypeRegistry; + private final ChannelTypeRegistry channelTypeRegistry; + private final ConfigDescriptionRegistry configDescRegistry; + + @Activate + public AbstractThingSyntaxGenerator(ThingTypeRegistry thingTypeRegistry, ChannelTypeRegistry channelTypeRegistry, + ConfigDescriptionRegistry configDescRegistry) { + this.thingTypeRegistry = thingTypeRegistry; + this.channelTypeRegistry = channelTypeRegistry; + this.configDescRegistry = configDescRegistry; + } + + /** + * {@link ConfigParameter} is a container for any configuration parameter defined by a name and a value. + */ + protected record ConfigParameter(String name, Object value) { + } + + /** + * Get the child things of a bridge thing, ordered by UID. + * + * @param thing the thing + * @return the sorted list of child things or an empty list if the thing is not a bridge thing + */ + protected List getChildThings(Thing thing) { + if (thing instanceof Bridge bridge) { + return bridge.getThings().stream().sorted((thing1, thing2) -> { + return thing1.getUID().getAsString().compareTo(thing2.getUID().getAsString()); + }).collect(Collectors.toList()); + } + return List.of(); + } + + /** + * Get the list of configuration parameters for a thing. + * + * If a configuration description is found for the thing type, the parameters are provided in the same order + * as in this configuration description, and any parameter having the default value is ignored. + * If not, the parameters are provided sorted by natural order of their names. + * + * @param thing the thing + * @return the sorted list of configuration parameters for the thing + */ + protected List getConfigurationParameters(Thing thing) { + return getConfigurationParameters(getConfigDescriptionParameters(thing), thing.getConfiguration()); + } + + /** + * Get the list of configuration parameters for a channel. + * + * If a configuration description is found for the channel type, the parameters are provided in the same order + * as in this configuration description, and any parameter having the default value is ignored. + * If not, the parameters are provided sorted by natural order of their names. + * + * @param thing the channel + * @return the sorted list of configuration parameters for the channel + */ + protected List getConfigurationParameters(Channel channel) { + return getConfigurationParameters(getConfigDescriptionParameters(channel), channel.getConfiguration()); + } + + private List getConfigurationParameters( + List configDescriptionParameter, Configuration configParameters) { + List parameters = new ArrayList<>(); + Set handledNames = new HashSet<>(); + for (ConfigDescriptionParameter param : configDescriptionParameter) { + String paramName = param.getName(); + Object value = configParameters.get(paramName); + Object defaultValue = ConfigUtil.getDefaultValueAsCorrectType(param); + if (value != null && !value.equals(defaultValue)) { + parameters.add(new ConfigParameter(paramName, value)); + } + handledNames.add(paramName); + } + for (String paramName : configParameters.keySet().stream().sorted().collect(Collectors.toList())) { + Object value = configParameters.get(paramName); + if (!handledNames.contains(paramName) && value != null) { + parameters.add(new ConfigParameter(paramName, value)); + } + } + return parameters; + } + + private List getConfigDescriptionParameters(Thing thing) { + List configParams = null; + ThingType thingType = thingTypeRegistry.getThingType(thing.getThingTypeUID()); + if (thingType != null) { + configParams = getConfigDescriptionParameters(thingType.getConfigDescriptionURI()); + } + return configParams != null ? configParams : List.of(); + } + + private List getConfigDescriptionParameters(Channel channel) { + List configParams = null; + ChannelTypeUID channelTypeUID = channel.getChannelTypeUID(); + if (channelTypeUID != null) { + ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID); + if (channelType != null) { + configParams = getConfigDescriptionParameters(channelType.getConfigDescriptionURI()); + } + } + return configParams != null ? configParams : List.of(); + } + + private @Nullable List getConfigDescriptionParameters(@Nullable URI descURI) { + if (descURI != null) { + ConfigDescription configDesc = configDescRegistry.getConfigDescription(descURI); + if (configDesc != null) { + return configDesc.getParameters(); + } + } + return null; + } + + /** + * Get non default channels. + * It includes extensible channels and channels with a non default configuration. + * + * Resulting channels are sorted in such a way that channels without channel type are after channels + * with a channel type. Sort is done first on the channel type and then on the channel UID. + * + * @param thing the thing + * @return the sorted list of channels + */ + protected List getNonDefaultChannels(Thing thing) { + ThingType thingType = thingTypeRegistry.getThingType(thing.getThingTypeUID()); + List ids = thingType != null ? thingType.getExtensibleChannelTypeIds() : List.of(); + List channels = thing + .getChannels().stream().filter(ch -> ch.getChannelTypeUID() == null + || ids.contains(ch.getChannelTypeUID().getId()) || channelWithNonDefaultConfig(ch)) + .collect(Collectors.toList()); + return channels.stream().sorted((ch1, ch2) -> { + ChannelTypeUID typeUID1 = ch1.getChannelTypeUID(); + ChannelTypeUID typeUID2 = ch2.getChannelTypeUID(); + if (typeUID1 != null && typeUID2 == null) { + return -1; + } else if (typeUID1 == null && typeUID2 != null) { + return 1; + } else if (typeUID1 != null && typeUID2 != null && !typeUID1.equals(typeUID2)) { + return typeUID1.getAsString().compareTo(typeUID2.getAsString()); + } else { + return ch1.getUID().getAsString().compareTo(ch2.getUID().getAsString()); + } + }).collect(Collectors.toList()); + } + + private boolean channelWithNonDefaultConfig(Channel channel) { + for (ConfigDescriptionParameter param : getConfigDescriptionParameters(channel)) { + Object value = channel.getConfiguration().get(param.getName()); + if (value != null) { + value = ConfigUtil.normalizeType(value, param); + if (!value.equals(ConfigUtil.getDefaultValueAsCorrectType(param))) { + return true; + } + } + } + return false; + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/ItemSyntaxGenerator.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/ItemSyntaxGenerator.java new file mode 100644 index 00000000000..1b844e997a3 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/ItemSyntaxGenerator.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2010-2025 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.thing.syntaxgenerator; + +import java.util.Collection; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.items.Item; +import org.openhab.core.items.Metadata; +import org.openhab.core.thing.link.ItemChannelLink; + +/** + * {@link ItemSyntaxGenerator} is the interface to implement by any syntax generator for {@link Item} object. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public interface ItemSyntaxGenerator { + + /** + * Returns the format of the syntax. + * + * @return the syntax format + */ + String getFormat(); + + /** + * Generate the syntax for a sorted list of items. + * + * @param items the items + * @param channelLinks the provided collection of channel links for these items + * @param metadata the provided collection of metadata for these items + * @return the syntax for the items + */ + String generateSyntax(List items, Collection channelLinks, Collection metadata); +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/ThingSyntaxGenerator.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/ThingSyntaxGenerator.java new file mode 100644 index 00000000000..b83cf7698ed --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/ThingSyntaxGenerator.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2010-2025 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.thing.syntaxgenerator; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.Thing; + +/** + * {@link ThingSyntaxGenerator} is the interface to implement by any syntax generator for {@link Thing} object. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public interface ThingSyntaxGenerator { + + /** + * Returns the format of the syntax. + * + * @return the syntax format + */ + String getFormat(); + + /** + * Generate the syntax for a sorted list of things. + * + * @param things the things + * @param preferPresentationAsTree true if presentation as a tree is preferred (support by generators is optional) + * @return the syntax for the things + */ + String generateSyntax(List things, boolean preferPresentationAsTree); +} diff --git a/itests/org.openhab.core.io.rest.core.tests/src/main/java/org/openhab/core/io/rest/core/internal/discovery/InboxResourceOSGITest.java b/itests/org.openhab.core.io.rest.core.tests/src/main/java/org/openhab/core/io/rest/core/internal/discovery/InboxResourceOSGITest.java index 7d940a544e7..53d9bf03d01 100644 --- a/itests/org.openhab.core.io.rest.core.tests/src/main/java/org/openhab/core/io/rest/core/internal/discovery/InboxResourceOSGITest.java +++ b/itests/org.openhab.core.io.rest.core.tests/src/main/java/org/openhab/core/io/rest/core/internal/discovery/InboxResourceOSGITest.java @@ -32,6 +32,7 @@ import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.type.ThingTypeRegistry; /** * @author Christoph Knauf - Initial contribution @@ -48,13 +49,16 @@ public class InboxResourceOSGITest extends JavaOSGiTest { private @NonNullByDefault({}) InboxResource resource; private @Mock @NonNullByDefault({}) Inbox inboxMock; + private @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistryMock; + private @Mock @NonNullByDefault({}) ConfigDescriptionRegistry configDescRegistryMock; @BeforeEach public void beforeEach() throws Exception { ConfigDescriptionRegistry configDescRegistry = getService(ConfigDescriptionRegistry.class); assertNotNull(configDescRegistry); - registerService(new InboxResource(inboxMock), InboxResource.class.getName()); + registerService(new InboxResource(inboxMock, thingTypeRegistryMock, configDescRegistryMock), + InboxResource.class.getName()); resource = getService(InboxResource.class); assertNotNull(resource); }