Skip to content

Commit

Permalink
Add custom tag registry + REST API to manage custom tags
Browse files Browse the repository at this point in the history
Related to openhab#3619

New registry for custom tags.
New managed provider to add/remove/update custom tags.
Storage of managed custom tags in a JSON DB file.
New REST API to add/remove/update custom 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
lolodomo committed May 28, 2023
1 parent d87007a commit c4ef256
Show file tree
Hide file tree
Showing 18 changed files with 940 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,33 @@
import java.util.List;
import java.util.Locale;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.semantics.SemanticTags;
import org.openhab.core.semantics.Tag;
import org.openhab.core.semantics.TagInfo;

/**
* A DTO representing a Semantic {@link Tag}.
*
* @author Jimmy Tanagra - initial contribution
* @author Laurent Garnier - Add uid, description and editable
*/
@NonNullByDefault
public class TagDTO {
String uid;
String name;
String label;
String description;
List<String> synonyms;
boolean editable;

public TagDTO(Class<? extends Tag> tag, Locale locale) {
public TagDTO() {
}

public TagDTO(Class<? extends Tag> tag, Locale locale, boolean editable) {
this.uid = tag.getAnnotation(TagInfo.class).id();
this.name = tag.getSimpleName();
this.label = SemanticTags.getLabel(tag, locale);
this.description = SemanticTags.getDescription(tag, locale);
this.synonyms = SemanticTags.getSynonyms(tag, locale);
this.editable = editable;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@
import java.util.Map;

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,6 +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.CustomTag;
import org.openhab.core.semantics.CustomTagImpl;
import org.openhab.core.semantics.CustomTagRegistry;
import org.openhab.core.semantics.ManagedCustomTagProvider;
import org.openhab.core.semantics.SemanticTags;
import org.openhab.core.semantics.Tag;
import org.openhab.core.semantics.TagInfo;
import org.openhab.core.semantics.model.equipment.Equipments;
import org.openhab.core.semantics.model.location.Locations;
import org.openhab.core.semantics.model.point.Points;
Expand All @@ -47,18 +59,22 @@
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
Expand All @@ -73,11 +89,21 @@ public class TagResource implements RESTResource {
/** The URI path to this resource */
public static final String PATH_TAGS = "tags";

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

private final LocaleService localeService;
private final CustomTagRegistry customTagRegistry;
private final ManagedCustomTagProvider managedCustomTagProvider;

// TODO pattern in @Path

@Activate
public TagResource(final @Reference LocaleService localeService) {
public TagResource(final @Reference LocaleService localeService,
final @Reference CustomTagRegistry customTagRegistry,
final @Reference ManagedCustomTagProvider managedCustomTagProvider) {
this.localeService = localeService;
this.customTagRegistry = customTagRegistry;
this.managedCustomTagProvider = managedCustomTagProvider;
}

@GET
Expand All @@ -90,12 +116,237 @@ public Response getTags(final @Context UriInfo uriInfo, final @Context HttpHeade
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() //
Locations.class.getSimpleName(),
Locations.stream().map(tag -> new TagDTO(tag, locale, isEditable(tag))).toList(), //
Equipments.class.getSimpleName(),
Equipments.stream().map(tag -> new TagDTO(tag, locale, isEditable(tag))).toList(), //
Points.class.getSimpleName(),
Points.stream().map(tag -> new TagDTO(tag, locale, isEditable(tag))).toList(), //
Properties.class.getSimpleName(),
Properties.stream().map(tag -> new TagDTO(tag, locale, isEditable(tag))).toList() //
);

return JSONResponse.createResponse(Status.OK, tags, 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 = TagDTO.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();

Class<? extends Tag> tag = SemanticTags.getById(uid);
if (tag != null) {
List<Class<? extends Tag>> tags = SemanticTags.getSubTags(tag);
tags.add(tag);
List<TagDTO> tagsDTO = tags.stream().map(t -> new TagDTO(t, locale, isEditable(t))).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 = TagDTO.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) TagDTO 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
Class<? extends Tag> tag = SemanticTags.getById(uid);
if (tag != null) {
// report a conflict
return getTagResponse(Status.CONFLICT, tag, locale,
"Tag " + tag.getAnnotation(TagInfo.class).id() + " already exists!");
}

// Extract the tag name and th eparent tag
// Check that the parent tag already exists
Class<? extends Tag> 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 = SemanticTags.getById(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);
}
tag = SemanticTags.getById(name);
if (tag != null) {
// report a conflict
return getTagResponse(Status.CONFLICT, tag, locale,
"Tag " + tag.getAnnotation(TagInfo.class).id() + " already exists!");
}

uid = parentTag.getAnnotation(TagInfo.class).id() + "_" + name;
CustomTag customTag = new CustomTagImpl(uid, data.label, data.description, data.synonyms);
managedCustomTagProvider.add(customTag);

return getTagResponse(Status.CREATED, SemanticTags.getById(uid), 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);

// check whether tag exists and throw 404 if not
Class<? extends Tag> tag = SemanticTags.getById(tagId.trim());
if (tag == null) {
return getTagResponse(Status.NOT_FOUND, null, locale, "Tag " + tagId + " does not exist!");
}

String uid = tag.getAnnotation(TagInfo.class).id();

// check whether custom tag exists and throw 404 if not
CustomTag customTag = customTagRegistry.get(uid);
if (customTag == null) {
return getTagResponse(Status.NOT_FOUND, null, locale, "Tag " + uid + " is not a custom tag.");
}

// ask whether the tag exists as a managed tag, so it can get updated, 405 otherwise
customTag = managedCustomTagProvider.get(uid);
if (customTag == null) {
return getTagResponse(Status.METHOD_NOT_ALLOWED, null, locale, "Tag " + uid + " is not editable.");
}

// Same checks for the sub-tree of tags
List<Class<? extends Tag>> tags = SemanticTags.getSubTags(tag);
for (Class<? extends Tag> t : tags) {
uid = t.getAnnotation(TagInfo.class).id();

// check whether custom tag exists and throw 404 if not
customTag = customTagRegistry.get(uid);
if (customTag == null) {
return getTagResponse(Status.NOT_FOUND, null, locale, "Tag " + uid + " is not a custom tag.");
}

// ask whether the tag exists as a managed tag, so it can get updated, 405 otherwise
customTag = managedCustomTagProvider.get(uid);
if (customTag == null) {
return getTagResponse(Status.METHOD_NOT_ALLOWED, null, locale, "Tag " + uid + " is not editable.");
}
}

tags.add(tag);

tags.forEach(t -> {
managedCustomTagProvider.remove(t.getAnnotation(TagInfo.class).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 = TagDTO.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) TagDTO data) {
final Locale locale = localeService.getLocale(language);

// check whether tag exists and throw 404 if not
Class<? extends Tag> tag = SemanticTags.getById(tagId.trim());
if (tag == null) {
return getTagResponse(Status.NOT_FOUND, null, locale, "Tag " + tagId + " does not exist!");
}

String uid = tag.getAnnotation(TagInfo.class).id();

// check whether custom tag exists and throw 404 if not
CustomTag customTag = customTagRegistry.get(uid);
if (customTag == null) {
return getTagResponse(Status.NOT_FOUND, null, locale, "Tag " + uid + " is not a custom tag.");
}

// ask whether the tag exists as a managed tag, so it can get updated, 405 otherwise
customTag = managedCustomTagProvider.get(uid);
if (customTag == null) {
return getTagResponse(Status.METHOD_NOT_ALLOWED, null, locale, "Tag " + uid + " is not editable.");
}

customTag = new CustomTagImpl(uid, data.label != null ? data.label : customTag.getLabel(),
data.description != null ? data.description : customTag.getDescription(),
data.synonyms != null ? data.synonyms : customTag.getSynonyms());
managedCustomTagProvider.update(customTag);

return getTagResponse(Status.OK, SemanticTags.getById(uid), locale, null);
}

private Response getTagResponse(Status status, @Nullable Class<? extends Tag> tag, Locale locale,
@Nullable String errorMsg) {
TagDTO tagDTO = tag != null ? new TagDTO(tag, locale, isEditable(tag)) : null;
return JSONResponse.createResponse(status, tagDTO, errorMsg);
}

private boolean isEditable(Class<? extends Tag> tag) {
return managedCustomTagProvider.get(tag.getAnnotation(TagInfo.class).id()) != null;
}
}
16 changes: 16 additions & 0 deletions bundles/org.openhab.core.semantics/model/generateTagClasses.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ public class Locations {
public static boolean add(Class<? extends Location> tag) {
return LOCATIONS.add(tag);
}
public static boolean remove(Class<? extends Location> tag) {
return LOCATIONS.remove(tag);
}
}
""")
file.close()
Expand Down Expand Up @@ -180,6 +184,10 @@ public class Equipments {
public static boolean add(Class<? extends Equipment> tag) {
return EQUIPMENTS.add(tag);
}
public static boolean remove(Class<? extends Equipment> tag) {
return EQUIPMENTS.remove(tag);
}
}
""")
file.close()
Expand Down Expand Up @@ -222,6 +230,10 @@ public class Points {
public static boolean add(Class<? extends Point> tag) {
return POINTS.add(tag);
}
public static boolean remove(Class<? extends Point> tag) {
return POINTS.remove(tag);
}
}
""")
file.close()
Expand Down Expand Up @@ -264,6 +276,10 @@ public class Properties {
public static boolean add(Class<? extends Property> tag) {
return PROPERTIES.add(tag);
}
public static boolean remove(Class<? extends Property> tag) {
return PROPERTIES.remove(tag);
}
}
""")
file.close()
Expand Down
Loading

0 comments on commit c4ef256

Please sign in to comment.