From 16061881cc0d1840cbd47155d7268ac30738b25c Mon Sep 17 00:00:00 2001 From: Laurent Garnier Date: Sat, 27 May 2023 22:40:02 +0200 Subject: [PATCH] Add semantic tag registry + REST API to manage user tags Related to #3619 New registry for semantic tags. New default semantic tags provider for all built-in semantic tags. New managed provider to add/remove/update user semantic tags. Storage of user semantic tags in a JSON DB file. New REST API to add/remove/update user tags in the semantic model. New REST API to get a sub-tree of the semantic tags. Signed-off-by: Laurent Garnier --- .../internal/tag/EnrichedSemanticTagDTO.java | 41 ++ .../io/rest/core/internal/tag/TagDTO.java | 38 -- .../rest/core/internal/tag/TagResource.java | 219 ++++++- .../model/generateTagClasses.groovy | 90 ++- .../semantics/ManagedSemanticTagProvider.java | 63 ++ .../openhab/core/semantics/SemanticTag.java | 71 +++ .../core/semantics/SemanticTagImpl.java | 130 ++++ .../core/semantics/SemanticTagProvider.java | 26 + .../core/semantics/SemanticTagRegistry.java | 30 + .../openhab/core/semantics/SemanticTags.java | 144 +++-- .../core/semantics/dto/SemanticTagDTO.java | 31 + .../semantics/dto/SemanticTagDTOMapper.java | 60 ++ .../internal/SemanticTagRegistryImpl.java | 120 ++++ .../internal/SemanticsServiceImpl.java | 61 +- .../model/DefaultSemanticTagProvider.java | 565 ++++++++++++++++++ .../semantics/model/equipment/Equipments.java | 8 +- .../semantics/model/location/Locations.java | 8 +- .../core/semantics/model/point/Points.java | 8 +- .../semantics/model/property/Properties.java | 8 +- .../core/semantics/SemanticTagsTest.java | 57 -- .../internal/SemanticsServiceImplTest.java | 198 +++++- 21 files changed, 1769 insertions(+), 207 deletions(-) create mode 100644 bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/EnrichedSemanticTagDTO.java delete mode 100644 bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/TagDTO.java create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/ManagedSemanticTagProvider.java create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTag.java create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagImpl.java create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagProvider.java create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagRegistry.java create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/SemanticTagDTO.java create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/SemanticTagDTOMapper.java create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticTagRegistryImpl.java create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/DefaultSemanticTagProvider.java diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/EnrichedSemanticTagDTO.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/EnrichedSemanticTagDTO.java new file mode 100644 index 00000000000..f19d33f6802 --- /dev/null +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/EnrichedSemanticTagDTO.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2023 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.io.rest.core.internal.tag; + +import java.util.List; + +import org.openhab.core.semantics.SemanticTag; + +/** + * A DTO representing a {@link SemanticTag}. + * + * @author Jimmy Tanagra - initial contribution + * @author Laurent Garnier - Class renamed and members uid, description and editable added + */ +public class EnrichedSemanticTagDTO { + String uid; + String name; + String label; + String description; + List synonyms; + boolean editable; + + public EnrichedSemanticTagDTO(SemanticTag tag, boolean editable) { + this.uid = tag.getUID(); + this.name = tag.getUID().substring(tag.getUID().lastIndexOf("_") + 1); + this.label = tag.getLabel(); + this.description = tag.getDescription(); + this.synonyms = tag.getSynonyms(); + this.editable = editable; + } +} diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/TagDTO.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/TagDTO.java deleted file mode 100644 index 37d68f17cec..00000000000 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/TagDTO.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2010-2023 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.io.rest.core.internal.tag; - -import java.util.List; -import java.util.Locale; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.semantics.SemanticTags; -import org.openhab.core.semantics.Tag; - -/** - * A DTO representing a Semantic {@link Tag}. - * - * @author Jimmy Tanagra - initial contribution - */ -@NonNullByDefault -public class TagDTO { - String name; - String label; - List synonyms; - - public TagDTO(Class tag, Locale locale) { - this.name = tag.getSimpleName(); - this.label = SemanticTags.getLabel(tag, locale); - this.synonyms = SemanticTags.getSynonyms(tag, locale); - } -} diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/TagResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/TagResource.java index be55ca2d26c..91f9f0c842e 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/TagResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/TagResource.java @@ -14,12 +14,17 @@ import java.util.List; import java.util.Locale; -import java.util.Map; +import java.util.stream.Collectors; import javax.annotation.security.RolesAllowed; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; @@ -35,10 +40,13 @@ import org.openhab.core.io.rest.LocaleService; import org.openhab.core.io.rest.RESTConstants; import org.openhab.core.io.rest.RESTResource; -import org.openhab.core.semantics.model.equipment.Equipments; -import org.openhab.core.semantics.model.location.Locations; -import org.openhab.core.semantics.model.point.Points; -import org.openhab.core.semantics.model.property.Properties; +import org.openhab.core.semantics.ManagedSemanticTagProvider; +import org.openhab.core.semantics.SemanticTag; +import org.openhab.core.semantics.SemanticTagImpl; +import org.openhab.core.semantics.SemanticTagRegistry; +import org.openhab.core.semantics.SemanticTags; +import org.openhab.core.semantics.Tag; +import org.openhab.core.semantics.TagInfo; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @@ -47,6 +55,8 @@ import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect; import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsName; import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -54,11 +64,13 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; /** * This class acts as a REST resource for retrieving a list of tags. * * @author Jimmy Tanagra - Initial contribution + * @author Laurent Garnier - Extend REST API to allow adding/updating/removing a custom tag */ @Component @JaxrsResource @@ -73,29 +85,208 @@ public class TagResource implements RESTResource { /** The URI path to this resource */ public static final String PATH_TAGS = "tags"; + private final Logger logger = LoggerFactory.getLogger(TagResource.class); + private final LocaleService localeService; + private final SemanticTagRegistry semanticTagRegistry; + private final ManagedSemanticTagProvider managedSemanticTagProvider; + + // TODO pattern in @Path @Activate - public TagResource(final @Reference LocaleService localeService) { + public TagResource(final @Reference LocaleService localeService, + final @Reference SemanticTagRegistry semanticTagRegistry, + final @Reference ManagedSemanticTagProvider managedSemanticTagProvider) { this.localeService = localeService; + this.semanticTagRegistry = semanticTagRegistry; + this.managedSemanticTagProvider = managedSemanticTagProvider; } @GET @RolesAllowed({ Role.USER, Role.ADMIN }) @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getTags", summary = "Get all available tags.", responses = { - @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDTO.class)))) }) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = EnrichedSemanticTagDTO.class)))) }) public Response getTags(final @Context UriInfo uriInfo, final @Context HttpHeaders httpHeaders, @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language) { final Locale locale = localeService.getLocale(language); - Map> tags = Map.of( // - Locations.class.getSimpleName(), Locations.stream().map(tag -> new TagDTO(tag, locale)).toList(), // - Equipments.class.getSimpleName(), Equipments.stream().map(tag -> new TagDTO(tag, locale)).toList(), // - Points.class.getSimpleName(), Points.stream().map(tag -> new TagDTO(tag, locale)).toList(), // - Properties.class.getSimpleName(), Properties.stream().map(tag -> new TagDTO(tag, locale)).toList() // - ); + List tagsDTO = semanticTagRegistry.getAll().stream() + .sorted((element1, element2) -> element1.getUID().compareTo(element2.getUID())) + .map(t -> new EnrichedSemanticTagDTO(t.localized(locale), isEditable(t))).collect(Collectors.toList()); + return JSONResponse.createResponse(Status.OK, tagsDTO, null); + } + + @GET + @RolesAllowed({ Role.USER, Role.ADMIN }) + @Path("/{tagId}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(operationId = "getTagAndSubTags", summary = "Gets tag and sub tags.", responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = EnrichedSemanticTagDTO.class)))), + @ApiResponse(responseCode = "404", description = "Tag not found.") }) + public Response getTagAndSubTags( + @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, + @PathParam("tagId") @Parameter(description = "tag id") String tagId) { + final Locale locale = localeService.getLocale(language); + String uid = tagId.trim(); + + SemanticTag tag = semanticTagRegistry.get(uid); + if (tag != null) { + List tagsDTO = semanticTagRegistry.getSubTree(tag).stream() + .sorted((element1, element2) -> element1.getUID().compareTo(element2.getUID())) + .map(t -> new EnrichedSemanticTagDTO(t.localized(locale), isEditable(t))) + .collect(Collectors.toList()); + return JSONResponse.createResponse(Status.OK, tagsDTO, null); + } else { + return getTagResponse(Status.NOT_FOUND, null, locale, "Tag " + uid + " does not exist!"); + } + } + + @POST + @RolesAllowed({ Role.ADMIN }) + @Consumes(MediaType.APPLICATION_JSON) + @Operation(operationId = "createCustomTag", summary = "Creates a new custom tag and adds it to the registry.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "201", description = "Created", content = @Content(schema = @Schema(implementation = EnrichedSemanticTagDTO.class))), + @ApiResponse(responseCode = "400", description = "The tag identifier is invalid."), + @ApiResponse(responseCode = "409", description = "A tag with the same identifier already exists.") }) + public Response create( + @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, + @Parameter(description = "tag data", required = true) EnrichedSemanticTagDTO data) { + final Locale locale = localeService.getLocale(language); + + if (data.uid == null) { + return getTagResponse(Status.BAD_REQUEST, null, locale, "Tag identifier is required!"); + } + + String uid = data.uid.trim(); + + // check if a tag with this UID already exists + SemanticTag tag = semanticTagRegistry.get(uid); + if (tag != null) { + // report a conflict + return getTagResponse(Status.CONFLICT, tag, locale, "Tag " + uid + " already exists!"); + } + + // Extract the tag name and th eparent tag + // Check that the parent tag already exists + SemanticTag parentTag = null; + int lastSeparator = uid.lastIndexOf("_"); + if (lastSeparator <= 0) { + return getTagResponse(Status.BAD_REQUEST, null, locale, "Invalid tag identifier " + uid); + } + String name = uid.substring(lastSeparator + 1); + parentTag = semanticTagRegistry.get(uid.substring(0, lastSeparator)); + if (parentTag == null) { + return getTagResponse(Status.BAD_REQUEST, null, locale, + "No existing parent tag with id " + uid.substring(0, lastSeparator)); + } else if (!name.matches("[A-Z][a-zA-Z0-9]+")) { + return getTagResponse(Status.BAD_REQUEST, null, locale, "Invalid tag name " + name); + } + Class tagClass = SemanticTags.getById(name); + if (tagClass != null) { + // report a conflict + return getTagResponse(Status.CONFLICT, semanticTagRegistry.get(tagClass.getAnnotation(TagInfo.class).id()), + locale, "Tag " + tagClass.getAnnotation(TagInfo.class).id() + " already exists!"); + } + + uid = parentTag.getUID() + "_" + name; + tag = new SemanticTagImpl(uid, data.label, data.description, data.synonyms); + managedSemanticTagProvider.add(tag); + + return getTagResponse(Status.CREATED, tag, locale, null); + } + + @DELETE + @RolesAllowed({ Role.ADMIN }) + @Path("/{tagId}") + @Operation(operationId = "removeCustomTag", summary = "Removes a custom tag and its sub tags from the registry.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK, was deleted."), + @ApiResponse(responseCode = "404", description = "Custom tag not found."), + @ApiResponse(responseCode = "405", description = "Custom tag not editable.") }) + public Response remove( + @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, + @PathParam("tagId") @Parameter(description = "tag id") String tagId) { + final Locale locale = localeService.getLocale(language); + + String uid = tagId.trim(); + + // check whether tag exists and throw 404 if not + SemanticTag tag = semanticTagRegistry.get(uid); + if (tag == null) { + // Try to retrieve the tag from the tag class in case the provided id is partial + Class tagClass = SemanticTags.getById(uid); + tag = tagClass == null ? null : semanticTagRegistry.get(tagClass.getAnnotation(TagInfo.class).id()); + if (tag == null) { + return getTagResponse(Status.NOT_FOUND, null, locale, "Tag " + tagId + " does not exist!"); + } + } + + // Get tags in reverse order + List uids = semanticTagRegistry.getSubTree(tag).stream().map(t -> t.getUID()) + .sorted((element1, element2) -> element2.compareTo(element1)).collect(Collectors.toList()); + for (String id : uids) { + // ask whether the tag exists as a managed tag, so it can get updated, 405 otherwise + if (managedSemanticTagProvider.get(id) == null) { + return getTagResponse(Status.METHOD_NOT_ALLOWED, null, locale, "Tag " + id + " is not editable."); + } + } + + uids.stream().map(id -> managedSemanticTagProvider.remove(id)); + + return Response.ok(null, MediaType.TEXT_PLAIN).build(); + } + + @PUT + @RolesAllowed({ Role.ADMIN }) + @Path("/{tagId}") + @Consumes(MediaType.APPLICATION_JSON) + @Operation(operationId = "updateCustomTag", summary = "Updates a custom tag.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = EnrichedSemanticTagDTO.class))), + @ApiResponse(responseCode = "404", description = "Custom tag not found."), + @ApiResponse(responseCode = "405", description = "Custom tag not editable.") }) + public Response update( + @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, + @PathParam("tagId") @Parameter(description = "tag id") String tagId, + @Parameter(description = "tag data", required = true) EnrichedSemanticTagDTO data) { + final Locale locale = localeService.getLocale(language); + + String uid = tagId.trim(); + + // check whether tag exists and throw 404 if not + SemanticTag tag = semanticTagRegistry.get(uid); + if (tag == null) { + // Try to retrieve the tag from the tag class in case the provided id is partial + Class tagClass = SemanticTags.getById(uid); + tag = tagClass == null ? null : semanticTagRegistry.get(tagClass.getAnnotation(TagInfo.class).id()); + if (tag == null) { + return getTagResponse(Status.NOT_FOUND, null, locale, "Tag " + tagId + " does not exist!"); + } + } + + // ask whether the tag exists as a managed tag, so it can get updated, 405 otherwise + if (managedSemanticTagProvider.get(uid) == null) { + return getTagResponse(Status.METHOD_NOT_ALLOWED, null, locale, "Tag " + uid + " is not editable."); + } + + tag = new SemanticTagImpl(uid, data.label != null ? data.label : tag.getLabel(), + data.description != null ? data.description : tag.getDescription(), + data.synonyms != null ? data.synonyms : tag.getSynonyms()); + managedSemanticTagProvider.update(tag); + + return getTagResponse(Status.OK, tag, locale, null); + } + + private Response getTagResponse(Status status, @Nullable SemanticTag tag, Locale locale, + @Nullable String errorMsg) { + EnrichedSemanticTagDTO tagDTO = tag != null ? new EnrichedSemanticTagDTO(tag.localized(locale), isEditable(tag)) + : null; + return JSONResponse.createResponse(status, tagDTO, errorMsg); + } - return JSONResponse.createResponse(Status.OK, tags, null); + private boolean isEditable(SemanticTag tag) { + return managedSemanticTagProvider.get(tag.getUID()) != null; } } diff --git a/bundles/org.openhab.core.semantics/model/generateTagClasses.groovy b/bundles/org.openhab.core.semantics/model/generateTagClasses.groovy index 20f9cb58e79..3d676a6997a 100755 --- a/bundles/org.openhab.core.semantics/model/generateTagClasses.groovy +++ b/bundles/org.openhab.core.semantics/model/generateTagClasses.groovy @@ -54,6 +54,7 @@ createLocationsFile(locations) createEquipmentsFile(equipments) createPointsFile(points) createPropertiesFile(properties) +createDefaultProviderFile(tagSets) println "\n\nTagSets:" for (String tagSet : tagSets) { @@ -106,8 +107,8 @@ def createLocationsFile(Set locations) { file.write(header) file.write("""package org.openhab.core.semantics.model.location; -import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -121,7 +122,7 @@ import org.openhab.core.semantics.Location; @NonNullByDefault public class Locations { - static final Set> LOCATIONS = new HashSet<>(); + static final Set> LOCATIONS = ConcurrentHashMap.newKeySet(); static { LOCATIONS.add(Location.class); @@ -138,6 +139,10 @@ public class Locations { public static boolean add(Class tag) { return LOCATIONS.add(tag); } + + public static boolean remove(Class tag) { + return LOCATIONS.remove(tag); + } } """) file.close() @@ -148,8 +153,8 @@ def createEquipmentsFile(Set equipments) { file.write(header) file.write("""package org.openhab.core.semantics.model.equipment; -import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -163,7 +168,7 @@ import org.openhab.core.semantics.Equipment; @NonNullByDefault public class Equipments { - static final Set> EQUIPMENTS = new HashSet<>(); + static final Set> EQUIPMENTS = ConcurrentHashMap.newKeySet(); static { EQUIPMENTS.add(Equipment.class); @@ -180,6 +185,10 @@ public class Equipments { public static boolean add(Class tag) { return EQUIPMENTS.add(tag); } + + public static boolean remove(Class tag) { + return EQUIPMENTS.remove(tag); + } } """) file.close() @@ -190,8 +199,8 @@ def createPointsFile(Set points) { file.write(header) file.write("""package org.openhab.core.semantics.model.point; -import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -205,7 +214,7 @@ import org.openhab.core.semantics.Point; @NonNullByDefault public class Points { - static final Set> POINTS = new HashSet<>(); + static final Set> POINTS = ConcurrentHashMap.newKeySet(); static { POINTS.add(Point.class); @@ -222,6 +231,10 @@ public class Points { public static boolean add(Class tag) { return POINTS.add(tag); } + + public static boolean remove(Class tag) { + return POINTS.remove(tag); + } } """) file.close() @@ -232,8 +245,8 @@ def createPropertiesFile(Set properties) { file.write(header) file.write("""package org.openhab.core.semantics.model.property; -import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -247,7 +260,7 @@ import org.openhab.core.semantics.Property; @NonNullByDefault public class Properties { - static final Set> PROPERTIES = new HashSet<>(); + static final Set> PROPERTIES = ConcurrentHashMap.newKeySet(); static { PROPERTIES.add(Property.class); @@ -264,6 +277,67 @@ public class Properties { public static boolean add(Class tag) { return PROPERTIES.add(tag); } + + public static boolean remove(Class tag) { + return PROPERTIES.remove(tag); + } +} +""") + file.close() +} + +def createDefaultProviderFile(def tagSets) { + def file = new FileWriter("${baseDir}/src/main/java/org/openhab/core/semantics/model/DefaultSemanticTagProvider.java") + file.write(header) + file.write("""package org.openhab.core.semantics.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.ProviderChangeListener; +import org.openhab.core.semantics.SemanticTag; +import org.openhab.core.semantics.SemanticTagImpl; +import org.openhab.core.semantics.SemanticTagProvider; +import org.osgi.service.component.annotations.Component; + +/** + * This class defines a provider of all default semantic tags. + * + * @author Generated from generateTagClasses.groovy - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = { SemanticTagProvider.class, DefaultSemanticTagProvider.class }) +public class DefaultSemanticTagProvider implements SemanticTagProvider { + + private List defaultTags; + + public DefaultSemanticTagProvider() { + this.defaultTags = new ArrayList<>(); + defaultTags.add(new SemanticTagImpl("Location", "", "", List.of())); + defaultTags.add(new SemanticTagImpl("Equipment", "", "", List.of())); + defaultTags.add(new SemanticTagImpl("Point", "", "", List.of())); + defaultTags.add(new SemanticTagImpl("Property", "", "", List.of())); +""") + for (line in parseCsv(new FileReader("${baseDir}/model/SemanticTags.csv"), separator: ',')) { + def tagId = (line.Parent ? tagSets.get(line.Parent) : line.Type) + "_" + line.Tag + file.write(" defaultTags.add(new SemanticTagImpl(\"${tagId}\", //\n \"${line.Label}\", //\n \"${line.Description}\", //\n \"${line.Synonyms}\"));\n") + } + file.write(""" } + + @Override + public Collection getAll() { + return defaultTags; + } + + @Override + public void addProviderChangeListener(ProviderChangeListener listener) { + } + + @Override + public void removeProviderChangeListener(ProviderChangeListener listener) { + } } """) file.close() diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/ManagedSemanticTagProvider.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/ManagedSemanticTagProvider.java new file mode 100644 index 00000000000..3a926f705be --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/ManagedSemanticTagProvider.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2023 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.semantics; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.common.registry.AbstractManagedProvider; +import org.openhab.core.semantics.dto.SemanticTagDTO; +import org.openhab.core.semantics.dto.SemanticTagDTOMapper; +import org.openhab.core.storage.StorageService; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * {@link ManagedSemanticTagProvider} is an OSGi service, that allows to add or remove + * semantic tags at runtime by calling {@link ManagedSemanticTagProvider#add(SemanticTag)} + * or {@link ManagedSemanticTagProvider#remove(String)}. + * An added semantic tag is automatically exposed to the {@link SemanticTagRegistry}. + * Persistence of added semantic tags is handled by a {@link StorageService}. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = { SemanticTagProvider.class, ManagedSemanticTagProvider.class }) +public class ManagedSemanticTagProvider extends AbstractManagedProvider + implements SemanticTagProvider { + + @Activate + public ManagedSemanticTagProvider(final @Reference StorageService storageService) { + super(storageService); + } + + @Override + protected String getStorageName() { + return SemanticTag.class.getName(); + } + + @Override + protected String keyToString(String key) { + return key; + } + + @Override + protected @Nullable SemanticTag toElement(String uid, SemanticTagDTO persistedTag) { + return SemanticTagDTOMapper.map(persistedTag); + } + + @Override + protected SemanticTagDTO toPersistableElement(SemanticTag tag) { + return SemanticTagDTOMapper.map(tag); + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTag.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTag.java new file mode 100644 index 00000000000..d1782911521 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTag.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2023 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.semantics; + +import java.util.List; +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.Identifiable; + +/** + * This interface defines the core features of an openHAB semantic tag. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public interface SemanticTag extends Identifiable { + + /** + * Returns the name of the semantic tag. + * + * @return the name of the semantic tag + */ + String getName(); + + /** + * Returns the UID of the parent tag. + * + * @return the UID of the parent tag + */ + String getParentUID(); + + /** + * Returns the label of the semantic tag. + * + * @return semantic tag label or an empty string if undefined + */ + String getLabel(); + + /** + * Returns the description of the semantic tag. + * + * @return semantic tag description or an empty string if undefined + */ + String getDescription(); + + /** + * Returns the synonyms of the semantic tag. + * + * @return semantic tag synonyms as a List + */ + List getSynonyms(); + + /** + * Returns the localized semantic tag. + * + * @param locale the locale to be used + * @return the localized semantic tag + */ + SemanticTag localized(Locale locale); +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagImpl.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagImpl.java new file mode 100644 index 00000000000..183462fae58 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagImpl.java @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2010-2023 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.semantics; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.ResourceBundle.Control; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This is the main implementing class of the {@link SemanticTag} interface. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class SemanticTagImpl implements SemanticTag { + + private static final String TAGS_BUNDLE_NAME = "tags"; + + private String uid; + private String name; + private String parent; + private String label; + private String description; + private List synonyms; + + public SemanticTagImpl(String uid, @Nullable String label, @Nullable String description, + @Nullable List synonyms) { + this(uid, label, description); + if (synonyms != null) { + this.synonyms = synonyms; + } + } + + public SemanticTagImpl(String uid, @Nullable String label, @Nullable String description, + @Nullable String synonyms) { + this(uid, label, description); + if (synonyms != null && !synonyms.isBlank()) { + this.synonyms = new ArrayList<>(); + for (String synonym : synonyms.split(",")) { + this.synonyms.add(synonym.trim()); + } + } + } + + private SemanticTagImpl(String uid, @Nullable String label, @Nullable String description) { + this.uid = uid; + int idx = uid.lastIndexOf("_"); + if (idx < 0) { + this.name = uid.trim(); + this.parent = ""; + } else { + this.name = uid.substring(idx + 1).trim(); + this.parent = uid.substring(0, idx).trim(); + } + this.label = label == null ? "" : label.trim(); + this.description = description == null ? "" : description.trim(); + this.synonyms = List.of(); + } + + @Override + public String getUID() { + return uid; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getParentUID() { + return parent; + } + + @Override + public String getLabel() { + return label; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public List getSynonyms() { + return synonyms; + } + + @Override + public SemanticTag localized(Locale locale) { + ResourceBundle rb = ResourceBundle.getBundle(TAGS_BUNDLE_NAME, locale, + Control.getNoFallbackControl(Control.FORMAT_PROPERTIES)); + String label; + List synonyms; + try { + String entry = rb.getString(uid); + int idx = entry.indexOf(","); + if (idx >= 0) { + label = entry.substring(0, idx); + String synonymsCsv = entry.substring(idx + 1); + synonyms = synonymsCsv.isBlank() ? null : List.of(synonymsCsv.split(",")); + } else { + label = entry; + synonyms = null; + } + } catch (MissingResourceException e) { + label = getLabel(); + synonyms = getSynonyms(); + } + + return new SemanticTagImpl(uid, label, getDescription(), synonyms); + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagProvider.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagProvider.java new file mode 100644 index 00000000000..3d4c0a5a14f --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagProvider.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2023 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.semantics; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.Provider; + +/** + * The {@link SemanticTagProvider} is responsible for providing semantic tags. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public interface SemanticTagProvider extends Provider { + +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagRegistry.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagRegistry.java new file mode 100644 index 00000000000..92392e99824 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTagRegistry.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2023 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.semantics; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.Registry; + +/** + * {@link SemanticTagRegistry} tracks all {@link SemanticTag}s from different {@link SemanticTagProvider}s + * and provides access to them. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public interface SemanticTagRegistry extends Registry { + + public List getSubTree(SemanticTag tag); +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTags.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTags.java index 58982dab6ce..56ffa9fdcf7 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTags.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTags.java @@ -12,6 +12,7 @@ */ package org.openhab.core.semantics; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; @@ -51,7 +52,7 @@ public class SemanticTags { private static final String TAGS_BUNDLE_NAME = "tags"; - private static final Map> TAGS = new TreeMap<>(); + private static final Map> TAGS = Collections.synchronizedMap(new TreeMap<>()); private static final Logger LOGGER = LoggerFactory.getLogger(SemanticTags.class); private static final SemanticClassLoader CLASS_LOADER = new SemanticClassLoader(); @@ -131,7 +132,7 @@ public static List getSynonyms(Class tag, Locale locale) } catch (MissingResourceException e) { synonyms = tagInfo.synonyms(); } - return Stream.of(synonyms.split(",")).map(String::trim).toList(); + return synonyms.isEmpty() ? List.of() : Stream.of(synonyms.split(",")).map(String::trim).toList(); } public static String getDescription(Class tag, Locale locale) { @@ -236,64 +237,31 @@ public static String getDescription(Class tag, Locale locale) { /** * Adds a new semantic tag with inferred label, empty synonyms and description. - * + * * The label will be inferred from the tag name by splitting the CamelCase with a space. - * + * * @param name the tag name to add * @param parent the parent tag that the new tag should belong to * @return the created semantic tag class, or null if it was already added. */ public static @Nullable Class add(String name, String parent) { - return add(name, parent, null, null, null); - } - - /** - * Adds a new semantic tag. - * - * @param name the tag name to add - * @param parent the parent tag that the new tag should belong to - * @param label an optional label. When null, the label will be inferred from the tag name, - * splitting the CamelCase with a space. - * @param synonyms a comma separated list of synonyms - * @param description the tag description - * @return the created semantic tag class, or null if it was already added. - */ - public static @Nullable Class add(String name, String parent, @Nullable String label, - @Nullable String synonyms, @Nullable String description) { Class parentClass = getById(parent); if (parentClass == null) { LOGGER.warn("Adding semantic tag '{}' failed because parent tag '{}' is not found.", name, parent); return null; } - return add(name, parentClass, label, synonyms, description); - } - - /** - * Adds a new semantic tag with inferred label, empty synonyms and description. - * - * The label will be inferred from the tag name by splitting the CamelCase with a space. - * - * @param name the tag name to add - * @param parent the parent tag that the new tag should belong to - * @return the created semantic tag class, or null if it was already added. - */ - public static @Nullable Class add(String name, Class parent) { - return add(name, parent, null, null, null); + return add(name, parentClass); } /** * Adds a new semantic tag. - * + * * @param name the tag name to add * @param parent the parent tag that the new tag should belong to - * @param label an optional label. When null, the label will be inferred from the tag name, - * splitting the CamelCase with a space. - * @param synonyms a comma separated list of synonyms - * @param description the tag description * @return the created semantic tag class, or null if it was already added. */ - public static @Nullable Class add(String name, Class parent, @Nullable String label, - @Nullable String synonyms, @Nullable String description) { + public static @Nullable Class add(String name, Class parent) { + LOGGER.trace("Semantics add name \"{}\" parent id {}", name, parent.getAnnotation(TagInfo.class).id()); if (getById(name) != null) { return null; } @@ -306,45 +274,53 @@ public static String getDescription(Class tag, Locale locale) { String parentId = parent.getAnnotation(TagInfo.class).id(); String type = parentId.split("_")[0]; String className = "org.openhab.core.semantics.model." + type.toLowerCase() + "." + name; - - // Infer label from name, splitting up CamelCaseALL99 -> Camel Case ALL 99 - label = Optional.ofNullable(label).orElseGet(() -> name.replaceAll("([A-Z][a-z]+|[A-Z][A-Z]+|[0-9]+)", " $1")) - .trim(); - synonyms = Optional.ofNullable(synonyms).orElse("").replaceAll("\\s*,\\s*", ",").trim(); - - // Create the tag interface - ClassWriter classWriter = new ClassWriter(0); - classWriter.visit(Opcodes.V11, Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT + Opcodes.ACC_INTERFACE, - className.replace('.', '/'), null, "java/lang/Object", - new String[] { parent.getName().replace('.', '/') }); - - // Add TagInfo Annotation - classWriter.visitSource("Status.java", null); - - AnnotationVisitor annotation = classWriter.visitAnnotation("Lorg/openhab/core/semantics/TagInfo;", true); - annotation.visit("id", parentId + "_" + name); - annotation.visit("label", label); - annotation.visit("synonyms", synonyms); - annotation.visit("description", Optional.ofNullable(description).orElse("").trim()); - annotation.visitEnd(); - - classWriter.visitEnd(); - byte[] byteCode = classWriter.toByteArray(); - Class newTag = null; + Class newTag; try { - newTag = CLASS_LOADER.defineClass(className, byteCode); - } catch (Exception e) { - LOGGER.warn("Failed creating a new semantic tag '{}': {}", className, e.getMessage()); - return null; + newTag = (Class) Class.forName(className, false, CLASS_LOADER); + LOGGER.trace("Class '{}' exists", className); + } catch (ClassNotFoundException e) { + newTag = null; + } + + if (newTag == null) { + // Create the tag interface + ClassWriter classWriter = new ClassWriter(0); + classWriter.visit(Opcodes.V11, Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT + Opcodes.ACC_INTERFACE, + className.replace('.', '/'), null, "java/lang/Object", + new String[] { parent.getName().replace('.', '/') }); + + // Add TagInfo Annotation + classWriter.visitSource("Status.java", null); + + AnnotationVisitor annotation = classWriter.visitAnnotation("Lorg/openhab/core/semantics/TagInfo;", true); + annotation.visit("id", parentId + "_" + name); + annotation.visit("label", ""); + annotation.visit("synonyms", ""); + annotation.visit("description", ""); + annotation.visitEnd(); + + classWriter.visitEnd(); + byte[] byteCode = classWriter.toByteArray(); + try { + newTag = (Class) CLASS_LOADER.defineClass(className, byteCode); + } catch (Exception e) { + LOGGER.warn("Failed creating a new semantic tag '{}': {}", className, e.getMessage()); + return null; + } } + addToModel(newTag); addTagSet(newTag); - if (LOGGER.isTraceEnabled()) { - LOGGER.trace("'{}' semantic {} tag added.", className, type); - } + LOGGER.info("'{}' semantic {} tag added.", className, type); return newTag; } + public static void remove(Class tag) { + removeTagSet(tag); + removeFromModel(tag); + LOGGER.info("'{}' semantic tag removed.", tag.getName()); + } + private static void addTagSet(Class tagSet) { String id = tagSet.getAnnotation(TagInfo.class).id(); while (id.indexOf("_") != -1) { @@ -354,6 +330,15 @@ private static void addTagSet(Class tagSet) { TAGS.put(id, tagSet); } + private static void removeTagSet(Class tagSet) { + String id = tagSet.getAnnotation(TagInfo.class).id(); + while (id.indexOf("_") != -1) { + TAGS.remove(id, tagSet); + id = id.substring(id.indexOf("_") + 1); + } + TAGS.remove(id, tagSet); + } + private static boolean addToModel(Class tag) { if (Location.class.isAssignableFrom(tag)) { return Locations.add((Class) tag); @@ -367,6 +352,19 @@ private static boolean addToModel(Class tag) { throw new IllegalArgumentException("Unknown type of tag " + tag); } + private static boolean removeFromModel(Class tag) { + if (Location.class.isAssignableFrom(tag)) { + return Locations.remove((Class) tag); + } else if (Equipment.class.isAssignableFrom(tag)) { + return Equipments.remove((Class) tag); + } else if (Point.class.isAssignableFrom(tag)) { + return Points.remove((Class) tag); + } else if (Property.class.isAssignableFrom(tag)) { + return Properties.remove((Class) tag); + } + throw new IllegalArgumentException("Unknown type of tag " + tag); + } + private static class SemanticClassLoader extends ClassLoader { public SemanticClassLoader() { super(SemanticTags.class.getClassLoader()); diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/SemanticTagDTO.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/SemanticTagDTO.java new file mode 100644 index 00000000000..db00e10f968 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/SemanticTagDTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2023 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.semantics.dto; + +import java.util.List; + +/** + * This is a data transfer object that is used to serialize semantic tags. + * + * @author Laurent Garnier - Initial contribution + */ +public class SemanticTagDTO { + + public String uid; + public String label; + public String description; + public List synonyms; + + public SemanticTagDTO() { + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/SemanticTagDTOMapper.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/SemanticTagDTOMapper.java new file mode 100644 index 00000000000..0987bb6e1d4 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/SemanticTagDTOMapper.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010-2023 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.semantics.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.semantics.SemanticTag; +import org.openhab.core.semantics.SemanticTagImpl; + +/** + * The {@link SemanticTagDTOMapper} is an utility class to map semantic tags into + * semantic tag data transfer objects (DTOs). + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class SemanticTagDTOMapper { + + /** + * Maps semantic tag DTO into semantic tag object. + * + * @param tagDTO the DTO + * @return the semantic tag object + */ + public static @Nullable SemanticTag map(@Nullable SemanticTagDTO tagDTO) { + if (tagDTO == null) { + throw new IllegalArgumentException("The argument 'tagDTO' must not be null."); + } + if (tagDTO.uid == null) { + throw new IllegalArgumentException("The argument 'tagDTO.uid' must not be null."); + } + + return new SemanticTagImpl(tagDTO.uid, tagDTO.label, tagDTO.description, tagDTO.synonyms); + } + + /** + * Maps semantic tag object into semantic tag DTO. + * + * @param tag the semantic tag + * @return the semantic tag DTO + */ + public static SemanticTagDTO map(SemanticTag tag) { + SemanticTagDTO tagDTO = new SemanticTagDTO(); + tagDTO.uid = tag.getUID(); + tagDTO.label = tag.getLabel(); + tagDTO.description = tag.getDescription(); + tagDTO.synonyms = tag.getSynonyms(); + return tagDTO; + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticTagRegistryImpl.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticTagRegistryImpl.java new file mode 100644 index 00000000000..a272365f0fa --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticTagRegistryImpl.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2010-2023 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.semantics.internal; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.AbstractRegistry; +import org.openhab.core.semantics.ManagedSemanticTagProvider; +import org.openhab.core.semantics.SemanticTag; +import org.openhab.core.semantics.SemanticTagProvider; +import org.openhab.core.semantics.SemanticTagRegistry; +import org.openhab.core.semantics.SemanticTags; +import org.openhab.core.semantics.Tag; +import org.openhab.core.semantics.TagInfo; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is the main implementing class of the {@link SemanticTagRegistry} interface. It + * keeps track of all declared semantic tags of all semantic tags providers and keeps + * their current state in memory. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true) +public class SemanticTagRegistryImpl extends AbstractRegistry + implements SemanticTagRegistry { + + private final Logger logger = LoggerFactory.getLogger(SemanticTagRegistryImpl.class); + + private final ManagedSemanticTagProvider managedProvider; + + @Activate + public SemanticTagRegistryImpl(@Reference ManagedSemanticTagProvider managedProvider) { + super(SemanticTagProvider.class); + this.managedProvider = managedProvider; + super.setManagedProvider(managedProvider); + } + + @Override + @Deactivate + protected void deactivate() { + super.unsetManagedProvider(managedProvider); + super.deactivate(); + } + + @Override + public List getSubTree(SemanticTag tag) { + List ids = getAll().stream().map(t -> t.getUID()).filter(uid -> uid.startsWith(tag.getUID() + "_")) + .collect(Collectors.toList()); + List tags = new ArrayList<>(); + tags.add(tag); + ids.forEach(id -> { + SemanticTag t = get(id); + if (t != null) { + tags.add(t); + } + }); + return tags; + } + + @Override + protected void onAddElement(SemanticTag tag) throws IllegalArgumentException { + logger.trace("onAddElement {}", tag.getUID()); + super.onAddElement(tag); + String uid = tag.getUID(); + Class tagClass = SemanticTags.getById(uid); + if (tagClass != null) { + // Class already exists + return; + } + Class parentTagClass = null; + int lastSeparator = uid.lastIndexOf("_"); + if (lastSeparator <= 0) { + throw new IllegalArgumentException("Invalid tag id " + uid); + } + String name = uid.substring(lastSeparator + 1); + parentTagClass = SemanticTags.getById(uid.substring(0, lastSeparator)); + if (parentTagClass == null) { + throw new IllegalArgumentException("No existing parent tag with id " + uid.substring(0, lastSeparator)); + } else if (!name.matches("[A-Z][a-zA-Z0-9]+")) { + throw new IllegalArgumentException("Invalid tag name " + name); + } + tagClass = SemanticTags.getById(name); + if (tagClass != null) { + throw new IllegalArgumentException("Tag " + tagClass.getAnnotation(TagInfo.class).id() + " already exist"); + } + if (SemanticTags.add(name, parentTagClass) == null) { + throw new IllegalArgumentException("Failed to create semantic tag " + uid); + } + } + + @Override + protected void onRemoveElement(SemanticTag tag) { + logger.trace("onRemoveElement {}", tag.getUID()); + super.onRemoveElement(tag); + Class tagClass = SemanticTags.getById(tag.getUID()); + if (tagClass != null) { + SemanticTags.remove(tagClass); + } + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsServiceImpl.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsServiceImpl.java index 4ef13018d74..487feee576f 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsServiceImpl.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsServiceImpl.java @@ -12,14 +12,18 @@ */ package org.openhab.core.semantics.internal; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; 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.ItemPredicates; @@ -30,10 +34,13 @@ import org.openhab.core.semantics.Equipment; import org.openhab.core.semantics.Location; import org.openhab.core.semantics.Point; +import org.openhab.core.semantics.SemanticTag; +import org.openhab.core.semantics.SemanticTagRegistry; import org.openhab.core.semantics.SemanticTags; import org.openhab.core.semantics.SemanticsPredicates; import org.openhab.core.semantics.SemanticsService; import org.openhab.core.semantics.Tag; +import org.openhab.core.semantics.TagInfo; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @@ -51,12 +58,15 @@ public class SemanticsServiceImpl implements SemanticsService { private final ItemRegistry itemRegistry; private final MetadataRegistry metadataRegistry; + private final SemanticTagRegistry semanticTagRegistry; @Activate public SemanticsServiceImpl(final @Reference ItemRegistry itemRegistry, - final @Reference MetadataRegistry metadataRegistry) { + final @Reference MetadataRegistry metadataRegistry, + final @Reference SemanticTagRegistry semanticTagRegistry) { this.itemRegistry = itemRegistry; this.metadataRegistry = metadataRegistry; + this.semanticTagRegistry = semanticTagRegistry; } @Override @@ -77,7 +87,7 @@ public Set getItemsInLocation(Class locationType) { @Override public Set getItemsInLocation(String labelOrSynonym, Locale locale) { Set items = new HashSet<>(); - List> tagList = SemanticTags.getByLabelOrSynonym(labelOrSynonym, locale); + List> tagList = getByLabelOrSynonym(labelOrSynonym, locale); if (!tagList.isEmpty()) { for (Class tag : tagList) { if (Location.class.isAssignableFrom(tag)) { @@ -112,4 +122,51 @@ private Predicate hasSynonym(String labelOrSynonym) { return false; }; } + + public @Nullable Class getByLabel(String tagLabel, Locale locale) { + Optional tag = semanticTagRegistry.getAll().stream() + .filter(t -> t.localized(locale).getLabel().equalsIgnoreCase(tagLabel)).findFirst(); + return tag.isPresent() ? SemanticTags.getById(tag.get().getUID()) : null; + } + + public List> getByLabelOrSynonym(String tagLabelOrSynonym, Locale locale) { + List tags = semanticTagRegistry.getAll().stream() + .filter(t -> getLabelAndSynonyms(t, locale).contains(tagLabelOrSynonym.toLowerCase(locale))) + .collect(Collectors.toList()); + List> tagList = new ArrayList<>(); + tags.forEach(t -> { + Class tag = SemanticTags.getById(t.getUID()); + if (tag != null) { + tagList.add(tag); + } + }); + return tagList; + } + + public List getLabelAndSynonyms(Class tagClass, Locale locale) { + SemanticTag tag = semanticTagRegistry.get(tagClass.getAnnotation(TagInfo.class).id()); + return tag == null ? List.of() : getLabelAndSynonyms(tag, locale); + } + + private List getLabelAndSynonyms(SemanticTag tag, Locale locale) { + SemanticTag localizedTag = tag.localized(locale); + Stream label = Stream.of(localizedTag.getLabel()); + Stream synonyms = localizedTag.getSynonyms().stream(); + return Stream.concat(label, synonyms).map(s -> s.toLowerCase(locale)).distinct().toList(); + } + + public String getLabel(Class tagClass, Locale locale) { + SemanticTag tag = semanticTagRegistry.get(tagClass.getAnnotation(TagInfo.class).id()); + return tag == null ? "" : tag.localized(locale).getLabel(); + } + + public List getSynonyms(Class tagClass, Locale locale) { + SemanticTag tag = semanticTagRegistry.get(tagClass.getAnnotation(TagInfo.class).id()); + return tag == null ? List.of() : tag.localized(locale).getSynonyms(); + } + + public String getDescription(Class tagClass, Locale locale) { + SemanticTag tag = semanticTagRegistry.get(tagClass.getAnnotation(TagInfo.class).id()); + return tag == null ? "" : tag.localized(locale).getDescription(); + } } diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/DefaultSemanticTagProvider.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/DefaultSemanticTagProvider.java new file mode 100644 index 00000000000..82945ef5aa0 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/DefaultSemanticTagProvider.java @@ -0,0 +1,565 @@ +/** + * Copyright (c) 2010-2023 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.semantics.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.ProviderChangeListener; +import org.openhab.core.semantics.SemanticTag; +import org.openhab.core.semantics.SemanticTagImpl; +import org.openhab.core.semantics.SemanticTagProvider; +import org.osgi.service.component.annotations.Component; + +/** + * This class defines a provider of all default semantic tags. + * + * @author Generated from generateTagClasses.groovy - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = { SemanticTagProvider.class, DefaultSemanticTagProvider.class }) +public class DefaultSemanticTagProvider implements SemanticTagProvider { + + private List defaultTags; + + public DefaultSemanticTagProvider() { + this.defaultTags = new ArrayList<>(); + defaultTags.add(new SemanticTagImpl("Location", "", "", List.of())); + defaultTags.add(new SemanticTagImpl("Equipment", "", "", List.of())); + defaultTags.add(new SemanticTagImpl("Point", "", "", List.of())); + defaultTags.add(new SemanticTagImpl("Property", "", "", List.of())); + defaultTags.add(new SemanticTagImpl("Location_Indoor", // + "Indoor", // + "Anything that is inside a closed building", // + "")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Apartment", // + "Apartment", // + "", // + "Apartments")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Building", // + "Building", // + "", // + "Buildings")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Building_Garage", // + "Garage", // + "", // + "Garages")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Building_House", // + "House", // + "", // + "Houses")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Building_Shed", // + "Shed", // + "", // + "Sheds")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Building_SummerHouse", // + "Summer House", // + "", // + "Summer Houses, Second Home, Second Homes")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Floor", // + "Floor", // + "", // + "Floors")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Floor_GroundFloor", // + "Ground Floor", // + "", // + "Ground Floors, Downstairs")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Floor_FirstFloor", // + "First Floor", // + "", // + "First Floors, Upstairs")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Floor_SecondFloor", // + "Second Floor", // + "", // + "Second Floors")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Floor_ThirdFloor", // + "Third Floor", // + "", // + "Third Floors")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Floor_Attic", // + "Attic", // + "", // + "Attics")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Floor_Basement", // + "Basement", // + "", // + "Basements")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Corridor", // + "Corridor", // + "", // + "Corridors, Hallway, Hallways")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room", // + "Room", // + "", // + "Rooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_Bathroom", // + "Bathroom", // + "", // + "Bathrooms, Bath, Baths, Powder Room, Powder Rooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_Bedroom", // + "Bedroom", // + "", // + "Bedrooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_BoilerRoom", // + "Boiler Room", // + "", // + "Boiler Rooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_Cellar", // + "Cellar", // + "", // + "Cellars")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_DiningRoom", // + "Dining Room", // + "", // + "Dining Rooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_Entry", // + "Entry", // + "", // + "Entries, Foyer, Foyers")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_FamilyRoom", // + "Family Room", // + "", // + "Family Rooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_GuestRoom", // + "Guest Room", // + "", // + "Guest Rooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_Kitchen", // + "Kitchen", // + "", // + "Kitchens")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_LaundryRoom", // + "Laundry Room", // + "", // + "Laundry Rooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_LivingRoom", // + "Living Room", // + "", // + "Living Rooms")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_Office", // + "Office", // + "", // + "Offices")); + defaultTags.add(new SemanticTagImpl("Location_Indoor_Room_Veranda", // + "Veranda", // + "", // + "Verandas")); + defaultTags.add(new SemanticTagImpl("Location_Outdoor", // + "Outdoor", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Location_Outdoor_Carport", // + "Carport", // + "", // + "Carports")); + defaultTags.add(new SemanticTagImpl("Location_Outdoor_Driveway", // + "Driveway", // + "", // + "Driveways")); + defaultTags.add(new SemanticTagImpl("Location_Outdoor_Garden", // + "Garden", // + "", // + "Gardens")); + defaultTags.add(new SemanticTagImpl("Location_Outdoor_Patio", // + "Patio", // + "", // + "Patios")); + defaultTags.add(new SemanticTagImpl("Location_Outdoor_Porch", // + "Porch", // + "", // + "Porches")); + defaultTags.add(new SemanticTagImpl("Location_Outdoor_Terrace", // + "Terrace", // + "", // + "Terraces, Deck, Decks")); + defaultTags.add(new SemanticTagImpl("Property_Temperature", // + "Temperature", // + "", // + "Temperatures")); + defaultTags.add(new SemanticTagImpl("Property_Light", // + "Light", // + "", // + "Lights, Lighting")); + defaultTags.add(new SemanticTagImpl("Property_ColorTemperature", // + "Color Temperature", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Humidity", // + "Humidity", // + "", // + "Moisture")); + defaultTags.add(new SemanticTagImpl("Property_Presence", // + "Presence", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Pressure", // + "Pressure", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Smoke", // + "Smoke", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Noise", // + "Noise", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Rain", // + "Rain", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Wind", // + "Wind", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Water", // + "Water", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_CO2", // + "CO2", // + "", // + "Carbon Dioxide")); + defaultTags.add(new SemanticTagImpl("Property_CO", // + "CO", // + "", // + "Carbon Monoxide")); + defaultTags.add(new SemanticTagImpl("Property_Energy", // + "Energy", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Power", // + "Power", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Voltage", // + "Voltage", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Current", // + "Current", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Frequency", // + "Frequency", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Gas", // + "Gas", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_SoundVolume", // + "Sound Volume", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Oil", // + "Oil", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Duration", // + "Duration", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Level", // + "Level", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Opening", // + "Opening", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Timestamp", // + "Timestamp", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Property_Ultraviolet", // + "Ultraviolet", // + "", // + "UV")); + defaultTags.add(new SemanticTagImpl("Property_Vibration", // + "Vibration", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Point_Alarm", // + "Alarm", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Point_Control", // + "Control", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Point_Control_Switch", // + "Switch", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Point_Measurement", // + "Measurement", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Point_Setpoint", // + "Setpoint", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Point_Status", // + "Status", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Point_Status_LowBattery", // + "LowBattery", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Point_Status_OpenLevel", // + "OpenLevel", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Point_Status_OpenState", // + "OpenState", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Point_Status_Tampered", // + "Tampered", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Point_Status_Tilt", // + "Tilt", // + "", // + "")); + defaultTags.add(new SemanticTagImpl("Equipment_AlarmSystem", // + "Alarm System", // + "", // + "Alarm Systems")); + defaultTags.add(new SemanticTagImpl("Equipment_Battery", // + "Battery", // + "", // + "Batteries")); + defaultTags.add(new SemanticTagImpl("Equipment_Blinds", // + "Blinds", // + "", // + "Rollershutter, Rollershutters, Roller shutter, Roller shutters, Shutter, Shutters")); + defaultTags.add(new SemanticTagImpl("Equipment_Boiler", // + "Boiler", // + "", // + "Boilers")); + defaultTags.add(new SemanticTagImpl("Equipment_Camera", // + "Camera", // + "", // + "Cameras")); + defaultTags.add(new SemanticTagImpl("Equipment_Car", // + "Car", // + "", // + "Cars")); + defaultTags.add(new SemanticTagImpl("Equipment_CleaningRobot", // + "Cleaning Robot", // + "", // + "Cleaning Robots, Vacuum robot, Vacuum robots")); + defaultTags.add(new SemanticTagImpl("Equipment_Door", // + "Door", // + "", // + "Doors")); + defaultTags.add(new SemanticTagImpl("Equipment_Door_BackDoor", // + "Back Door", // + "", // + "Back Doors")); + defaultTags.add(new SemanticTagImpl("Equipment_Door_CellarDoor", // + "Cellar Door", // + "", // + "Cellar Doors")); + defaultTags.add(new SemanticTagImpl("Equipment_Door_FrontDoor", // + "Front Door", // + "", // + "Front Doors, Frontdoor, Frontdoors")); + defaultTags.add(new SemanticTagImpl("Equipment_Door_GarageDoor", // + "Garage Door", // + "", // + "Garage Doors")); + defaultTags.add(new SemanticTagImpl("Equipment_Door_Gate", // + "Gate", // + "", // + "Gates")); + defaultTags.add(new SemanticTagImpl("Equipment_Door_InnerDoor", // + "Inner Door", // + "", // + "Inner Doors")); + defaultTags.add(new SemanticTagImpl("Equipment_Door_SideDoor", // + "Side Door", // + "", // + "Side Doors")); + defaultTags.add(new SemanticTagImpl("Equipment_Doorbell", // + "Doorbell", // + "", // + "Doorbells")); + defaultTags.add(new SemanticTagImpl("Equipment_Fan", // + "Fan", // + "", // + "Fans")); + defaultTags.add(new SemanticTagImpl("Equipment_Fan_CeilingFan", // + "Ceiling Fan", // + "", // + "Ceiling Fans")); + defaultTags.add(new SemanticTagImpl("Equipment_Fan_KitchenHood", // + "Kitchen Hood", // + "", // + "Kitchen Hoods")); + defaultTags.add(new SemanticTagImpl("Equipment_HVAC", // + "HVAC", // + "", // + "Heating, Ventilation, Air Conditioning, A/C, A/Cs, AC")); + defaultTags.add(new SemanticTagImpl("Equipment_Inverter", // + "Inverter", // + "", // + "Inverters")); + defaultTags.add(new SemanticTagImpl("Equipment_LawnMower", // + "Lawn Mower", // + "", // + "Lawn Mowers")); + defaultTags.add(new SemanticTagImpl("Equipment_Lightbulb", // + "Lightbulb", // + "", // + "Lightbulbs, Bulb, Bulbs, Lamp, Lamps, Lights, Lighting")); + defaultTags.add(new SemanticTagImpl("Equipment_Lightbulb_LightStripe", // + "Light Stripe", // + "", // + "Light Stripes")); + defaultTags.add(new SemanticTagImpl("Equipment_Lock", // + "Lock", // + "", // + "Locks")); + defaultTags.add(new SemanticTagImpl("Equipment_NetworkAppliance", // + "Network Appliance", // + "", // + "Network Appliances")); + defaultTags.add(new SemanticTagImpl("Equipment_PowerOutlet", // + "Power Outlet", // + "", // + "Power Outlets, Outlet, Outlets")); + defaultTags.add(new SemanticTagImpl("Equipment_Projector", // + "Projector", // + "", // + "Projectors, Beamer, Beamers")); + defaultTags.add(new SemanticTagImpl("Equipment_Pump", // + "Pump", // + "", // + "Pumps")); + defaultTags.add(new SemanticTagImpl("Equipment_RadiatorControl", // + "Radiator Control", // + "", // + "Radiator Controls, Radiator, Radiators")); + defaultTags.add(new SemanticTagImpl("Equipment_Receiver", // + "Receiver", // + "", // + "Receivers, Audio Receiver, Audio Receivers, AV Receiver, AV Receivers")); + defaultTags.add(new SemanticTagImpl("Equipment_RemoteControl", // + "Remote Control", // + "", // + "Remote Controls")); + defaultTags.add(new SemanticTagImpl("Equipment_Screen", // + "Screen", // + "", // + "Screens")); + defaultTags.add(new SemanticTagImpl("Equipment_Screen_Television", // + "Television", // + "", // + "Televisions, TV, TVs")); + defaultTags.add(new SemanticTagImpl("Equipment_Sensor", // + "Sensor", // + "", // + "Sensors")); + defaultTags.add(new SemanticTagImpl("Equipment_Sensor_MotionDetector", // + "Motion Detector", // + "", // + "Motion Detectors, Motion sensor, Motion sensors")); + defaultTags.add(new SemanticTagImpl("Equipment_Sensor_SmokeDetector", // + "Smoke Detector", // + "", // + "Smoke Detectors")); + defaultTags.add(new SemanticTagImpl("Equipment_Siren", // + "Siren", // + "", // + "Sirens")); + defaultTags.add(new SemanticTagImpl("Equipment_Smartphone", // + "Smartphone", // + "", // + "Smartphones, Phone, Phones")); + defaultTags.add(new SemanticTagImpl("Equipment_Speaker", // + "Speaker", // + "", // + "Speakers")); + defaultTags.add(new SemanticTagImpl("Equipment_Valve", // + "Valve", // + "", // + "Valves")); + defaultTags.add(new SemanticTagImpl("Equipment_VoiceAssistant", // + "Voice Assistant", // + "", // + "Voice Assistants")); + defaultTags.add(new SemanticTagImpl("Equipment_WallSwitch", // + "Wall Switch", // + "", // + "Wall Switches")); + defaultTags.add(new SemanticTagImpl("Equipment_WebService", // + "Web Service", // + "", // + "Web Services")); + defaultTags.add(new SemanticTagImpl("Equipment_WebService_WeatherService", // + "Weather Service", // + "", // + "Weather Services")); + defaultTags.add(new SemanticTagImpl("Equipment_WhiteGood", // + "White Good", // + "", // + "White Goods")); + defaultTags.add(new SemanticTagImpl("Equipment_WhiteGood_Dishwasher", // + "Dishwasher", // + "", // + "Dishwashers")); + defaultTags.add(new SemanticTagImpl("Equipment_WhiteGood_Dryer", // + "Dryer", // + "", // + "Dryers")); + defaultTags.add(new SemanticTagImpl("Equipment_WhiteGood_Freezer", // + "Freezer", // + "", // + "Freezers")); + defaultTags.add(new SemanticTagImpl("Equipment_WhiteGood_Oven", // + "Oven", // + "", // + "Ovens")); + defaultTags.add(new SemanticTagImpl("Equipment_WhiteGood_Refrigerator", // + "Refrigerator", // + "", // + "Refrigerators")); + defaultTags.add(new SemanticTagImpl("Equipment_WhiteGood_WashingMachine", // + "Washing Machine", // + "", // + "Washing Machines")); + defaultTags.add(new SemanticTagImpl("Equipment_Window", // + "Window", // + "", // + "Windows")); + } + + @Override + public Collection getAll() { + return defaultTags; + } + + @Override + public void addProviderChangeListener(ProviderChangeListener listener) { + } + + @Override + public void removeProviderChangeListener(ProviderChangeListener listener) { + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Equipments.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Equipments.java index d99172b88c6..63534fc5115 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Equipments.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Equipments.java @@ -12,8 +12,8 @@ */ package org.openhab.core.semantics.model.equipment; -import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -27,7 +27,7 @@ @NonNullByDefault public class Equipments { - static final Set> EQUIPMENTS = new HashSet<>(); + static final Set> EQUIPMENTS = ConcurrentHashMap.newKeySet(); static { EQUIPMENTS.add(Equipment.class); @@ -93,4 +93,8 @@ public static Stream> stream() { public static boolean add(Class tag) { return EQUIPMENTS.add(tag); } + + public static boolean remove(Class tag) { + return EQUIPMENTS.remove(tag); + } } diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Locations.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Locations.java index 6c5063dfb1d..35defc9bc76 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Locations.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Locations.java @@ -12,8 +12,8 @@ */ package org.openhab.core.semantics.model.location; -import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -27,7 +27,7 @@ @NonNullByDefault public class Locations { - static final Set> LOCATIONS = new HashSet<>(); + static final Set> LOCATIONS = ConcurrentHashMap.newKeySet(); static { LOCATIONS.add(Location.class); @@ -76,4 +76,8 @@ public static Stream> stream() { public static boolean add(Class tag) { return LOCATIONS.add(tag); } + + public static boolean remove(Class tag) { + return LOCATIONS.remove(tag); + } } diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Points.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Points.java index d7950695f1e..5224eacd151 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Points.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Points.java @@ -12,8 +12,8 @@ */ package org.openhab.core.semantics.model.point; -import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -27,7 +27,7 @@ @NonNullByDefault public class Points { - static final Set> POINTS = new HashSet<>(); + static final Set> POINTS = ConcurrentHashMap.newKeySet(); static { POINTS.add(Point.class); @@ -51,4 +51,8 @@ public static Stream> stream() { public static boolean add(Class tag) { return POINTS.add(tag); } + + public static boolean remove(Class tag) { + return POINTS.remove(tag); + } } diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Properties.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Properties.java index 06de5ebba85..dc65a495028 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Properties.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Properties.java @@ -12,8 +12,8 @@ */ package org.openhab.core.semantics.model.property; -import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -27,7 +27,7 @@ @NonNullByDefault public class Properties { - static final Set> PROPERTIES = new HashSet<>(); + static final Set> PROPERTIES = ConcurrentHashMap.newKeySet(); static { PROPERTIES.add(Property.class); @@ -67,4 +67,8 @@ public static Stream> stream() { public static boolean add(Class tag) { return PROPERTIES.add(tag); } + + public static boolean remove(Class tag) { + return PROPERTIES.remove(tag); + } } diff --git a/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/SemanticTagsTest.java b/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/SemanticTagsTest.java index 56043f29e34..c532aded7c3 100644 --- a/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/SemanticTagsTest.java +++ b/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/SemanticTagsTest.java @@ -12,13 +12,9 @@ */ package org.openhab.core.semantics; -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; -import java.util.Locale; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -29,14 +25,11 @@ import org.openhab.core.semantics.model.equipment.CleaningRobot; import org.openhab.core.semantics.model.equipment.Equipments; import org.openhab.core.semantics.model.location.Bathroom; -import org.openhab.core.semantics.model.location.Kitchen; import org.openhab.core.semantics.model.location.Locations; import org.openhab.core.semantics.model.location.Room; import org.openhab.core.semantics.model.point.Measurement; import org.openhab.core.semantics.model.point.Points; -import org.openhab.core.semantics.model.property.Light; import org.openhab.core.semantics.model.property.Properties; -import org.openhab.core.semantics.model.property.SoundVolume; import org.openhab.core.semantics.model.property.Temperature; /** @@ -75,37 +68,6 @@ public void testByTagId() { assertEquals(Bathroom.class, SemanticTags.getById("Location_Indoor_Room_Bathroom")); } - @Test - public void testByLabel() { - assertEquals(Kitchen.class, SemanticTags.getByLabel("Kitchen", Locale.ENGLISH)); - assertEquals(Kitchen.class, SemanticTags.getByLabel("Küche", Locale.GERMAN)); - assertNull(SemanticTags.getByLabel("Bad", Locale.GERMAN)); - } - - @Test - public void testByLabelOrSynonym() { - assertEquals(Kitchen.class, SemanticTags.getByLabelOrSynonym("Kitchen", Locale.ENGLISH).iterator().next()); - assertEquals(Kitchen.class, SemanticTags.getByLabelOrSynonym("Küche", Locale.GERMAN).iterator().next()); - assertEquals(Bathroom.class, SemanticTags.getByLabelOrSynonym("Badezimmer", Locale.GERMAN).iterator().next()); - } - - @Test - public void testGetLabel() { - assertEquals("Kitchen", SemanticTags.getLabel(Kitchen.class, Locale.ENGLISH)); - assertEquals("Sound Volume", SemanticTags.getLabel(SoundVolume.class, Locale.ENGLISH)); - } - - @Test - public void testGetSynonyms() { - assertThat(SemanticTags.getSynonyms(Light.class, Locale.ENGLISH), hasItems("Lights", "Lighting")); - } - - @Test - public void testGetDescription() { - Class tag = SemanticTags.add("TestDesc", Light.class, null, null, "Test Description"); - assertEquals("Test Description", SemanticTags.getDescription(tag, Locale.ENGLISH)); - } - @Test public void testGetSemanticType() { assertEquals(Bathroom.class, SemanticTags.getSemanticType(locationItem)); @@ -139,7 +101,6 @@ public void testAddLocation() { Class customTag = SemanticTags.add(tagName, Location.class); assertNotNull(customTag); assertEquals(customTag, SemanticTags.getById(tagName)); - assertEquals(customTag, SemanticTags.getByLabel("Custom Location", Locale.getDefault())); assertTrue(Locations.stream().toList().contains(customTag)); GroupItem myItem = new GroupItem("MyLocation"); @@ -162,7 +123,6 @@ public void testAddEquipment() { Class customTag = SemanticTags.add(tagName, Equipment.class); assertNotNull(customTag); assertEquals(customTag, SemanticTags.getById(tagName)); - assertEquals(customTag, SemanticTags.getByLabel("Custom Equipment", Locale.getDefault())); assertTrue(Equipments.stream().toList().contains(customTag)); GroupItem myItem = new GroupItem("MyEquipment"); @@ -185,7 +145,6 @@ public void testAddPoint() { Class customTag = SemanticTags.add(tagName, Point.class); assertNotNull(customTag); assertEquals(customTag, SemanticTags.getById(tagName)); - assertEquals(customTag, SemanticTags.getByLabel("Custom Point", Locale.getDefault())); assertTrue(Points.stream().toList().contains(customTag)); GroupItem myItem = new GroupItem("MyItem"); @@ -208,7 +167,6 @@ public void testAddProperty() { Class customTag = SemanticTags.add(tagName, Property.class); assertNotNull(customTag); assertEquals(customTag, SemanticTags.getById(tagName)); - assertEquals(customTag, SemanticTags.getByLabel("Custom Property", Locale.getDefault())); assertTrue(Properties.stream().toList().contains(customTag)); GroupItem myItem = new GroupItem("MyItem"); @@ -232,19 +190,4 @@ public void testAddingExistingTagShouldFail() { assertNotNull(SemanticTags.add("CustomLocation1", Location.class)); assertNull(SemanticTags.add("CustomLocation1", Location.class)); } - - @Test - public void testAddWithCustomLabel() { - Class tag = SemanticTags.add("CustomProperty2", Property.class, " Custom Label ", null, null); - assertEquals(tag, SemanticTags.getByLabel("Custom Label", Locale.getDefault())); - } - - @Test - public void testAddWithSynonyms() { - String synonyms = " Synonym1, Synonym2 , Synonym With Space "; - Class tag = SemanticTags.add("CustomProperty3", Property.class, null, synonyms, null); - assertEquals(tag, SemanticTags.getByLabelOrSynonym("Synonym1", Locale.getDefault()).get(0)); - assertEquals(tag, SemanticTags.getByLabelOrSynonym("Synonym2", Locale.getDefault()).get(0)); - assertEquals(tag, SemanticTags.getByLabelOrSynonym("Synonym With Space", Locale.getDefault()).get(0)); - } } diff --git a/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticsServiceImplTest.java b/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticsServiceImplTest.java index 3ff77cb163d..087ef12d64b 100644 --- a/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticsServiceImplTest.java +++ b/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticsServiceImplTest.java @@ -12,9 +12,11 @@ */ package org.openhab.core.semantics.internal; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import java.util.List; import java.util.Locale; import java.util.Set; import java.util.stream.Stream; @@ -32,11 +34,18 @@ import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.MetadataRegistry; import org.openhab.core.library.CoreItemFactory; +import org.openhab.core.semantics.SemanticTag; +import org.openhab.core.semantics.SemanticTagImpl; +import org.openhab.core.semantics.SemanticTagRegistry; +import org.openhab.core.semantics.Tag; +import org.openhab.core.semantics.model.equipment.CleaningRobot; import org.openhab.core.semantics.model.location.Bathroom; import org.openhab.core.semantics.model.location.LivingRoom; +import org.openhab.core.semantics.model.location.Room; /** * @author Kai Kreuzer - Initial contribution + * @author Kai Kreuzer - Tests added for existing and new methods */ @ExtendWith(MockitoExtension.class) @NonNullByDefault @@ -44,12 +53,17 @@ public class SemanticsServiceImplTest { private @Mock @NonNullByDefault({}) ItemRegistry itemRegistryMock; private @Mock @NonNullByDefault({}) MetadataRegistry metadataRegistryMock; + private @Mock @NonNullByDefault({}) SemanticTagRegistry semanticTagRegistryMock; private @Mock @NonNullByDefault({}) UnitProvider unitProviderMock; private @NonNullByDefault({}) GroupItem locationItem; private @NonNullByDefault({}) GroupItem equipmentItem; private @NonNullByDefault({}) GenericItem pointItem; + private @NonNullByDefault({}) SemanticTag roomTag; + private @NonNullByDefault({}) SemanticTag bathroomTag; + private @NonNullByDefault({}) SemanticTag cleaningRobotTag; + private @NonNullByDefault({}) SemanticsServiceImpl service; @BeforeEach @@ -69,28 +83,198 @@ public void setup() throws Exception { pointItem.addGroupName(locationItem.getName()); locationItem.addMember(pointItem); - when(itemRegistryMock.stream()).thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) - .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) - .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)); + roomTag = new SemanticTagImpl("Location_Indoor_Room", "", "A room", ""); + bathroomTag = new SemanticTagImpl("Location_Indoor_Room_Bathroom", "", "A bathroom", ""); + cleaningRobotTag = new SemanticTagImpl("Equipment_CleaningRobot", "", "A cleaning robot", ""); - service = new SemanticsServiceImpl(itemRegistryMock, metadataRegistryMock); + service = new SemanticsServiceImpl(itemRegistryMock, metadataRegistryMock, semanticTagRegistryMock); } @Test public void testGetItemsInLocation() throws Exception { + when(itemRegistryMock.stream()).thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) + .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) + .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)); + Set items = service.getItemsInLocation(Bathroom.class); + assertEquals(1, items.size()); assertTrue(items.contains(pointItem)); - items = service.getItemsInLocation("Room", Locale.ENGLISH); + items = service.getItemsInLocation(Room.class); + assertEquals(1, items.size()); assertTrue(items.contains(pointItem)); + + items = service.getItemsInLocation(LivingRoom.class); + assertTrue(items.isEmpty()); } @Test public void testGetItemsInLocationByString() throws Exception { + when(itemRegistryMock.stream()).thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) + .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) + .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) + .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) + .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)) + .thenReturn(Stream.of(locationItem, equipmentItem, pointItem)); + when(semanticTagRegistryMock.getAll()).thenReturn(List.of(roomTag, bathroomTag, cleaningRobotTag)); + when(metadataRegistryMock.get(any())).thenReturn(null); + + // Label of a location group item Set items = service.getItemsInLocation("joe's room", Locale.ENGLISH); + assertEquals(1, items.size()); assertTrue(items.contains(pointItem)); - items = service.getItemsInLocation(LivingRoom.class); + // Location tag label + items = service.getItemsInLocation("bathroom", Locale.ENGLISH); + assertEquals(1, items.size()); + assertTrue(items.contains(pointItem)); + + // Location tag synonym + items = service.getItemsInLocation("powder room", Locale.ENGLISH); + assertEquals(1, items.size()); + assertTrue(items.contains(pointItem)); + + // Location parent tag label + items = service.getItemsInLocation("Room", Locale.ENGLISH); + assertEquals(1, items.size()); + assertTrue(items.contains(pointItem)); + + // Existing item label + items = service.getItemsInLocation("my Test label", Locale.ENGLISH); + assertTrue(items.isEmpty()); + + // Unknown item label + items = service.getItemsInLocation("wrong label", Locale.ENGLISH); assertTrue(items.isEmpty()); } + + @Test + public void testGetLabel() { + when(semanticTagRegistryMock.get("Location_Indoor_Room_Bathroom")).thenReturn(bathroomTag); + when(semanticTagRegistryMock.get("Equipment_CleaningRobot")).thenReturn(cleaningRobotTag); + + assertEquals("Bathroom", service.getLabel(Bathroom.class, Locale.ENGLISH)); + assertEquals("Robot de nettoyage", service.getLabel(CleaningRobot.class, Locale.FRENCH)); + } + + @Test + public void testGetDescription() { + when(semanticTagRegistryMock.get("Location_Indoor_Room_Bathroom")).thenReturn(bathroomTag); + when(semanticTagRegistryMock.get("Equipment_CleaningRobot")).thenReturn(cleaningRobotTag); + + assertEquals("A bathroom", service.getDescription(Bathroom.class, Locale.ENGLISH)); + assertEquals("A cleaning robot", service.getDescription(CleaningRobot.class, Locale.FRENCH)); + } + + @Test + public void testGetSynonyms() { + when(semanticTagRegistryMock.get("Location_Indoor_Room_Bathroom")).thenReturn(bathroomTag); + when(semanticTagRegistryMock.get("Equipment_CleaningRobot")).thenReturn(cleaningRobotTag); + + List result = service.getSynonyms(Bathroom.class, Locale.ENGLISH); + assertEquals(5, result.size()); + assertEquals("Bathrooms", result.get(0)); + assertEquals("Bath", result.get(1)); + assertEquals("Baths", result.get(2)); + assertEquals("Powder Room", result.get(3)); + assertEquals("Powder Rooms", result.get(4)); + + result = service.getSynonyms(CleaningRobot.class, Locale.FRENCH); + assertEquals(3, result.size()); + assertEquals("Robos de nettoyage", result.get(0)); + assertEquals("Robot aspirateur", result.get(1)); + assertEquals("Robots aspirateur", result.get(2)); + } + + @Test + public void testGetLabelAndSynonyms() { + when(semanticTagRegistryMock.get("Location_Indoor_Room_Bathroom")).thenReturn(bathroomTag); + when(semanticTagRegistryMock.get("Equipment_CleaningRobot")).thenReturn(cleaningRobotTag); + + List result = service.getLabelAndSynonyms(Bathroom.class, Locale.ENGLISH); + assertEquals(6, result.size()); + assertEquals("bathroom", result.get(0)); + assertEquals("bathrooms", result.get(1)); + assertEquals("bath", result.get(2)); + assertEquals("baths", result.get(3)); + assertEquals("powder room", result.get(4)); + assertEquals("powder rooms", result.get(5)); + + result = service.getLabelAndSynonyms(CleaningRobot.class, Locale.FRENCH); + assertEquals(4, result.size()); + assertEquals("robot de nettoyage", result.get(0)); + assertEquals("robos de nettoyage", result.get(1)); + assertEquals("robot aspirateur", result.get(2)); + assertEquals("robots aspirateur", result.get(3)); + } + + @Test + public void testGetByLabel() { + when(semanticTagRegistryMock.getAll()).thenReturn(List.of(roomTag, bathroomTag, cleaningRobotTag)); + + // Exact label + Class tag = service.getByLabel("Bathroom", Locale.ENGLISH); + assertEquals(Bathroom.class, tag); + // Label with different case + tag = service.getByLabel("bathroom", Locale.ENGLISH); + assertEquals(Bathroom.class, tag); + // Synonym + tag = service.getByLabel("Bath", Locale.ENGLISH); + assertNull(tag); + + // Exact label + tag = service.getByLabel("Robot de nettoyage", Locale.FRENCH); + assertEquals(CleaningRobot.class, tag); + // Label with different case + tag = service.getByLabel("robot de nettoyage", Locale.FRENCH); + assertEquals(CleaningRobot.class, tag); + // Synonym + tag = service.getByLabel("Robot aspirateur", Locale.FRENCH); + assertNull(tag); + } + + @Test + public void testGetByLabelOrSynonym() { + when(semanticTagRegistryMock.getAll()).thenReturn(List.of(roomTag, bathroomTag, cleaningRobotTag)); + + // Exact label + List> tags = service.getByLabelOrSynonym("Bathroom", Locale.ENGLISH); + assertEquals(1, tags.size()); + assertEquals(Bathroom.class, tags.get(0)); + // Label with different case + tags = service.getByLabelOrSynonym("BATHROOM", Locale.ENGLISH); + assertEquals(1, tags.size()); + assertEquals(Bathroom.class, tags.get(0)); + // Exact synonym + tags = service.getByLabelOrSynonym("Powder Room", Locale.ENGLISH); + assertEquals(1, tags.size()); + assertEquals(Bathroom.class, tags.get(0)); + // Synonym with different case + tags = service.getByLabelOrSynonym("POWDER Rooms", Locale.ENGLISH); + assertEquals(1, tags.size()); + assertEquals(Bathroom.class, tags.get(0)); + // Neither label nor synonyms + tags = service.getByLabelOrSynonym("other bath", Locale.ENGLISH); + assertTrue(tags.isEmpty()); + + // Exact label + tags = service.getByLabelOrSynonym("Robot de nettoyage", Locale.FRENCH); + assertEquals(1, tags.size()); + assertEquals(CleaningRobot.class, tags.get(0)); + // Label with different case + tags = service.getByLabelOrSynonym("ROBOT de nettoyage", Locale.FRENCH); + assertEquals(1, tags.size()); + assertEquals(CleaningRobot.class, tags.get(0)); + // Exact synonym + tags = service.getByLabelOrSynonym("Robot aspirateur", Locale.FRENCH); + assertEquals(1, tags.size()); + assertEquals(CleaningRobot.class, tags.get(0)); + // Synonym with different case + tags = service.getByLabelOrSynonym("ROBOTS aspirateur", Locale.FRENCH); + assertEquals(1, tags.size()); + assertEquals(CleaningRobot.class, tags.get(0)); + // Neither label nor synonyms + tags = service.getByLabelOrSynonym("Robot cuiseur", Locale.FRENCH); + assertTrue(tags.isEmpty()); + } }