Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add semantic tag registry + REST API to manage user tags #3636

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small note on consistency - it doesn't seem to be much enriched compared to other enriched DTO types we have at REST layer, its just an regular representation which does not ship anything beyond SementicTag itself. Or I am wrong?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enriched just because of the "editable" property.

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,13 @@
import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
import org.openhab.core.semantics.model.equipment.Equipments;
import org.openhab.core.semantics.model.location.Locations;
import org.openhab.core.semantics.model.point.Points;
import org.openhab.core.semantics.model.property.Properties;
import org.openhab.core.semantics.ManagedSemanticTagProvider;
import org.openhab.core.semantics.SemanticTag;
import org.openhab.core.semantics.SemanticTagImpl;
import org.openhab.core.semantics.SemanticTagRegistry;
import org.openhab.core.semantics.SemanticTags;
import org.openhab.core.semantics.Tag;
import org.openhab.core.semantics.TagInfo;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
Expand All @@ -54,11 +62,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 +84,195 @@ 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!");
}

// 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));
lolodomo marked this conversation as resolved.
Show resolved Hide resolved
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
@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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be simplified to a place where we just confirm given tag id is managed or not? I suppose we will not reach a place where parent tag might be managed and its children might not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is currently not possible but could become possible later when an unmanaged provider is added.

I am not yet sure if we should or not allow unmanaged tags to be added as childs to a managed tag and vice-versa. Because this will make loading of unmanaged and managed providers very difficult.

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