Skip to content

Commit

Permalink
[rest] Add semantic tag registry + API to manage user tags (#3646)
Browse files Browse the repository at this point in the history
* 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.

Semantic tag class annotations are removed.

Semantic tag classes are now created at runtime.
Classes Locations, Equipments, Points and Properties are removed

Static methods SemanticTags.add removed
The adding of semantic tag classes is now managed only by the tag registry.

Avoids calling static method SemanticTags.getById when possible

SemanticsMetadataProvider service now requires semanticTagRegistry to start.

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
  • Loading branch information
lolodomo committed Jun 16, 2023
1 parent 6e83d3f commit f86635f
Show file tree
Hide file tree
Showing 159 changed files with 1,784 additions and 4,334 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.semantics.SemanticTags;
import org.openhab.core.semantics.SemanticTagRegistry;
import org.openhab.core.semantics.SemanticsPredicates;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
Expand Down Expand Up @@ -182,6 +182,7 @@ private static void respectForwarded(final UriBuilder uriBuilder, final @Context
private final ManagedItemProvider managedItemProvider;
private final MetadataRegistry metadataRegistry;
private final MetadataSelectorMatcher metadataSelectorMatcher;
private final SemanticTagRegistry semanticTagRegistry;
private final ItemRegistryChangeListener resetLastModifiedItemChangeListener = new ResetLastModifiedItemChangeListener();
private final RegistryChangeListener<Metadata> resetLastModifiedMetadataChangeListener = new ResetLastModifiedMetadataChangeListener();

Expand All @@ -196,7 +197,8 @@ public ItemResource(//
final @Reference LocaleService localeService, //
final @Reference ManagedItemProvider managedItemProvider,
final @Reference MetadataRegistry metadataRegistry,
final @Reference MetadataSelectorMatcher metadataSelectorMatcher) {
final @Reference MetadataSelectorMatcher metadataSelectorMatcher,
final @Reference SemanticTagRegistry semanticTagRegistry) {
this.dtoMapper = dtoMapper;
this.eventPublisher = eventPublisher;
this.itemBuilderFactory = itemBuilderFactory;
Expand All @@ -205,6 +207,7 @@ public ItemResource(//
this.managedItemProvider = managedItemProvider;
this.metadataRegistry = metadataRegistry;
this.metadataSelectorMatcher = metadataSelectorMatcher;
this.semanticTagRegistry = semanticTagRegistry;

this.itemRegistry.addRegistryChangeListener(resetLastModifiedItemChangeListener);
this.metadataRegistry.addRegistryChangeListener(resetLastModifiedMetadataChangeListener);
Expand Down Expand Up @@ -865,7 +868,8 @@ public Response getSemanticItem(final @Context UriInfo uriInfo, final @Context H
@PathParam("semanticClass") @Parameter(description = "semantic class") String semanticClassName) {
Locale locale = localeService.getLocale(language);

Class<? extends org.openhab.core.semantics.Tag> semanticClass = SemanticTags.getById(semanticClassName);
Class<? extends org.openhab.core.semantics.Tag> semanticClass = semanticTagRegistry
.getTagClassById(semanticClassName);
if (semanticClass == null) {
return Response.status(Status.NOT_FOUND).build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@
*/
package org.openhab.core.io.rest.core.internal.tag;

import java.util.Comparator;
import java.util.List;
import java.util.Locale;
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;
Expand All @@ -35,10 +40,10 @@
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.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
Expand All @@ -54,11 +59,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 user tags
*/
@Component
@JaxrsResource
Expand All @@ -74,28 +81,166 @@ public class TagResource implements RESTResource {
public static final String PATH_TAGS = "tags";

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)))) })
@Operation(operationId = "getSemanticTags", summary = "Get all available semantic tags.", responses = {
@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<String, List<TagDTO>> 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<EnrichedSemanticTagDTO> tagsDTO = semanticTagRegistry.getAll().stream()
.sorted(Comparator.comparing(SemanticTag::getUID))
.map(t -> new EnrichedSemanticTagDTO(t.localized(locale), semanticTagRegistry.isEditable(t))).toList();
return JSONResponse.createResponse(Status.OK, tagsDTO, null);
}

@GET
@RolesAllowed({ Role.USER, Role.ADMIN })
@Path("/{tagId}")
@Produces(MediaType.APPLICATION_JSON)
@Operation(operationId = "getSemanticTagAndSubTags", summary = "Gets a semantic tag and its sub tags.", responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = EnrichedSemanticTagDTO.class)))),
@ApiResponse(responseCode = "404", description = "Semantic 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<EnrichedSemanticTagDTO> tagsDTO = semanticTagRegistry.getSubTree(tag).stream()
.sorted(Comparator.comparing(SemanticTag::getUID))
.map(t -> new EnrichedSemanticTagDTO(t.localized(locale), semanticTagRegistry.isEditable(t)))
.toList();
return JSONResponse.createResponse(Status.OK, tagsDTO, null);
} else {
return JSONResponse.createErrorResponse(Status.NOT_FOUND, "Tag " + uid + " does not exist!");
}
}

