forked from openhab/openhab-core
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add semantic tag registry + REST API to manage user tags
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 <[email protected]>
- Loading branch information
Showing
23 changed files
with
1,743 additions
and
303 deletions.
There are no files selected for viewing
41 changes: 41 additions & 0 deletions
41
...core/src/main/java/org/openhab/core/io/rest/core/internal/tag/EnrichedSemanticTagDTO.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
320 changes: 320 additions & 0 deletions
320
...st.core/src/main/java/org/openhab/core/io/rest/core/internal/tag/SemanticTagResource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.