From 98c24062691314cbe67deab42328dc350e76409e 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 Signed-off-by: Laurent Garnier --- .../internal/discovery/InboxResource.java | 82 ++++++- .../rest/core/internal/item/ItemResource.java | 51 ++++- .../core/internal/thing/ThingResource.java | 54 ++++- .../core/model/core/ModelRepository.java | 9 + .../core/internal/ModelRepositoryImpl.java | 18 ++ .../core/model/ItemsRuntimeModule.xtend | 9 +- .../model/formatting/ItemsFormatter.xtend | 10 +- bundles/org.openhab.core.model.thing/bnd.bnd | 3 + .../core/model/thing/ThingRuntimeModule.xtend | 10 +- .../thing/formatting/ThingFormatter.xtend | 15 +- .../ItemDslSyntaxGenerator.java | 172 +++++++++++++++ .../ThingDslSyntaxGenerator.java | 164 ++++++++++++++ .../SyntaxGeneratorsServiceImpl.java | 182 +++++++++++++++ .../AbstractItemSyntaxGenerator.java | 178 +++++++++++++++ .../AbstractThingSyntaxGenerator.java | 208 ++++++++++++++++++ .../syntaxgenerator/ItemSyntaxGenerator.java | 42 ++++ .../SyntaxGeneratorsService.java | 66 ++++++ .../syntaxgenerator/ThingSyntaxGenerator.java | 43 ++++ .../discovery/InboxResourceOSGITest.java | 8 +- 19 files changed, 1308 insertions(+), 16 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/internal/syntaxgenerator/SyntaxGeneratorsServiceImpl.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/SyntaxGeneratorsService.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..f61601b30aa 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.Set; 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,6 +56,10 @@ 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.SyntaxGeneratorsService; +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; @@ -96,10 +112,18 @@ 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 SyntaxGeneratorsService syntaxGeneratorsService; @Activate - public InboxResource(final @Reference Inbox inbox) { + public InboxResource(final @Reference Inbox inbox, final @Reference ThingTypeRegistry thingTypeRegistry, + final @Reference ConfigDescriptionRegistry configDescRegistry, + final @Reference SyntaxGeneratorsService syntaxGeneratorsService) { this.inbox = inbox; + this.thingTypeRegistry = thingTypeRegistry; + this.configDescRegistry = configDescRegistry; + this.syntaxGeneratorsService = syntaxGeneratorsService; } @POST @@ -182,4 +206,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) { + if (!syntaxGeneratorsService.isGeneratorForThingAvailable(format)) { + 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(); + } + Configuration config = buildThingConfiguration(thingType, result.getProperties()); + Thing thing = ThingFactory.createThing(thingType, result.getThingUID(), config, result.getBridgeUID(), + configDescRegistry); + + return Response.ok(syntaxGeneratorsService.generateSyntaxForThings(format, Set.of(thing), false)).build(); + } + + private Configuration buildThingConfiguration(ThingType thingType, Map properties) { + Map configParams = new HashMap<>(); + for (ConfigDescriptionParameter param : getConfigDescriptionParameters(thingType)) { + Object value = properties.get(param.getName()); + if (value != null) { + configParams.put(param.getName(), ConfigUtil.normalizeType(value, param)); + } + } + return new Configuration(configParams); + } + + private List getConfigDescriptionParameters(ThingType thingType) { + URI descURI = thingType.getConfigDescriptionURI(); + if (descURI != null) { + ConfigDescription desc = configDescRegistry.getConfigDescription(descURI); + if (desc != null) { + return desc.getParameters(); + } + } + return List.of(); + } } 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..09e17b8c981 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 @@ -89,6 +89,7 @@ import org.openhab.core.library.types.UpDownType; import org.openhab.core.semantics.SemanticTagRegistry; import org.openhab.core.semantics.SemanticsPredicates; +import org.openhab.core.thing.syntaxgenerator.SyntaxGeneratorsService; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.core.types.TypeParser; @@ -183,6 +184,7 @@ private static void respectForwarded(final UriBuilder uriBuilder, final @Context private final MetadataSelectorMatcher metadataSelectorMatcher; private final SemanticTagRegistry semanticTagRegistry; private final TimeZoneProvider timeZoneProvider; + private final SyntaxGeneratorsService syntaxGeneratorsService; private final RegistryChangedRunnableListener resetLastModifiedItemChangeListener = new RegistryChangedRunnableListener<>( () -> lastModified = null); @@ -202,7 +204,8 @@ public ItemResource(// final @Reference MetadataRegistry metadataRegistry, final @Reference MetadataSelectorMatcher metadataSelectorMatcher, final @Reference SemanticTagRegistry semanticTagRegistry, - final @Reference TimeZoneProvider timeZoneProvider) { + final @Reference TimeZoneProvider timeZoneProvider, + final @Reference SyntaxGeneratorsService syntaxGeneratorsService) { this.dtoMapper = dtoMapper; this.eventPublisher = eventPublisher; this.itemBuilderFactory = itemBuilderFactory; @@ -213,6 +216,7 @@ public ItemResource(// this.metadataSelectorMatcher = metadataSelectorMatcher; this.semanticTagRegistry = semanticTagRegistry; this.timeZoneProvider = timeZoneProvider; + this.syntaxGeneratorsService = syntaxGeneratorsService; this.itemRegistry.addRegistryChangeListener(resetLastModifiedItemChangeListener); this.metadataRegistry.addRegistryChangeListener(resetLastModifiedMetadataChangeListener); @@ -901,6 +905,51 @@ 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) { + if (!syntaxGeneratorsService.isGeneratorForItemAvailable(format)) { + String message = "No syntax available for format " + format + "!"; + return Response.status(Response.Status.BAD_REQUEST).entity(message).build(); + } + return Response.ok(syntaxGeneratorsService.generateSyntaxForItems(format, itemRegistry.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) { + if (!syntaxGeneratorsService.isGeneratorForItemAvailable(format)) { + 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(); + } + + return Response.ok(syntaxGeneratorsService.generateSyntaxForItems(format, Set.of(item))).build(); + } + private JsonObject buildStatusObject(String itemName, String status, @Nullable String message) { JsonObject jo = new JsonObject(); jo.addProperty("name", itemName); 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..8d1bd8de2ae 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 @@ -95,6 +95,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.SyntaxGeneratorsService; import org.openhab.core.thing.type.BridgeType; import org.openhab.core.thing.type.ChannelType; import org.openhab.core.thing.type.ChannelTypeRegistry; @@ -171,6 +172,7 @@ public class ThingResource implements RESTResource { private final ThingTypeRegistry thingTypeRegistry; private final RegistryChangedRunnableListener resetLastModifiedChangeListener = new RegistryChangedRunnableListener<>( () -> lastModified = null); + private final SyntaxGeneratorsService syntaxGeneratorsService; private @Context @NonNullByDefault({}) UriInfo uriInfo; private @Nullable Date lastModified = null; @@ -192,7 +194,8 @@ public ThingResource( // final @Reference ThingManager thingManager, // final @Reference ThingRegistry thingRegistry, final @Reference ThingStatusInfoI18nLocalizationService thingStatusInfoI18nLocalizationService, - final @Reference ThingTypeRegistry thingTypeRegistry) { + final @Reference ThingTypeRegistry thingTypeRegistry, + final @Reference SyntaxGeneratorsService syntaxGeneratorsService) { this.dtoMapper = dtoMapper; this.channelTypeRegistry = channelTypeRegistry; this.configStatusService = configStatusService; @@ -206,6 +209,7 @@ public ThingResource( // this.thingRegistry = thingRegistry; this.thingStatusInfoI18nLocalizationService = thingStatusInfoI18nLocalizationService; this.thingTypeRegistry = thingTypeRegistry; + this.syntaxGeneratorsService = syntaxGeneratorsService; this.thingRegistry.addRegistryChangeListener(resetLastModifiedChangeListener); } @@ -720,6 +724,54 @@ 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("false") @QueryParam("preferPresentationAsTree") @Parameter(description = "prefer a presentation as a tree if supported by the generator") boolean preferPresentationAsTree) { + if (!syntaxGeneratorsService.isGeneratorForThingAvailable(format)) { + String message = "No syntax available for format " + format + "!"; + return Response.status(Response.Status.BAD_REQUEST).entity(message).build(); + } + return Response.ok(syntaxGeneratorsService.generateSyntaxForThings(format, 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) { + if (!syntaxGeneratorsService.isGeneratorForThingAvailable(format)) { + 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(syntaxGeneratorsService.generateSyntaxForThings(format, Set.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(), 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..abecca16c9e 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 @@ -92,4 +92,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..a5a7ee1674d 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; @@ -227,6 +228,23 @@ public void removeModelRepositoryChangeListener(ModelRepositoryChangeListener li listeners.remove(listener); } + @Override + public String generateSyntaxFromModelContent(String extension, EObject content) { + String result = ""; + 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..4475d3f71e0 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,15 +26,15 @@ 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.setIndentationIncrement.after(modelItemTypeRule) - c.setIndentationDecrement.before(modelItemTypeRule) - c.setIndentationIncrement.after(modelGroupItemRule) - c.setIndentationDecrement.before(modelGroupItemRule) + // c.setIndentationIncrement.after(modelItemTypeRule) + // c.setIndentationDecrement.before(modelItemTypeRule) + // c.setIndentationIncrement.after(modelGroupItemRule) + // c.setIndentationDecrement.before(modelGroupItemRule) c.autoLinewrap = 160 c.setLinewrap(0, 1, 2).before(SL_COMMENTRule) 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..f35bb78a045 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,9 +30,18 @@ import org.eclipse.xtext.formatting.impl.FormattingConfig */ class ThingFormatter extends AbstractDeclarativeFormatter { -// @Inject extension ThingGrammarAccess + @Inject extension ThingGrammarAccess override protected void configureFormatting(FormattingConfig c) { + c.setLinewrap(1, 1, 2).before(modelBridgeRule) + c.setLinewrap(1, 1, 2).before(modelThingRule) + c.setLinewrap(1, 1, 2).before(modelChannelRule) + + c.setIndentationIncrement.before(modelChannelRule) + c.setIndentationDecrement.after(modelChannelRule) + + c.autoLinewrap = 160 + // 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) 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..cee4bcaa38b --- /dev/null +++ b/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/syntaxgenerator/ItemDslSyntaxGenerator.java @@ -0,0 +1,172 @@ +/* + * 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.List; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +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.items.MetadataRegistry; +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.link.ItemChannelLinkRegistry; +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 Logger logger = LoggerFactory.getLogger(ItemDslSyntaxGenerator.class); + + private final ModelRepository modelRepository; + + @Activate + public ItemDslSyntaxGenerator(final @Reference ModelRepository modelRepository, + final @Reference MetadataRegistry metadataRegistry, + final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry) { + super(metadataRegistry, itemChannelLinkRegistry); + this.modelRepository = modelRepository; + } + + @Override + public String getFormat() { + return "DSL"; + } + + @Override + public synchronized String generateSyntax(List items) { + ItemModel model = ItemsFactory.eINSTANCE.createItemModel(); + for (Item item : items) { + model.getItems().add(buildModelItem(item)); + } + return modelRepository.generateSyntaxFromModelContent("items", model); + } + + private ModelItem buildModelItem(Item item) { + 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 : getChannelLinks(item)) { + 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 : getMetadata(item)) { + 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..3afe774137a --- /dev/null +++ b/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/syntaxgenerator/ThingDslSyntaxGenerator.java @@ -0,0 +1,164 @@ +/* + * 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 Logger logger = LoggerFactory.getLogger(ThingDslSyntaxGenerator.class); + + 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, 0, 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, int depth, 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 || depth == 0) { + 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, depth + 1, 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/internal/syntaxgenerator/SyntaxGeneratorsServiceImpl.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/syntaxgenerator/SyntaxGeneratorsServiceImpl.java new file mode 100644 index 00000000000..e017c837b27 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/syntaxgenerator/SyntaxGeneratorsServiceImpl.java @@ -0,0 +1,182 @@ +/* + * 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.internal.syntaxgenerator; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.items.GroupItem; +import org.openhab.core.items.Item; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.thing.syntaxgenerator.ItemSyntaxGenerator; +import org.openhab.core.thing.syntaxgenerator.SyntaxGeneratorsService; +import org.openhab.core.thing.syntaxgenerator.ThingSyntaxGenerator; +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; + +/** + * {@link SyntaxGeneratorsServiceImpl} is the service in charge of generating syntax for items and things. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = SyntaxGeneratorsService.class) +public class SyntaxGeneratorsServiceImpl implements SyntaxGeneratorsService { + + // private final Logger logger = LoggerFactory.getLogger(SyntaxGeneratorsServiceImpl.class); + + private final ThingRegistry thingRegistry; + private final ItemChannelLinkRegistry itemChannelLinkRegistry; + + private final Map itemSyntaxGenerators = new ConcurrentHashMap<>(); + private final Map thingSyntaxGenerators = new ConcurrentHashMap<>(); + + @Activate + public SyntaxGeneratorsServiceImpl(final @Reference ThingRegistry thingRegistry, + final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry) { + this.thingRegistry = thingRegistry; + this.itemChannelLinkRegistry = itemChannelLinkRegistry; + } + + @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()); + } + + @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()); + } + + @Override + public boolean isGeneratorForItemAvailable(String format) { + return itemSyntaxGenerators.get(format) != null; + } + + @Override + public @NonNull String generateSyntaxForItems(String format, Collection items) { + ItemSyntaxGenerator generator = itemSyntaxGenerators.get(format); + return generator == null ? "" : generator.generateSyntax(sortItems(items)); + } + + @Override + public boolean isGeneratorForThingAvailable(String format) { + return thingSyntaxGenerators.get(format) != null; + } + + @Override + public @NonNull String generateSyntaxForThings(String format, Collection things, + boolean preferPresentationAsTree) { + ThingSyntaxGenerator generator = thingSyntaxGenerators.get(format); + return generator == null ? "" : generator.generateSyntax(sortThings(things), preferPresentationAsTree); + } + + 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); + } + } + } + } + } + + /* + * 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.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..305ef8edcf2 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/AbstractItemSyntaxGenerator.java @@ -0,0 +1,178 @@ +/* + * 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.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.items.GroupItem; +import org.openhab.core.items.Item; +import org.openhab.core.items.Metadata; +import org.openhab.core.items.MetadataRegistry; +import org.openhab.core.library.CoreItemFactory; +import org.openhab.core.thing.link.ItemChannelLink; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; + +/** + * {@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 Logger logger = LoggerFactory.getLogger(AbstractItemSyntaxGenerator.class); + + private final MetadataRegistry metadataRegistry; + private final ItemChannelLinkRegistry itemChannelLinkRegistry; + + public AbstractItemSyntaxGenerator(MetadataRegistry metadataRegistry, + ItemChannelLinkRegistry itemChannelLinkRegistry) { + this.metadataRegistry = metadataRegistry; + this.itemChannelLinkRegistry = itemChannelLinkRegistry; + } + + /** + * Get the list of channel links defined for an item, sorted by natural order of their UID. + * + * @param item the item + * @return the sorted list of channel links for this item + */ + protected List getChannelLinks(Item item) { + return itemChannelLinkRegistry.getLinks(item.getName()).stream().sorted((link1, link2) -> { + return link1.getLinkedUID().getAsString().compareTo(link2.getLinkedUID().getAsString()); + }).collect(Collectors.toList()); + } + + /** + * Get the list of configuration parameters for a channel link, sorted by natural order of their names + * with the exception of parameter "profile" provided at first. + * + * @param channelLink the channel link + * @return a sorted list of configuration parameters for the channel link + */ + protected List getConfigurationParameters(ItemChannelLink channelLink) { + Map configParams = channelLink.getConfiguration().getProperties(); + List paramNames = configParams.keySet().stream().sorted((key1, key2) -> { + if ("profile".equals(key1)) { + return -1; + } else if ("profile".equals(key2)) { + return 1; + } else { + return key1.compareTo(key2); + } + }).collect(Collectors.toList()); + return getConfigurationParameters(paramNames, configParams); + } + + /** + * Get the list of metadata defined for an item, sorted by natural order of their namespaces. + * The "semantics" namespace is ignored. + * + * @param item the item + * @return the sorted list of metadata for this item + */ + protected List getMetadata(Item item) { + return metadataRegistry.getAll().stream().filter(md -> !"semantics".equals(md.getUID().getNamespace()) + && md.getUID().getItemName().equals(item.getName())).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) { + // Sort the config parameters + String namespace = metadata.getUID().getNamespace(); + Map configParams = metadata.getConfiguration(); + List paramNames = configParams.keySet().stream().sorted((key1, key2) -> { + if ("stateDescription".equals(namespace) && "min".equals(key1)) { + return -1; + } else if ("stateDescription".equals(namespace) && "min".equals(key2)) { + return 1; + } else if ("stateDescription".equals(namespace) && "max".equals(key1)) { + return -1; + } else if ("stateDescription".equals(namespace) && "max".equals(key2)) { + return 1; + } else if ("stateDescription".equals(namespace) && "step".equals(key1)) { + return -1; + } else if ("stateDescription".equals(namespace) && "step".equals(key2)) { + return 1; + } else { + return key1.compareTo(key2); + } + }).collect(Collectors.toList()); + return getConfigurationParameters(paramNames, configParams); + } + + private List getConfigurationParameters(List paramNames, + Map configParams) { + 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; + } + + /** + * {@link ConfigParameter} is a container for any configuration parameter defined by a name and a value. + */ + protected record ConfigParameter(String name, Object value) { + } +} 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..d1ad21aa4ec --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/AbstractThingSyntaxGenerator.java @@ -0,0 +1,208 @@ +/* + * 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 Logger logger = LoggerFactory.getLogger(AbstractThingSyntaxGenerator.class); + + 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; + } + + /** + * Get the child things of a bridge thing, ordered by UID. + * + * @param thing the thing + * @return the ordered list of child thing 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 a 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 a 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 configParamNames = configParameters.keySet(); + 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 : configParamNames.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 before channels + * with a channel type. Then the sort is done on the channel type and finally on the channel UID. + * + * @param thing the thing + * @return the ordered 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; + } + + /** + * {@link ConfigParameter} is a container for any configuration parameter defined by a name and a value. + */ + protected record ConfigParameter(String name, Object value) { + } +} 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..335876758f0 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/ItemSyntaxGenerator.java @@ -0,0 +1,42 @@ +/* + * 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.items.Item; + +/** + * {@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 + * @return the syntax for the items + */ + String generateSyntax(List items); +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/SyntaxGeneratorsService.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/SyntaxGeneratorsService.java new file mode 100644 index 00000000000..a873398cad5 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/syntaxgenerator/SyntaxGeneratorsService.java @@ -0,0 +1,66 @@ +/* + * 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 org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.items.Item; +import org.openhab.core.thing.Thing; + +/** + * {@link SyntaxGeneratorsService} is the interface for the service in charge of generating syntax + * for items and things. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public interface SyntaxGeneratorsService { + + /** + * Indicates if an item generator is available for a given syntax format. + * + * @param format the syntax format + * @return true if the generator is available for the provided syntax format; false if not + */ + boolean isGeneratorForItemAvailable(String format); + + /** + * Generates the syntax for a collection of items in the given syntax format. + * + * @param format the requested syntax format + * @param items the items + * @return the generated syntax for the items in the requested format or an empty string if this generator is not + * available + */ + String generateSyntaxForItems(String format, Collection items); + + /** + * Indicates if a thing generator is available for a given syntax format. + * + * @param format the syntax format + * @return true if the generator is available for the provided syntax format; false if not + */ + boolean isGeneratorForThingAvailable(String format); + + /** + * Generates the syntax for a collection of things in the given syntax format. + * + * @param format the requested syntax format + * @param things the things + * @param preferPresentationAsTree true if presentation as a tree is preferred (support by generators is optional) + * @return the generated syntax for the things in the requested format or an empty string if this generator is not + * available + */ + String generateSyntaxForThings(String format, Collection things, boolean preferPresentationAsTree); +} 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..438c75bbaa6 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,8 @@ 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.syntaxgenerator.SyntaxGeneratorsService; +import org.openhab.core.thing.type.ThingTypeRegistry; /** * @author Christoph Knauf - Initial contribution @@ -48,13 +50,17 @@ public class InboxResourceOSGITest extends JavaOSGiTest { private @NonNullByDefault({}) InboxResource resource; private @Mock @NonNullByDefault({}) Inbox inboxMock; + private @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistryMock; + private @Mock @NonNullByDefault({}) ConfigDescriptionRegistry configDescRegistryMock; + private @Mock @NonNullByDefault({}) SyntaxGeneratorsService syntaxGeneratorsServiceMock; @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, + syntaxGeneratorsServiceMock), InboxResource.class.getName()); resource = getService(InboxResource.class); assertNotNull(resource); }