@POST
@RolesAllowed({ Role.ADMIN })
@Consumes(MediaType.APPLICATION_JSON)
@Operation(operationId = "createSemanticTag", summary = "Creates a new semantic 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 JSONResponse.createErrorResponse(Status.BAD_REQUEST, "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 JSONResponse.createResponse(Status.CONFLICT,
new EnrichedSemanticTagDTO(tag.localized(locale), semanticTagRegistry.isEditable(tag)),
"Tag " + uid + " already exists!");
}

tag = new SemanticTagImpl(uid, data.label, data.description, data.synonyms);

// Check that a tag with this uid can be added to the registry
if (!semanticTagRegistry.canBeAdded(tag)) {
return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Invalid tag identifier " + uid);
}

managedSemanticTagProvider.add(tag);

return JSONResponse.createResponse(Status.CREATED,
new EnrichedSemanticTagDTO(tag.localized(locale), semanticTagRegistry.isEditable(tag)), null);
}

@DELETE
@RolesAllowed({ Role.ADMIN })
@Path("/{tagId}")
@Operation(operationId = "removeSemanticTag", summary = "Removes a semantic 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 = "Semantic tag not found."),
@ApiResponse(responseCode = "405", description = "Semantic tag not removable.") })
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) {
return JSONResponse.createErrorResponse(Status.NOT_FOUND, "Tag " + uid + " does not exist!");
}

// Check that tag is removable, 405 otherwise
if (!semanticTagRegistry.isEditable(tag)) {
return JSONResponse.createErrorResponse(Status.METHOD_NOT_ALLOWED, "Tag " + uid + " is not removable.");
}

semanticTagRegistry.removeSubTree(tag);

return Response.ok(null, MediaType.TEXT_PLAIN).build();
}

@PUT
@RolesAllowed({ Role.ADMIN })
@Path("/{tagId}")
@Consumes(MediaType.APPLICATION_JSON)
@Operation(operationId = "updateSemanticTag", summary = "Updates a semantic tag.", security = {
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = EnrichedSemanticTagDTO.class))),
@ApiResponse(responseCode = "404", description = "Semantic tag not found."),
@ApiResponse(responseCode = "405", description = "Semantic 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) {
return JSONResponse.createErrorResponse(Status.NOT_FOUND, "Tag " + uid + " does not exist!");
}

// Check that tag is editable, 405 otherwise
if (!semanticTagRegistry.isEditable(tag)) {
return JSONResponse.createErrorResponse(Status.METHOD_NOT_ALLOWED, "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 JSONResponse.createResponse(Status.OK, tags, null);
return JSONResponse.createResponse(Status.OK,
new EnrichedSemanticTagDTO(tag.localized(locale), semanticTagRegistry.isEditable(tag)), null);
}
}

0 comments on commit f86635f

Please sign in to comment.