-
-
Notifications
You must be signed in to change notification settings - Fork 422
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
Changes from all commits
f6670ea
575785a
4a0a909
25701dc
d4dbc58
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -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 | ||
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.