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.

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
  • Loading branch information
lolodomo committed Jun 2, 2023
1 parent d87007a commit 58899c3
Show file tree
Hide file tree
Showing 23 changed files with 1,743 additions and 303 deletions.
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
/**
* 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 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;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.auth.Role;
import org.openhab.core.io.rest.JSONResponse;
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.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;
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired;
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;
import io.swagger.v3.oas.annotations.media.ArraySchema;
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
@JaxrsName(SemanticTagResource.PATH_TAGS)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(SemanticTagResource.PATH_TAGS)
@io.swagger.v3.oas.annotations.tags.Tag(name = SemanticTagResource.PATH_TAGS)
@NonNullByDefault
public class SemanticTagResource implements RESTResource {

/** The URI path to this resource */
public static final String PATH_TAGS = "tags";

private final Logger logger = LoggerFactory.getLogger(SemanticTagResource.class);

private final LocaleService localeService;
private final SemanticTagRegistry semanticTagRegistry;
private final ManagedSemanticTagProvider managedSemanticTagProvider;

// TODO pattern in @Path

@Activate
public SemanticTagResource(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 = 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);

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 = "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<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!");
}
}

/**
* 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 = 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<? extends Tag> 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 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);

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<? extends Tag> 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<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.stream().map(id -> managedSemanticTagProvider.remove(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 = 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<? extends Tag> 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);
}

private boolean isEditable(SemanticTag tag) {
return managedSemanticTagProvider.get(tag.getUID()) != null;
}
}

0 comments on commit 58899c3

Please sign in to comment.