From c4ef2569f867eaf4cfefb84d4fd012f2e48e21a2 Mon Sep 17 00:00:00 2001 From: Laurent Garnier Date: Sat, 27 May 2023 22:40:02 +0200 Subject: [PATCH] Add custom tag registry + REST API to manage custom tags Related to #3619 New registry for custom tags. New managed provider to add/remove/update custom tags. Storage of managed custom tags in a JSON DB file. New REST API to add/remove/update custom tags in the semantic model. New REST API to get a sub-tree of the semantic tags. Signed-off-by: Laurent Garnier --- .../io/rest/core/internal/tag/TagDTO.java | 15 +- .../rest/core/internal/tag/TagResource.java | 261 +++++++++++++++++- .../model/generateTagClasses.groovy | 16 ++ .../org/openhab/core/semantics/CustomTag.java | 62 +++++ .../openhab/core/semantics/CustomTagImpl.java | 74 +++++ .../semantics/CustomTagNotFoundException.java | 30 ++ .../core/semantics/CustomTagProvider.java | 26 ++ .../core/semantics/CustomTagRegistry.java | 44 +++ .../semantics/ManagedCustomTagProvider.java | 63 +++++ .../openhab/core/semantics/SemanticTags.java | 145 +++++++--- .../core/semantics/dto/CustomTagDTO.java | 31 +++ .../semantics/dto/CustomTagDTOMapper.java | 59 ++++ .../internal/CustomTagRegistryImpl.java | 146 ++++++++++ .../semantics/model/equipment/Equipments.java | 4 + .../semantics/model/location/Locations.java | 4 + .../core/semantics/model/point/Points.java | 4 + .../semantics/model/property/Properties.java | 4 + .../core/semantics/SemanticTagsTest.java | 8 +- 18 files changed, 940 insertions(+), 56 deletions(-) create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTag.java create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTagImpl.java create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTagNotFoundException.java create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTagProvider.java create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTagRegistry.java create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/ManagedCustomTagProvider.java create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/CustomTagDTO.java create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/CustomTagDTOMapper.java create mode 100644 bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/CustomTagRegistryImpl.java 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 index 37d68f17cec..1df8e571749 100644 --- 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 @@ -15,24 +15,33 @@ 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; +import org.openhab.core.semantics.TagInfo; /** * A DTO representing a Semantic {@link Tag}. * * @author Jimmy Tanagra - initial contribution + * @author Laurent Garnier - Add uid, description and editable */ -@NonNullByDefault public class TagDTO { + String uid; String name; String label; + String description; List synonyms; + boolean editable; - public TagDTO(Class tag, Locale locale) { + public TagDTO() { + } + + public TagDTO(Class tag, Locale locale, boolean editable) { + this.uid = tag.getAnnotation(TagInfo.class).id(); this.name = tag.getSimpleName(); this.label = SemanticTags.getLabel(tag, locale); + this.description = SemanticTags.getDescription(tag, locale); this.synonyms = SemanticTags.getSynonyms(tag, locale); + this.editable = editable; } } 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..f9631afd451 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 @@ -17,9 +17,14 @@ import java.util.Map; 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,6 +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.CustomTag; +import org.openhab.core.semantics.CustomTagImpl; +import org.openhab.core.semantics.CustomTagRegistry; +import org.openhab.core.semantics.ManagedCustomTagProvider; +import org.openhab.core.semantics.SemanticTags; +import org.openhab.core.semantics.Tag; +import org.openhab.core.semantics.TagInfo; import org.openhab.core.semantics.model.equipment.Equipments; import org.openhab.core.semantics.model.location.Locations; import org.openhab.core.semantics.model.point.Points; @@ -47,6 +59,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 +68,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,11 +89,21 @@ 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 CustomTagRegistry customTagRegistry; + private final ManagedCustomTagProvider managedCustomTagProvider; + + // TODO pattern in @Path @Activate - public TagResource(final @Reference LocaleService localeService) { + public TagResource(final @Reference LocaleService localeService, + final @Reference CustomTagRegistry customTagRegistry, + final @Reference ManagedCustomTagProvider managedCustomTagProvider) { this.localeService = localeService; + this.customTagRegistry = customTagRegistry; + this.managedCustomTagProvider = managedCustomTagProvider; } @GET @@ -90,12 +116,237 @@ public Response getTags(final @Context UriInfo uriInfo, final @Context HttpHeade 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() // + Locations.class.getSimpleName(), + Locations.stream().map(tag -> new TagDTO(tag, locale, isEditable(tag))).toList(), // + Equipments.class.getSimpleName(), + Equipments.stream().map(tag -> new TagDTO(tag, locale, isEditable(tag))).toList(), // + Points.class.getSimpleName(), + Points.stream().map(tag -> new TagDTO(tag, locale, isEditable(tag))).toList(), // + Properties.class.getSimpleName(), + Properties.stream().map(tag -> new TagDTO(tag, locale, isEditable(tag))).toList() // ); return JSONResponse.createResponse(Status.OK, tags, 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 = TagDTO.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(); + + Class tag = SemanticTags.getById(uid); + if (tag != null) { + List> tags = SemanticTags.getSubTags(tag); + tags.add(tag); + List tagsDTO = tags.stream().map(t -> new TagDTO(t, locale, isEditable(t))).toList(); + return JSONResponse.createResponse(Status.OK, tagsDTO, null); + } else { + return getTagResponse(Status.NOT_FOUND, null, locale, "Tag " + uid + " does not exist!"); + } + } + + /** + * create a new custom tag + * + * @param language + * @param tagId + * @param label + * @param description + * @param synonyms + * @return Response holding the newly created tag or error information + */ + @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 = TagDTO.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) TagDTO 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 + Class tag = SemanticTags.getById(uid); + if (tag != null) { + // report a conflict + return getTagResponse(Status.CONFLICT, tag, locale, + "Tag " + tag.getAnnotation(TagInfo.class).id() + " already exists!"); + } + + // Extract the tag name and th eparent tag + // Check that the parent tag already exists + Class 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 = SemanticTags.getById(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); + } + tag = SemanticTags.getById(name); + if (tag != null) { + // report a conflict + return getTagResponse(Status.CONFLICT, tag, locale, + "Tag " + tag.getAnnotation(TagInfo.class).id() + " already exists!"); + } + + uid = parentTag.getAnnotation(TagInfo.class).id() + "_" + name; + CustomTag customTag = new CustomTagImpl(uid, data.label, data.description, data.synonyms); + managedCustomTagProvider.add(customTag); + + return getTagResponse(Status.CREATED, SemanticTags.getById(uid), locale, null); + } + + /** + * Delete a custom tag, if possible. Tag deletion might be impossible if the + * custom tag is not managed, will return CONFLICT. + * + * @param language + * @param tagId + * @return Response with status/error information + */ + @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); + + // check whether tag exists and throw 404 if not + Class tag = SemanticTags.getById(tagId.trim()); + if (tag == null) { + return getTagResponse(Status.NOT_FOUND, null, locale, "Tag " + tagId + " does not exist!"); + } + + String uid = tag.getAnnotation(TagInfo.class).id(); + + // check whether custom tag exists and throw 404 if not + CustomTag customTag = customTagRegistry.get(uid); + if (customTag == null) { + return getTagResponse(Status.NOT_FOUND, null, locale, "Tag " + uid + " is not a custom tag."); + } + + // ask whether the tag exists as a managed tag, so it can get updated, 405 otherwise + customTag = managedCustomTagProvider.get(uid); + if (customTag == null) { + return getTagResponse(Status.METHOD_NOT_ALLOWED, null, locale, "Tag " + uid + " is not editable."); + } + + // Same checks for the sub-tree of tags + List> tags = SemanticTags.getSubTags(tag); + for (Class t : tags) { + uid = t.getAnnotation(TagInfo.class).id(); + + // check whether custom tag exists and throw 404 if not + customTag = customTagRegistry.get(uid); + if (customTag == null) { + return getTagResponse(Status.NOT_FOUND, null, locale, "Tag " + uid + " is not a custom tag."); + } + + // ask whether the tag exists as a managed tag, so it can get updated, 405 otherwise + customTag = managedCustomTagProvider.get(uid); + if (customTag == null) { + return getTagResponse(Status.METHOD_NOT_ALLOWED, null, locale, "Tag " + uid + " is not editable."); + } + } + + tags.add(tag); + + tags.forEach(t -> { + managedCustomTagProvider.remove(t.getAnnotation(TagInfo.class).id()); + }); + + return Response.ok(null, MediaType.TEXT_PLAIN).build(); + } + + /** + * Update a custom tag. + * + * @param language + * @param tagId + * @param label + * @param description + * @param synonyms + * @return Response with the updated custom tag or error information + */ + @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 = TagDTO.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) TagDTO data) { + final Locale locale = localeService.getLocale(language); + + // check whether tag exists and throw 404 if not + Class tag = SemanticTags.getById(tagId.trim()); + if (tag == null) { + return getTagResponse(Status.NOT_FOUND, null, locale, "Tag " + tagId + " does not exist!"); + } + + String uid = tag.getAnnotation(TagInfo.class).id(); + + // check whether custom tag exists and throw 404 if not + CustomTag customTag = customTagRegistry.get(uid); + if (customTag == null) { + return getTagResponse(Status.NOT_FOUND, null, locale, "Tag " + uid + " is not a custom tag."); + } + + // ask whether the tag exists as a managed tag, so it can get updated, 405 otherwise + customTag = managedCustomTagProvider.get(uid); + if (customTag == null) { + return getTagResponse(Status.METHOD_NOT_ALLOWED, null, locale, "Tag " + uid + " is not editable."); + } + + customTag = new CustomTagImpl(uid, data.label != null ? data.label : customTag.getLabel(), + data.description != null ? data.description : customTag.getDescription(), + data.synonyms != null ? data.synonyms : customTag.getSynonyms()); + managedCustomTagProvider.update(customTag); + + return getTagResponse(Status.OK, SemanticTags.getById(uid), locale, null); + } + + private Response getTagResponse(Status status, @Nullable Class tag, Locale locale, + @Nullable String errorMsg) { + TagDTO tagDTO = tag != null ? new TagDTO(tag, locale, isEditable(tag)) : null; + return JSONResponse.createResponse(status, tagDTO, errorMsg); + } + + private boolean isEditable(Class tag) { + return managedCustomTagProvider.get(tag.getAnnotation(TagInfo.class).id()) != null; + } } diff --git a/bundles/org.openhab.core.semantics/model/generateTagClasses.groovy b/bundles/org.openhab.core.semantics/model/generateTagClasses.groovy index 20f9cb58e79..78151d39c21 100755 --- a/bundles/org.openhab.core.semantics/model/generateTagClasses.groovy +++ b/bundles/org.openhab.core.semantics/model/generateTagClasses.groovy @@ -138,6 +138,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() @@ -180,6 +184,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() @@ -222,6 +230,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() @@ -264,6 +276,10 @@ 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() diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTag.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTag.java new file mode 100644 index 00000000000..ef0278e1a47 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTag.java @@ -0,0 +1,62 @@ +/** + * 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.Identifiable; + +/** + * This interface defines the core features of an openHAB custom tag. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public interface CustomTag extends Identifiable { + + /** + * Returns the name of the tag. + * + * @return the name of the tag + */ + String getName(); + + /** + * Returns the UID of the parent tag. + * + * @return the UID of the parent tag + */ + String getParentUID(); + + /** + * Returns the label of the tag. + * + * @return tag label or null if undefined + */ + String getLabel(); + + /** + * Returns the description of the tag. + * + * @return tag description or null if undefined + */ + String getDescription(); + + /** + * Returns the synonyms of the tag. + * + * @return tag synonyms as a List + */ + List getSynonyms(); +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTagImpl.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTagImpl.java new file mode 100644 index 00000000000..51d8625c00c --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTagImpl.java @@ -0,0 +1,74 @@ +/** + * 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.eclipse.jdt.annotation.Nullable; + +/** + * This is the main implementing class of the {@link CustomTag} interface. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class CustomTagImpl implements CustomTag { + + private String uid; + private String name; + private String parent; + private String label; + private String description; + private List synonyms; + + public CustomTagImpl(String uid, @Nullable String label, @Nullable String description, + @Nullable List synonyms) { + this.uid = uid; + this.name = uid.substring(uid.lastIndexOf("_") + 1).trim(); + this.parent = uid.substring(0, uid.lastIndexOf("_")).trim(); + this.label = label == null ? "" : label; + this.description = description == null ? "" : description; + this.synonyms = synonyms == null ? List.of() : synonyms; + } + + @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; + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTagNotFoundException.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTagNotFoundException.java new file mode 100644 index 00000000000..6dbb8482dc4 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTagNotFoundException.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 org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This exception is thrown by the {@link CustomTagRegistry} if custom tag could not be found. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class CustomTagNotFoundException extends Exception { + + private static final long serialVersionUID = 1L; + + public CustomTagNotFoundException(String id) { + super("Custom tag '" + id + "' could not be found in the custom tag registry"); + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTagProvider.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTagProvider.java new file mode 100644 index 00000000000..a71780ea6c4 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTagProvider.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 CustomTagProvider} is responsible for providing custom tags. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public interface CustomTagProvider extends Provider { + +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTagRegistry.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTagRegistry.java new file mode 100644 index 00000000000..d3d16044d46 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/CustomTagRegistry.java @@ -0,0 +1,44 @@ +/** + * 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.Collection; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.Registry; + +/** + * {@link CustomTagRegistry} tracks all {@link CustomTag}s from different {@link CustomTagProvider}s + * and provides access to them. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public interface CustomTagRegistry extends Registry { + + /** + * This method retrieves a single custom tag from the registry. + * + * @param uid the custom tag unique identifier + * @return the uniquely identified custom tag + * @throws CustomTagNotFoundException if no custom tag matches the input + */ + CustomTag getCustomTag(String uid) throws CustomTagNotFoundException; + + /** + * This method retrieves all custom tags that are currently available in the registry + * + * @return a collection of all available custom tags + */ + Collection getCustomTags(); +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/ManagedCustomTagProvider.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/ManagedCustomTagProvider.java new file mode 100644 index 00000000000..d2b1dcc23f4 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/ManagedCustomTagProvider.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.CustomTagDTO; +import org.openhab.core.semantics.dto.CustomTagDTOMapper; +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 ManagedCustomTagProvider} is an OSGi service, that allows to add or remove + * custom tags at runtime by calling {@link ManagedCustomTagProvider#add(CustomTag)} or + * {@link ManagedCustomTagProvider#remove(String)}. + * An added custom tag is automatically exposed to the {@link CustomTagRegistry}. + * Persistence of added custom tags is handled by a {@link StorageService}. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = { CustomTagProvider.class, ManagedCustomTagProvider.class }) +public class ManagedCustomTagProvider extends AbstractManagedProvider + implements CustomTagProvider { + + @Activate + public ManagedCustomTagProvider(final @Reference StorageService storageService) { + super(storageService); + } + + @Override + protected String getStorageName() { + return CustomTag.class.getName(); + } + + @Override + protected String keyToString(String key) { + return key; + } + + @Override + protected @Nullable CustomTag toElement(String uid, CustomTagDTO persistedCustomTag) { + return CustomTagDTOMapper.map(persistedCustomTag); + } + + @Override + protected CustomTagDTO toPersistableElement(CustomTag customTag) { + return CustomTagDTOMapper.map(customTag); + } +} 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..f60e5252c19 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.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; @@ -51,6 +52,7 @@ public class SemanticTags { private static final String TAGS_BUNDLE_NAME = "tags"; + // TODO thread safe ? private static final Map> TAGS = new TreeMap<>(); private static final Logger LOGGER = LoggerFactory.getLogger(SemanticTags.class); @@ -131,7 +133,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,20 +238,20 @@ 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); + return add(name, parent, "", "", ""); } /** * 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, @@ -258,8 +260,8 @@ public static String getDescription(Class tag, Locale locale) { * @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) { + public static @Nullable Class add(String name, String parent, String label, String synonyms, + String description) { Class parentClass = getById(parent); if (parentClass == null) { LOGGER.warn("Adding semantic tag '{}' failed because parent tag '{}' is not found.", name, parent); @@ -270,20 +272,20 @@ 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, Class parent) { - return add(name, parent, null, null, null); + return add(name, parent, "", "", ""); } /** * 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, @@ -292,8 +294,10 @@ public static String getDescription(Class tag, Locale locale) { * @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, String label, + String synonyms, String description) { + LOGGER.trace("Semantics add name \"{}\" parent id {} label \"{}\" description \"{}\" synonyms \"{}\"", name, + parent.getAnnotation(TagInfo.class).id(), label, description, synonyms); if (getById(name) != null) { return null; } @@ -306,45 +310,76 @@ 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; + if (label.isEmpty()) { + // Infer label from name, splitting up CamelCaseALL99 -> Camel Case ALL 99 + label = name.replaceAll("([A-Z][a-z]+|[A-Z][A-Z]+|[0-9]+)", " $1"); + } + synonyms = synonyms.replaceAll("\\s*,\\s*", ","); - // 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 oldTag; try { - newTag = CLASS_LOADER.defineClass(className, byteCode); - } catch (Exception e) { - LOGGER.warn("Failed creating a new semantic tag '{}': {}", className, e.getMessage()); - return null; + oldTag = (Class) Class.forName(className, false, CLASS_LOADER); + LOGGER.trace("Class '{}' exists", className); + } catch (ClassNotFoundException e) { + oldTag = null; } + + Class newTag; + if (oldTag == 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", label.trim()); + annotation.visit("synonyms", synonyms.trim()); + annotation.visit("description", description.trim()); + 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; + } + } else { + // TODO updata TagInfo class annotation + newTag = oldTag; + } + 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()); + } + + public static List> getSubTags(Class tag) { + List keys = TAGS.keySet().stream() + .filter(id -> id.startsWith(tag.getAnnotation(TagInfo.class).id() + "_")).collect(Collectors.toList()); + List> tags = new ArrayList<>(); + keys.forEach(key -> { + Class t = TAGS.get(key); + if (t != null) { + tags.add(t); + } + }); + return tags; + } + private static void addTagSet(Class tagSet) { String id = tagSet.getAnnotation(TagInfo.class).id(); while (id.indexOf("_") != -1) { @@ -354,6 +389,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 +411,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/CustomTagDTO.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/CustomTagDTO.java new file mode 100644 index 00000000000..a00348fbc8e --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/CustomTagDTO.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 custom tags. + * + * @author Laurent Garnier - Initial contribution + */ +public class CustomTagDTO { + + public String uid; + public String label; + public String description; + public List synonyms; + + public CustomTagDTO() { + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/CustomTagDTOMapper.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/CustomTagDTOMapper.java new file mode 100644 index 00000000000..df8359a544a --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/dto/CustomTagDTOMapper.java @@ -0,0 +1,59 @@ +/** + * 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.CustomTag; +import org.openhab.core.semantics.CustomTagImpl; + +/** + * The {@link CustomTagDTOMapper} is an utility class to map custom tags into custom tag data transfer objects (DTOs). + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class CustomTagDTOMapper { + + /** + * Maps custom tag DTO into custom tag object. + * + * @param customTagDTO the DTO + * @return the custom tag object + */ + public static @Nullable CustomTag map(@Nullable CustomTagDTO customTagDTO) { + if (customTagDTO == null) { + throw new IllegalArgumentException("The argument 'customTagDTO' must not be null."); + } + if (customTagDTO.uid == null) { + throw new IllegalArgumentException("The argument 'customTagDTO.uid' must not be null."); + } + + return new CustomTagImpl(customTagDTO.uid, customTagDTO.label, customTagDTO.description, customTagDTO.synonyms); + } + + /** + * Maps custom tag object into custom tag DTO. + * + * @param customTag the custom tag + * @return the custom tag DTO + */ + public static CustomTagDTO map(CustomTag customTag) { + CustomTagDTO customTagDTO = new CustomTagDTO(); + customTagDTO.uid = customTag.getUID(); + customTagDTO.label = customTag.getLabel(); + customTagDTO.description = customTag.getDescription(); + customTagDTO.synonyms = customTag.getSynonyms(); + return customTagDTO; + } +} diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/CustomTagRegistryImpl.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/CustomTagRegistryImpl.java new file mode 100644 index 00000000000..be9a43f5045 --- /dev/null +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/CustomTagRegistryImpl.java @@ -0,0 +1,146 @@ +/** + * 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.Collection; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.AbstractRegistry; +import org.openhab.core.semantics.CustomTag; +import org.openhab.core.semantics.CustomTagNotFoundException; +import org.openhab.core.semantics.CustomTagProvider; +import org.openhab.core.semantics.CustomTagRegistry; +import org.openhab.core.semantics.ManagedCustomTagProvider; +import org.openhab.core.semantics.SemanticTags; +import org.openhab.core.semantics.Tag; +import org.openhab.core.semantics.TagInfo; +import org.openhab.core.service.ReadyService; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is the main implementing class of the {@link CustomTagRegistry} interface. It + * keeps track of all declared custom tags of all custom tags providers and keeps their + * current state in memory. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true) +public class CustomTagRegistryImpl extends AbstractRegistry + implements CustomTagRegistry { + + private final Logger logger = LoggerFactory.getLogger(CustomTagRegistryImpl.class); + + public CustomTagRegistryImpl() { + super(CustomTagProvider.class); + } + + @Override + public CustomTag getCustomTag(String uid) throws CustomTagNotFoundException { + final CustomTag customTag = get(uid); + if (customTag == null) { + throw new CustomTagNotFoundException(uid); + } else { + return customTag; + } + } + + @Override + public Collection getCustomTags() { + return getAll(); + } + + @Override + protected void onAddElement(CustomTag customTag) throws IllegalArgumentException { + logger.trace("onAddElement {}", customTag.getUID()); + super.onAddElement(customTag); + createSemanticTag(customTag.getUID(), customTag.getLabel(), customTag.getDescription(), + customTag.getSynonyms()); + } + + @Override + protected void onRemoveElement(CustomTag customTag) { + logger.trace("onRemoveElement {}", customTag.getUID()); + super.onRemoveElement(customTag); + Class tag = SemanticTags.getById(customTag.getUID()); + if (tag != null) { + SemanticTags.remove(tag); + } + } + + @Override + protected void onUpdateElement(CustomTag oldCustomTag, CustomTag customTag) throws IllegalArgumentException { + logger.trace("onUpdateElement {}", customTag.getUID()); + super.onUpdateElement(oldCustomTag, customTag); + Class tag = SemanticTags.getById(customTag.getUID()); + if (tag == null) { + throw new IllegalArgumentException("No existing tag with id " + customTag.getUID()); + } + SemanticTags.remove(tag); + createSemanticTag(customTag.getUID(), customTag.getLabel(), customTag.getDescription(), + customTag.getSynonyms()); + } + + private void createSemanticTag(String uid, String label, String description, List synonyms) + throws IllegalArgumentException { + Class tag = SemanticTags.getById(uid); + if (tag != null) { + throw new IllegalArgumentException("Tag " + tag.getAnnotation(TagInfo.class).id() + " already exist"); + } + Class parentTag = null; + int lastSeparator = uid.lastIndexOf("_"); + if (lastSeparator <= 0) { + throw new IllegalArgumentException("Invalid tag id " + uid); + } + String name = uid.substring(lastSeparator + 1); + parentTag = SemanticTags.getById(uid.substring(0, lastSeparator)); + if (parentTag == 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); + } + tag = SemanticTags.getById(name); + if (tag != null) { + throw new IllegalArgumentException("Tag " + tag.getAnnotation(TagInfo.class).id() + " already exist"); + } + if (SemanticTags.add(name, parentTag, label, String.join(",", synonyms), description) == null) { + throw new IllegalArgumentException("Failed to create semantic tag " + uid); + } + } + + @Override + @Reference + protected void setReadyService(ReadyService readyService) { + super.setReadyService(readyService); + } + + @Override + protected void unsetReadyService(ReadyService readyService) { + super.unsetReadyService(readyService); + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + protected void setManagedProvider(ManagedCustomTagProvider provider) { + super.setManagedProvider(provider); + } + + protected void unsetManagedProvider(ManagedCustomTagProvider provider) { + super.unsetManagedProvider(provider); + } +} 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..164487115bb 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 @@ -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..0a3721c019f 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 @@ -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..79c1f9e6aa4 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 @@ -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..c5ebd4418e5 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 @@ -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..b57e080f3f6 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,7 +12,7 @@ */ package org.openhab.core.semantics; -import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.CoreMatchers.hasItems; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; @@ -102,7 +102,7 @@ public void testGetSynonyms() { @Test public void testGetDescription() { - Class tag = SemanticTags.add("TestDesc", Light.class, null, null, "Test Description"); + Class tag = SemanticTags.add("TestDesc", Light.class, "", "", "Test Description"); assertEquals("Test Description", SemanticTags.getDescription(tag, Locale.ENGLISH)); } @@ -235,14 +235,14 @@ public void testAddingExistingTagShouldFail() { @Test public void testAddWithCustomLabel() { - Class tag = SemanticTags.add("CustomProperty2", Property.class, " Custom Label ", null, null); + Class tag = SemanticTags.add("CustomProperty2", Property.class, " Custom Label ", "", ""); 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); + Class tag = SemanticTags.add("CustomProperty3", Property.class, "", synonyms, ""); 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));