/**
* Copyright 2016 Hortonworks.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package com.hortonworks.registries.tag.service;
import com.codahale.metrics.annotation.Timed;
import com.hortonworks.registries.common.QueryParam;
import com.hortonworks.registries.common.exception.service.exception.request.EntityNotFoundException;
import com.hortonworks.registries.common.util.WSUtils;
import com.hortonworks.registries.tag.Tag;
import com.hortonworks.registries.tag.TaggedEntity;
import com.hortonworks.registries.tag.dto.TagDto;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
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.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static javax.ws.rs.core.Response.Status.CREATED;
import static javax.ws.rs.core.Response.Status.OK;
/**
* REST resource for managing hierarchical tags in a system.
* <p>
* <b>Note:</b> The JAVADOCS should be updated whenever there are any changes to the api.
*/
@Path("/v1/catalog")
@Produces(MediaType.APPLICATION_JSON)
public class TagCatalogResource {
public static final Map<String, String> SUCCESS_MESSAGE_ENTITY = Collections.singletonMap("responseMessage", "Success");
private final TagService tagService;
public TagCatalogResource(TagService tagService) {
this.tagService = tagService;
}
/**
* <p>
* Lists all the tags in the system or the ones matching specific query params. For example to
* list all the tags in the system,
* </p>
* <b>GET /api/v1/catalog/tags</b>
* <p>
* <pre>
* {
* "entities": [
* {
* "id": 3,
* "name": "thermostat",
* "description": "thermostat device",
* "timestamp": 1481673224536,
* "tagIds": [
* 2
* ]
* },
* {
* "id": 2,
* "name": "device",
* "description": "device tag",
* "timestamp": 1481673156548,
* "tagIds": []
* }
* ,
* ...
* ]
* }
* </pre>
* <p>
* <p>
* The tags can also be listed based on specific query params. For example to list
* tag(s) with the name "device",
* </p>
* <b>GET /api/v1/catalog/tags?name=device</b>
* <pre>
* {
* "entities": [
* {
* "id": 2,
* "name": "device",
* "description": "device tag",
* "timestamp": 1481673156548,
* "tagIds": []
* }
* ]
* }
* </pre>
*
* @param uriInfo the URI info which contains the query params
* @return the response
*/
@GET
@Path("/tags")
@Timed
public Response listTags(@Context UriInfo uriInfo) {
List<QueryParam> queryParams = new ArrayList<>();
MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
Collection<Tag> tags;
if (params.isEmpty()) {
tags = tagService.listTags();
} else {
queryParams = WSUtils.buildQueryParameters(params);
tags = tagService.listTags(queryParams);
}
if (tags != null)
return WSUtils.respondEntities(makeTagDto(tags), OK);
throw EntityNotFoundException.byFilter(queryParams.toString());
}
/**
* <p>
* Gets a specific tag by Id. For example,
* </p>
* <b>GET /api/v1/catalog/tags/1</b>
* <pre>
* {
* "id": 2,
* "name": "device",
* "description": "device tag",
* "timestamp": 1481673156548,
* "tagIds": []
* }
* </pre>
*
* @param tagId the tag id
* @return the response
*/
@GET
@Path("/tags/{id}")
@Timed
public Response getTagById(@PathParam("id") Long tagId) {
Tag result = tagService.getTag(tagId);
if (result != null) {
return WSUtils.respondEntity(makeTagDto(result), OK);
}
throw EntityNotFoundException.byId(tagId.toString());
}
/**
* <p>
* Creates a tag in the system. For example,
* </p>
* <b>POST /api/v1/catalog/tags</b>
* <pre>
* {
* "name": "device",
* "description": "device tag"
* }
* </pre>
* <i>Sample success response: </i>
* <pre>
* {
* "id": 2,
* "name": "device",
* "description": "device tag",
* "timestamp": 1481673156548,
* "tagIds": []
* }
* </pre>
* <p>
* Tags can be optionally nested. To create a nested tag, specify a parent tag id
* while creating the tag. Parent tags can also be added later via the update api (PUT).
* For example a thermostat tag can be added as a child of device tag as follows.
* </p>
* <p>
* <b>POST /api/v1/catalog/tags</b>
* <pre>
* {
* "name": "thermostat",
* "description": "thermostat device",
* "tagIds": [1]
* }
* </pre>
* <i>Response:</i>
* <pre>
* {
* "id": 3,
* "name": "thermostat",
* "description": "thermostat device",
* "timestamp": 1481673224536,
* "tagIds": [
* 2
* ]
* }
* </pre>
*
* @param tagDto the tag object to be created
* @return the response
*/
@POST
@Path("/tags")
@Timed
public Response addTag(TagDto tagDto) {
Tag createdTag = tagService.addTag(makeTag(tagDto));
return WSUtils.respondEntity(makeTagDto(createdTag), CREATED);
}
/**
* <p>
* Removes a tag from the system. Tags cannot be removed if it has any associated entities.
* </p>
* <b>DELETE /api/v1/catalog/tags/3</b>
* <pre>
* {
* "id": 3,
* "name": "thermostat",
* "description": "thermostat device",
* "timestamp": 1481673224536,
* "tagIds": [
* 2
* ]
* }
* </pre>
* <p>
* Trying to delete a tag that has some child entities would result in an error. A tag should be
* first removed from all the entities (including child tags) before it can be deleted.
* </p>
* <b>DELETE /api/v1/catalog/tags/1</b>
* <pre>
* {
* "responseMessage": "An exception with message [Tag not empty, has child entities.] was thrown while processing request."
* }
* </pre>
*
* @param tagId the id of the tag to be deleted
* @return the response
*/
@DELETE
@Path("/tags/{id}")
@Timed
public Response removeTag(@PathParam("id") Long tagId) {
Tag removedTag = tagService.removeTag(tagId);
if (removedTag != null) {
return WSUtils.respondEntity(makeTagDto(removedTag), OK);
}
throw EntityNotFoundException.byId(removedTag.toString());
}
/**
* <p>Updates a tag in the system.</p>
* <p>
* <b>PUT /api/v1/catalog/tags/1</b>
* <pre>
* {
* "name": "device",
* "description": "updated device tag"
* }
* </pre>
* <i>Sample success response: </i>
* <pre>
* {
* "id": 2,
* "name": "device",
* "description": "updated device tag",
* "timestamp": 1481673558385,
* "tagIds": []
* }
* </pre>
*
* @param tagId the id of the tag to be updated
* @param tagDto the updated tag object
* @return the response
*/
@PUT
@Path("/tags/{id}")
@Timed
public Response addOrUpdateTag(@PathParam("id") Long tagId, TagDto tagDto) {
Tag newTag = tagService.addOrUpdateTag(tagId, makeTag(tagDto));
return WSUtils.respondEntity(makeTagDto(newTag), OK);
}
/**
* <p>
* Tags the entity with the given tag. For example,
* </p>
* <b>POST /api/v1/catalog/tags/:TAG_ID/entities/:NAME_SPACE/:ENTITY_ID</b>
* <i>Sample success response: </i>
* <pre>
* {
* "responseMessage": "Success",
* }
* </pre>
*
* @param tagId
* @param namespace
* @param entityId
* @return
*/
@POST
@Path("/tags/{id}/entities/{namespace}/{entity-id}")
@Timed
public Response addTagForEntity(@PathParam("id") Long tagId, @PathParam("namespace") String namespace, @PathParam("entity-id") Long entityId) {
Tag tag = tagService.getTag(tagId);
if(tag != null) {
tagService.addTagsForStorable(new TaggedEntity(namespace, entityId), Collections.singletonList(tag));
return WSUtils.respondEntity(SUCCESS_MESSAGE_ENTITY, CREATED);
}
throw EntityNotFoundException.byId(tagId.toString());
}
/**
* <p>
* Removes the given tag from the entity.
* </p>
* <b>DELETE /api/v1/catalog/tags/:TAG_ID/entities/:NAME_SPACE/:ENTITY_ID</b>
* <pre>
* {
* "responseMessage": "Success",
* }
* </pre>
* <p>
*
* @param tagId
* @param namespace
* @param entityId
* @return
*/
@DELETE
@Path("/tags/{id}/entities/{namespace}/{entity-id}")
@Timed
public Response removeTagFromEntity(@PathParam("id") Long tagId, @PathParam("namespace") String namespace, @PathParam("entity-id") Long entityId) {
Tag tag = tagService.getTag(tagId);
if(tag !=null ) {
tagService.removeTagsFromStorable(new TaggedEntity(namespace, entityId), Collections.singletonList(tag));
return WSUtils.respondEntity(SUCCESS_MESSAGE_ENTITY, CREATED);
}
throw EntityNotFoundException.byId(tagId.toString());
}
/**
* <p>
* Gets all the entities tagged with the given tag id. This also recursively gets
* all the entities belonging to any child tags of the given tag. The child tags itself
* are not included in the result.
* </p>
* <b>GET /api/v1/catalog/tags/1/entities</b>
* <pre>
* {
* "entities":[{
* "id":12,
* "namespace":"tag"
* }]
* }
* </pre>
*
* @param tagId the tag id
* @return the response
*/
@GET
@Path("/tags/{id}/entities")
@Timed
public Response getTaggedEntities(@PathParam("id") Long tagId) {
List<TaggedEntity> result = tagService.getEntities(tagId, true);
if (result != null) {
return WSUtils.respondEntities(result, OK);
}
throw EntityNotFoundException.byId(tagId.toString());
}
/**
* <p>
* Gets all associated tags for a given entity id.
*
* </p>
* <b>GET /api/v1/catalog//taggedentities/{namespace}/{id}/tags</b>
* <pre>
* {
* "entities":[{
* "id": 1,
* "name": "device",
* "description": "updated device tag",
* "timestamp": 1462870576965,
* "tagIds": []
* }]
* }
* </pre>
*
* @return the response
*/
@GET
@Path("/taggedentities/{namespace}/{id}/tags")
@Timed
public Response getTagsForEntity(@PathParam("namespace") String namespace, @PathParam("id") Long entityId) {
List<Tag> tags = tagService.getTags(new TaggedEntity(namespace, entityId));
if (tags != null) {
return WSUtils.respondEntities(makeTagDto(tags), OK);
}
throw EntityNotFoundException.byId(entityId.toString());
}
private Collection<TagDto> makeTagDto(Collection<Tag> tags) {
List<TagDto> tagDtos = new ArrayList<>();
for (Tag tag : tags) {
tagDtos.add(makeTagDto(tag));
}
return tagDtos;
}
private TagDto makeTagDto(Tag newTag) {
return new TagDto(newTag);
}
private Tag makeTag(TagDto tagDto) {
List<Tag> parentTags = new ArrayList<>();
if (tagDto.getTagIds() != null) {
for (Long tagId : tagDto.getTagIds()) {
Tag tag = tagService.getTag(tagId);
if (tag == null) {
throw new IllegalArgumentException("Tag with id " + tagId + " does not exist.");
}
parentTags.add(tag);
}
}
Tag tag = new Tag();
tag.setId(tagDto.getId());
tag.setName(tagDto.getName());
tag.setDescription(tagDto.getDescription());
tag.setTimestamp(tagDto.getTimestamp());
tag.setTags(parentTags);
return tag;
}
}