/** * Copyright (c) 2014-2017 by the respective copyright holders. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.smarthome.io.rest.core.item; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Set; import javax.annotation.security.RolesAllowed; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; 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.QueryParam; import javax.ws.rs.WebApplicationException; 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.ResponseBuilder; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriInfo; import org.eclipse.smarthome.core.auth.Role; import org.eclipse.smarthome.core.events.EventPublisher; import org.eclipse.smarthome.core.items.ActiveItem; import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.items.GroupItem; import org.eclipse.smarthome.core.items.Item; import org.eclipse.smarthome.core.items.ItemFactory; import org.eclipse.smarthome.core.items.ItemNotFoundException; import org.eclipse.smarthome.core.items.ItemRegistry; import org.eclipse.smarthome.core.items.ManagedItemProvider; import org.eclipse.smarthome.core.items.dto.GroupItemDTO; import org.eclipse.smarthome.core.items.dto.ItemDTOMapper; import org.eclipse.smarthome.core.items.events.ItemEventFactory; import org.eclipse.smarthome.core.library.items.RollershutterItem; import org.eclipse.smarthome.core.library.items.SwitchItem; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.UpDownType; import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.State; import org.eclipse.smarthome.core.types.TypeParser; import org.eclipse.smarthome.io.rest.JSONResponse; import org.eclipse.smarthome.io.rest.LocaleUtil; import org.eclipse.smarthome.io.rest.SatisfiableRESTResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; /** * <p> * This class acts as a REST resource for items and provides different methods to interact with them, like retrieving * lists of items, sending commands to them or checking a single status. * </p> * * <p> * The typical content types are plain text for status values and XML or JSON(P) for more complex data structures * </p> * * <p> * This resource is registered with the Jersey servlet. * </p> * * @author Kai Kreuzer - Initial contribution and API * @author Dennis Nobel - Added methods for item management * @author Andre Fuechsel - Added tag support * @author Chris Jackson - Added method to write complete item bean * @author Stefan Bußweiler - Migration to new ESH event concept * @author Yordan Zhelev - Added Swagger annotations * @author Jörg Plewe - refactoring, error handling * @author Franck Dechavanne - Added DTOs to ApiResponses */ @Path(ItemResource.PATH_ITEMS) @Api(value = ItemResource.PATH_ITEMS) public class ItemResource implements SatisfiableRESTResource { private final Logger logger = LoggerFactory.getLogger(ItemResource.class); /** The URI path to this resource */ public static final String PATH_ITEMS = "items"; @Context UriInfo uriInfo; private ItemRegistry itemRegistry; private EventPublisher eventPublisher; private ManagedItemProvider managedItemProvider; private Set<ItemFactory> itemFactories = new HashSet<>(); protected void setItemRegistry(ItemRegistry itemRegistry) { this.itemRegistry = itemRegistry; } protected void unsetItemRegistry(ItemRegistry itemRegistry) { this.itemRegistry = null; } protected void setEventPublisher(EventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } protected void unsetEventPublisher(EventPublisher eventPublisher) { this.eventPublisher = null; } protected void setManagedItemProvider(ManagedItemProvider managedItemProvider) { this.managedItemProvider = managedItemProvider; } protected void unsetManagedItemProvider(ManagedItemProvider managedItemProvider) { this.managedItemProvider = null; } protected void addItemFactory(ItemFactory itemFactory) { this.itemFactories.add(itemFactory); } protected void removeItemFactory(ItemFactory itemFactory) { this.itemFactories.remove(itemFactory); } @GET @RolesAllowed({ Role.USER, Role.ADMIN }) @Produces(MediaType.APPLICATION_JSON) @ApiOperation(value = "Get all available items.", response = EnrichedItemDTO.class, responseContainer = "List") @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = EnrichedItemDTO.class, responseContainer = "List") }) public Response getItems(@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @ApiParam(value = "language") String language, @QueryParam("type") @ApiParam(value = "item type filter", required = false) String type, @QueryParam("tags") @ApiParam(value = "item tag filter", required = false) String tags, @DefaultValue("false") @QueryParam("recursive") @ApiParam(value = "get member items recursivly", required = false) boolean recursive) { final Locale locale = LocaleUtil.getLocale(language); logger.debug("Received HTTP GET request at '{}'", uriInfo.getPath()); Object responseObject = getItemBeans(type, tags, recursive, locale); return Response.ok(responseObject).build(); } @GET @RolesAllowed({ Role.USER, Role.ADMIN }) @Path("/{itemname: [a-zA-Z_0-9]*}") @Produces({ MediaType.APPLICATION_JSON }) @ApiOperation(value = "Gets a single item.", response = EnrichedItemDTO.class) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = EnrichedItemDTO.class), @ApiResponse(code = 404, message = "Item not found") }) public Response getItemData(@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @ApiParam(value = "language") String language, @PathParam("itemname") @ApiParam(value = "item name", required = true) String itemname) { final Locale locale = LocaleUtil.getLocale(language); logger.debug("Received HTTP GET request at '{}'", uriInfo.getPath()); // get item Item item = getItem(itemname); // if it exists if (item != null) { logger.debug("Received HTTP GET request at '{}'.", uriInfo.getPath()); return getItemResponse(Status.OK, item, locale, null); } else { logger.info("Received HTTP GET request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname); return getItemNotFoundResponse(itemname); } } /** * * @param itemname * @return */ @GET @RolesAllowed({ Role.USER, Role.ADMIN }) @Path("/{itemname: [a-zA-Z_0-9]*}/state") @Produces(MediaType.TEXT_PLAIN) @ApiOperation(value = "Gets the state of an item.") @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = String.class), @ApiResponse(code = 404, message = "Item not found") }) public Response getPlainItemState( @PathParam("itemname") @ApiParam(value = "item name", required = true) String itemname) { // get item Item item = getItem(itemname); // if it exists if (item != null) { logger.debug("Received HTTP GET request at '{}'.", uriInfo.getPath()); // we cannot use JSONResponse.createResponse() bc. MediaType.TEXT_PLAIN // return JSONResponse.createResponse(Status.OK, item.getState().toString(), null); return Response.ok(item.getState().toFullString()).build(); } else { logger.info("Received HTTP GET request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname); return getItemNotFoundResponse(itemname); } } @PUT @RolesAllowed({ Role.USER, Role.ADMIN }) @Path("/{itemname: [a-zA-Z_0-9]*}/state") @Consumes(MediaType.TEXT_PLAIN) @ApiOperation(value = "Updates the state of an item.") @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Item not found"), @ApiResponse(code = 400, message = "Item state null") }) public Response putItemState( @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @ApiParam(value = "language") String language, @PathParam("itemname") @ApiParam(value = "item name", required = true) String itemname, @ApiParam(value = "valid item state (e.g. ON, OFF)", required = true) String value) { final Locale locale = LocaleUtil.getLocale(language); // get Item Item item = getItem(itemname); // if Item exists if (item != null) { // try to parse a State from the input State state = TypeParser.parseState(item.getAcceptedDataTypes(), value); if (state != null) { // set State and report OK logger.debug("Received HTTP PUT request at '{}' with value '{}'.", uriInfo.getPath(), value); eventPublisher.post(ItemEventFactory.createStateEvent(itemname, state)); return getItemResponse(Status.ACCEPTED, null, locale, null); } else { // State could not be parsed logger.warn("Received HTTP PUT request at '{}' with an invalid status value '{}'.", uriInfo.getPath(), value); return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "State could not be parsed: " + value); } } else { // Item does not exist logger.info("Received HTTP PUT request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname); return getItemNotFoundResponse(itemname); } } @POST @RolesAllowed({ Role.USER, Role.ADMIN }) @Path("/{itemname: [a-zA-Z_0-9]*}") @Consumes(MediaType.TEXT_PLAIN) @ApiOperation(value = "Sends a command to an item.") @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Item not found"), @ApiResponse(code = 400, message = "Item command null") }) public Response postItemCommand( @PathParam("itemname") @ApiParam(value = "item name", required = true) String itemname, @ApiParam(value = "valid item command (e.g. ON, OFF, UP, DOWN, REFRESH)", required = true) String value) { Item item = getItem(itemname); Command command = null; if (item != null) { if ("toggle".equalsIgnoreCase(value) && (item instanceof SwitchItem || item instanceof RollershutterItem)) { if (OnOffType.ON.equals(item.getStateAs(OnOffType.class))) { command = OnOffType.OFF; } if (OnOffType.OFF.equals(item.getStateAs(OnOffType.class))) { command = OnOffType.ON; } if (UpDownType.UP.equals(item.getStateAs(UpDownType.class))) { command = UpDownType.DOWN; } if (UpDownType.DOWN.equals(item.getStateAs(UpDownType.class))) { command = UpDownType.UP; } } else { command = TypeParser.parseCommand(item.getAcceptedCommandTypes(), value); } if (command != null) { logger.debug("Received HTTP POST request at '{}' with value '{}'.", uriInfo.getPath(), value); eventPublisher.post(ItemEventFactory.createCommandEvent(itemname, command)); ResponseBuilder resbuilder = Response.ok(); resbuilder.type(MediaType.TEXT_PLAIN); return resbuilder.build(); } else { logger.warn("Received HTTP POST request at '{}' with an invalid status value '{}'.", uriInfo.getPath(), value); return Response.status(Status.BAD_REQUEST).build(); } } else { logger.info("Received HTTP POST request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname); throw new WebApplicationException(404); } } @PUT @RolesAllowed({ Role.ADMIN }) @Path("/{itemName: [a-zA-Z_0-9]*}/members/{memberItemName: [a-zA-Z_0-9]*}") @ApiOperation(value = "Adds a new member to a group item.") @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Item or member item not found or item is not of type group item."), @ApiResponse(code = 405, message = "Member item is not editable.") }) public Response addMember(@PathParam("itemName") @ApiParam(value = "item name", required = true) String itemName, @PathParam("memberItemName") @ApiParam(value = "member item name", required = true) String memberItemName) { try { Item item = itemRegistry.getItem(itemName); if (!(item instanceof GroupItem)) { return Response.status(Status.NOT_FOUND).build(); } GroupItem groupItem = (GroupItem) item; Item memberItem = itemRegistry.getItem(memberItemName); if (!(memberItem instanceof GenericItem)) { return Response.status(Status.NOT_FOUND).build(); } if (managedItemProvider.get(memberItemName) == null) { return Response.status(Status.METHOD_NOT_ALLOWED).build(); } GenericItem genericMemberItem = (GenericItem) memberItem; genericMemberItem.addGroupName(groupItem.getName()); managedItemProvider.update(genericMemberItem); return Response.ok().build(); } catch (ItemNotFoundException e) { return Response.status(Status.NOT_FOUND).build(); } } @DELETE @RolesAllowed({ Role.ADMIN }) @Path("/{itemName: [a-zA-Z_0-9]*}/members/{memberItemName: [a-zA-Z_0-9]*}") @ApiOperation(value = "Removes an existing member from a group item.") @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Item or member item not found or item is not of type group item."), @ApiResponse(code = 405, message = "Member item is not editable.") }) public Response removeMember(@PathParam("itemName") @ApiParam(value = "item name", required = true) String itemName, @PathParam("memberItemName") @ApiParam(value = "member item name", required = true) String memberItemName) { try { Item item = itemRegistry.getItem(itemName); if (!(item instanceof GroupItem)) { return Response.status(Status.NOT_FOUND).build(); } GroupItem groupItem = (GroupItem) item; Item memberItem = itemRegistry.getItem(memberItemName); if (!(memberItem instanceof GenericItem)) { return Response.status(Status.NOT_FOUND).build(); } if (managedItemProvider.get(memberItemName) == null) { return Response.status(Status.METHOD_NOT_ALLOWED).build(); } GenericItem genericMemberItem = (GenericItem) memberItem; genericMemberItem.removeGroupName(groupItem.getName()); managedItemProvider.update(genericMemberItem); return Response.ok().build(); } catch (ItemNotFoundException e) { return Response.status(Status.NOT_FOUND).build(); } } @DELETE @RolesAllowed({ Role.ADMIN }) @Path("/{itemname: [a-zA-Z_0-9]*}") @ApiOperation(value = "Removes an item from the registry.") @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Item not found or item is not editable.") }) public Response removeItem(@PathParam("itemname") @ApiParam(value = "item name", required = true) String itemname) { if (managedItemProvider.remove(itemname) == null) { logger.info("Received HTTP DELETE request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname); return Response.status(Status.NOT_FOUND).build(); } return Response.ok().build(); } @PUT @RolesAllowed({ Role.ADMIN }) @Path("/{itemname: [a-zA-Z_0-9]*}/tags/{tag}") @ApiOperation(value = "Adds a tag to an item.") @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Item not found."), @ApiResponse(code = 405, message = "Item not editable.") }) public Response addTag(@PathParam("itemname") @ApiParam(value = "item name", required = true) String itemname, @PathParam("tag") @ApiParam(value = "tag", required = true) String tag) { Item item = getItem(itemname); if (item == null) { logger.info("Received HTTP PUT request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname); return Response.status(Status.NOT_FOUND).build(); } if (managedItemProvider.get(itemname) == null) { return Response.status(Status.METHOD_NOT_ALLOWED).build(); } ((ActiveItem) item).addTag(tag); managedItemProvider.update(item); return Response.ok().build(); } @DELETE @RolesAllowed({ Role.ADMIN }) @Path("/{itemname: [a-zA-Z_0-9]*}/tags/{tag}") @ApiOperation(value = "Removes a tag from an item.") @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 404, message = "Item not found."), @ApiResponse(code = 405, message = "Item not editable.") }) public Response removeTag(@PathParam("itemname") @ApiParam(value = "item name", required = true) String itemname, @PathParam("tag") @ApiParam(value = "tag", required = true) String tag) { Item item = getItem(itemname); if (item == null) { logger.info("Received HTTP DELETE request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname); return Response.status(Status.NOT_FOUND).build(); } if (managedItemProvider.get(itemname) == null) { return Response.status(Status.METHOD_NOT_ALLOWED).build(); } ((ActiveItem) item).removeTag(tag); managedItemProvider.update(item); return Response.ok().build(); } /** * Create or Update an item by supplying an item bean. * * @param itemname * @param item the item bean. * @return */ @PUT @RolesAllowed({ Role.ADMIN }) @Path("/{itemname: [a-zA-Z_0-9]*}") @Consumes(MediaType.APPLICATION_JSON) @ApiOperation(value = "Adds a new item to the registry or updates the existing item.") @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = String.class), @ApiResponse(code = 201, message = "Item created."), @ApiResponse(code = 400, message = "Item null."), @ApiResponse(code = 404, message = "Item not found."), @ApiResponse(code = 405, message = "Item not editable.") }) public Response createOrUpdateItem( @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @ApiParam(value = "language") String language, @PathParam("itemname") @ApiParam(value = "item name", required = true) String itemname, @ApiParam(value = "item data", required = true) GroupItemDTO item) { final Locale locale = LocaleUtil.getLocale(language); // If we didn't get an item bean, then return! if (item == null) { return Response.status(Status.BAD_REQUEST).build(); } ActiveItem newItem = ItemDTOMapper.map(item, itemFactories); if (newItem == null) { logger.warn("Received HTTP PUT request at '{}' with an invalid item type '{}'.", uriInfo.getPath(), item.type); return Response.status(Status.BAD_REQUEST).build(); } // Update the label newItem.setLabel(item.label); if (item.category != null) { newItem.setCategory(item.category); } if (item.groupNames != null) { newItem.addGroupNames(item.groupNames); } if (item.tags != null) { newItem.addTags(item.tags); } // Save the item if (getItem(itemname) == null) { // item does not yet exist, create it managedItemProvider.add(newItem); return getItemResponse(Status.CREATED, newItem, locale, null); } else if (managedItemProvider.get(itemname) != null) { // item already exists as a managed item, update it managedItemProvider.update(newItem); return getItemResponse(Status.OK, newItem, locale, null); } else { // Item exists but cannot be updated logger.warn("Cannot update existing item '{}', because is not managed.", itemname); return JSONResponse.createErrorResponse(Status.METHOD_NOT_ALLOWED, "Cannot update non-managed Item " + itemname); } } /** * helper: Response to be sent to client if a Thing cannot be found * * @param thingUID * @return Response configured for 'item not found' */ private static Response getItemNotFoundResponse(String itemname) { String message = "Item " + itemname + " does not exist!"; return JSONResponse.createResponse(Status.NOT_FOUND, null, message); } /** * Prepare a response representing the Item depending in the status. * * @param status * @param item can be null * @param locale the locale * @param errormessage optional message in case of error * @return Response configured to represent the Item in depending on the status */ private Response getItemResponse(Status status, Item item, Locale locale, String errormessage) { Object entity = null != item ? EnrichedItemDTOMapper.map(item, true, uriInfo.getBaseUri(), locale) : null; return JSONResponse.createResponse(status, entity, errormessage); } /** * convenience shortcut * * @param itemname * @return Item addressed by itemname */ private Item getItem(String itemname) { Item item = itemRegistry.get(itemname); return item; } private List<EnrichedItemDTO> getItemBeans(String type, String tags, boolean recursive, Locale locale) { List<EnrichedItemDTO> beans = new LinkedList<>(); Collection<Item> items; if (tags == null) { if (type == null) { items = itemRegistry.getItems(); } else { items = itemRegistry.getItemsOfType(type); } } else { String[] tagList = tags.split(","); if (type == null) { items = itemRegistry.getItemsByTag(tagList); } else { items = itemRegistry.getItemsByTagAndType(type, tagList); } } if (items != null) { for (Item item : items) { beans.add(EnrichedItemDTOMapper.map(item, recursive, uriInfo.getBaseUri(), locale)); } } return beans; } @Override public boolean isSatisfied() { return itemRegistry != null && managedItemProvider != null && eventPublisher != null && !itemFactories.isEmpty(); } }