Skip to content

Commit

Permalink
Add semantic tag registry + REST API to manage user tags
Browse files Browse the repository at this point in the history
Related to openhab#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 <[email protected]>
  • Loading branch information
lolodomo committed Jun 10, 2023
1 parent d87007a commit 2324794
Show file tree
Hide file tree
Showing 159 changed files with 1,736 additions and 4,334 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,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 @@ -174,6 +174,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;

@Activate
public ItemResource(//
Expand All @@ -184,7 +185,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 @@ -193,6 +195,7 @@ public ItemResource(//
this.managedItemProvider = managedItemProvider;
this.metadataRegistry = metadataRegistry;
this.metadataSelectorMatcher = metadataSelectorMatcher;
this.semanticTagRegistry = semanticTagRegistry;
}

private UriBuilder uriBuilder(final UriInfo uriInfo, final HttpHeaders httpHeaders) {
Expand Down Expand Up @@ -814,7 +817,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 @@ -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;
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,177 @@ 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((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 = "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 = "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((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 = "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 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!");
}

// Check that the provided uid is a valid new tag id
if (!semanticTagRegistry.isNewIdValid(uid)) {
return getTagResponse(Status.BAD_REQUEST, null, locale, "Invalid tag identifier " + uid);
}

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 = "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 = "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) {
return getTagResponse(Status.NOT_FOUND, null, locale, "Tag " + uid + " does not exist!");
}

// Get tags in reverse order
List<String> 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.forEach(id -> managedSemanticTagProvider.remove(id));

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 = "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) {
return getTagResponse(Status.NOT_FOUND, null, locale, "Tag " + uid + " 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;
}
}
Loading

0 comments on commit 2324794

Please sign in to comment.