/** * 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.persistence; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Locale; import javax.annotation.security.RolesAllowed; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; 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.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 org.eclipse.smarthome.core.auth.Role; import org.eclipse.smarthome.core.items.Item; import org.eclipse.smarthome.core.items.ItemNotFoundException; import org.eclipse.smarthome.core.items.ItemRegistry; import org.eclipse.smarthome.core.library.types.DateTimeType; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.OpenClosedType; import org.eclipse.smarthome.core.persistence.FilterCriteria; import org.eclipse.smarthome.core.persistence.FilterCriteria.Ordering; import org.eclipse.smarthome.core.persistence.HistoricItem; import org.eclipse.smarthome.core.persistence.ModifiablePersistenceService; import org.eclipse.smarthome.core.persistence.PersistenceService; import org.eclipse.smarthome.core.persistence.PersistenceServiceRegistry; import org.eclipse.smarthome.core.persistence.QueryablePersistenceService; import org.eclipse.smarthome.core.persistence.dto.ItemHistoryDTO; import org.eclipse.smarthome.core.persistence.dto.PersistenceServiceDTO; 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; /** * This class acts as a REST resource for history data and provides different methods to interact with the persistence * store * * @author Chris Jackson - Initial Contribution and add support for ModifiablePersistenceService * @author Kai Kreuzer - Refactored to use PersistenceServiceRegistryImpl * @author Franck Dechavanne - Added DTOs to ApiResponses * */ @Path(PersistenceResource.PATH) @Api(value = PersistenceResource.PATH) public class PersistenceResource implements SatisfiableRESTResource { private final Logger logger = LoggerFactory.getLogger(PersistenceResource.class); private final int MILLISECONDS_PER_DAY = 86400000; private final String MODIFYABLE = "Modifiable"; private final String QUERYABLE = "Queryable"; private final String STANDARD = "Standard"; // The URI path to this resource public static final String PATH = "persistence"; private ItemRegistry itemRegistry; private PersistenceServiceRegistry persistenceServiceRegistry; protected void setPersistenceServiceRegistry(PersistenceServiceRegistry persistenceServiceRegistry) { this.persistenceServiceRegistry = persistenceServiceRegistry; } protected void unsetPersistenceServiceRegistry(PersistenceServiceRegistry persistenceServiceRegistry) { this.persistenceServiceRegistry = null; } protected void setItemRegistry(ItemRegistry itemRegistry) { this.itemRegistry = itemRegistry; } protected void unsetItemRegistry(ItemRegistry itemRegistry) { this.itemRegistry = null; } @GET @RolesAllowed({ Role.ADMIN }) @Produces({ MediaType.APPLICATION_JSON }) @ApiOperation(value = "Gets a list of persistence services.", response = String.class, responseContainer = "List") @ApiResponses(value = @ApiResponse(code = 200, message = "OK", response = String.class, responseContainer = "List")) public Response httpGetPersistenceServices(@Context HttpHeaders headers, @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @ApiParam(value = HttpHeaders.ACCEPT_LANGUAGE) String language) { Locale locale = LocaleUtil.getLocale(language); Object responseObject = getPersistenceServiceList(locale); return Response.ok(responseObject).build(); } @GET @RolesAllowed({ Role.ADMIN }) @Path("/items") @Produces({ MediaType.APPLICATION_JSON }) @ApiOperation(value = "Gets a list of items available via a specific persistence service.", response = String.class, responseContainer = "List") @ApiResponses(value = @ApiResponse(code = 200, message = "OK", response = String.class, responseContainer = "List")) public Response httpGetPersistenceServiceItems(@Context HttpHeaders headers, @ApiParam(value = "Id of the persistence service. If not provided the default service will be used", required = false) @QueryParam("serviceId") String serviceId) { return getServiceItemList(serviceId); } @GET @RolesAllowed({ Role.USER, Role.ADMIN }) @Path("/items/{itemname: [a-zA-Z_0-9]*}") @Produces({ MediaType.APPLICATION_JSON }) @ApiOperation(value = "Gets item persistence data from the persistence service.", response = ItemHistoryDTO.class) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = ItemHistoryDTO.class), @ApiResponse(code = 404, message = "Unknown Item or persistence service") }) public Response httpGetPersistenceItemData(@Context HttpHeaders headers, @ApiParam(value = "Id of the persistence service. If not provided the default service will be used", required = false) @QueryParam("serviceId") String serviceId, @ApiParam(value = "The item name", required = true) @PathParam("itemname") String itemName, @ApiParam(value = "Start time of the data to return. Will default to 1 day before endtime. [" + DateTimeType.DATE_PATTERN_WITH_TZ_AND_MS + "]", required = false) @QueryParam("starttime") String startTime, @ApiParam(value = "End time of the data to return. Will default to current time. [" + DateTimeType.DATE_PATTERN_WITH_TZ_AND_MS + "]", required = false) @QueryParam("endtime") String endTime, @ApiParam(value = "Page number of data to return. This parameter will enable paging.", required = false) @QueryParam("page") int pageNumber, @ApiParam(value = "The length of each page.", required = false) @QueryParam("pagelength") int pageLength, @ApiParam(value = "Gets one value before and after the requested period.", required = false) @QueryParam("boundary") boolean boundary) { return getItemHistoryDTO(serviceId, itemName, startTime, endTime, pageNumber, pageLength, boundary); } @DELETE @RolesAllowed({ Role.ADMIN }) @Path("/items/{itemname: [a-zA-Z_0-9]*}") @Produces({ MediaType.APPLICATION_JSON }) @ApiOperation(value = "Delete item data from a specific persistence service.", response = String.class, responseContainer = "List") @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = String.class, responseContainer = "List"), @ApiResponse(code = 400, message = "Invalid filter parameters"), @ApiResponse(code = 404, message = "Unknown persistence service") }) public Response httpDeletePersistenceServiceItem(@Context HttpHeaders headers, @ApiParam(value = "Id of the persistence service.", required = true) @QueryParam("serviceId") String serviceId, @ApiParam(value = "The item name.", required = true) @PathParam("itemname") String itemName, @ApiParam(value = "Start time of the data to return. [" + DateTimeType.DATE_PATTERN_WITH_TZ_AND_MS + "]", required = true) @QueryParam("starttime") String startTime, @ApiParam(value = "End time of the data to return. [" + DateTimeType.DATE_PATTERN_WITH_TZ_AND_MS + "]", required = true) @QueryParam("endtime") String endTime) { return deletePersistenceItemData(serviceId, itemName, startTime, endTime); } @PUT @RolesAllowed({ Role.ADMIN }) @Path("/items/{itemname: [a-zA-Z_0-9]*}") @Produces({ MediaType.APPLICATION_JSON }) @ApiOperation(value = "Stores item persistence data into the persistence service.", response = ItemHistoryDTO.class) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = ItemHistoryDTO.class), @ApiResponse(code = 404, message = "Unknown Item or persistence service") }) public Response httpPutPersistenceItemData(@Context HttpHeaders headers, @ApiParam(value = "Id of the persistence service. If not provided the default service will be used", required = false) @QueryParam("serviceId") String serviceId, @ApiParam(value = "The item name.", required = true) @PathParam("itemname") String itemName, @ApiParam(value = "Time of the data to be stored. Will default to current time. [" + DateTimeType.DATE_PATTERN_WITH_TZ_AND_MS + "]", required = true) @QueryParam("time") String time, @ApiParam(value = "The state to store.", required = true) @QueryParam("state") String value) { return putItemState(serviceId, itemName, value, time); } private Date convertTime(String sTime) { DateTimeType dateTime = new DateTimeType(sTime); return dateTime.getCalendar().getTime(); } private Response getItemHistoryDTO(String serviceId, String itemName, String timeBegin, String timeEnd, int pageNumber, int pageLength, boolean boundary) { // Benchmarking timer... long timerStart = System.currentTimeMillis(); // If serviceId is null, then use the default service PersistenceService service = null; if (serviceId == null) { serviceId = persistenceServiceRegistry.getDefaultId(); } service = persistenceServiceRegistry.get(serviceId); if (service == null) { logger.debug("Persistence service not found '{}'.", serviceId); return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Persistence service not found: " + serviceId); } if (!(service instanceof QueryablePersistenceService)) { logger.debug("Persistence service not queryable '{}'.", serviceId); return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Persistence service not queryable: " + serviceId); } QueryablePersistenceService qService = (QueryablePersistenceService) service; Date dateTimeBegin = new Date(); Date dateTimeEnd = dateTimeBegin; if (timeBegin != null) { dateTimeBegin = convertTime(timeBegin); } if (timeEnd != null) { dateTimeEnd = convertTime(timeEnd); } // End now... if (dateTimeEnd.getTime() == 0) { dateTimeEnd = new Date(); } if (dateTimeBegin.getTime() == 0) { dateTimeBegin = new Date(dateTimeEnd.getTime() - MILLISECONDS_PER_DAY); } // Default to 1 days data if the times are the same or the start time is newer than the end time if (dateTimeBegin.getTime() >= dateTimeEnd.getTime()) { dateTimeBegin = new Date(dateTimeEnd.getTime() - MILLISECONDS_PER_DAY); } FilterCriteria filter; Iterable<HistoricItem> result; State state = null; Long quantity = 0l; ItemHistoryDTO dto = new ItemHistoryDTO(); dto.name = itemName; filter = new FilterCriteria(); filter.setItemName(itemName); // If "boundary" is true then we want to get one value before and after the requested period // This is necessary for values that don't change often otherwise data will start after the start of the graph // (or not at all if there's no change during the graph period) if (boundary) { // Get the value before the start time. filter.setEndDate(dateTimeBegin); filter.setPageSize(1); filter.setOrdering(Ordering.DESCENDING); result = qService.query(filter); if (result != null && result.iterator().hasNext()) { dto.addData(dateTimeBegin.getTime(), result.iterator().next().getState()); quantity++; } } if (pageLength == 0) { filter.setPageNumber(0); filter.setPageSize(Integer.MAX_VALUE); } else { filter.setPageNumber(pageNumber); filter.setPageSize(pageLength); } filter.setBeginDate(dateTimeBegin); filter.setEndDate(dateTimeEnd); filter.setOrdering(Ordering.ASCENDING); result = qService.query(filter); if (result != null) { Iterator<HistoricItem> it = result.iterator(); // Iterate through the data while (it.hasNext()) { HistoricItem historicItem = it.next(); state = historicItem.getState(); // For 'binary' states, we need to replicate the data // to avoid diagonal lines if (state instanceof OnOffType || state instanceof OpenClosedType) { dto.addData(historicItem.getTimestamp().getTime(), state); } dto.addData(historicItem.getTimestamp().getTime(), state); quantity++; } } if (boundary) { // Get the value after the end time. filter.setBeginDate(dateTimeEnd); filter.setPageSize(1); filter.setOrdering(Ordering.ASCENDING); result = qService.query(filter); if (result != null && result.iterator().hasNext()) { dto.addData(dateTimeEnd.getTime(), result.iterator().next().getState()); quantity++; } } dto.datapoints = Long.toString(quantity); logger.debug("Persistence returned {} rows in {}ms", dto.datapoints, System.currentTimeMillis() - timerStart); return JSONResponse.createResponse(Status.OK, dto, ""); } /** * Gets a list of persistence services currently configured in the system * * @return list of persistence services as {@link ServiceBean} */ private List<PersistenceServiceDTO> getPersistenceServiceList(Locale locale) { List<PersistenceServiceDTO> dtoList = new ArrayList<PersistenceServiceDTO>(); for (PersistenceService service : persistenceServiceRegistry.getAll()) { PersistenceServiceDTO serviceDTO = new PersistenceServiceDTO(); serviceDTO.id = service.getId(); serviceDTO.label = service.getLabel(locale); if (service instanceof ModifiablePersistenceService) { serviceDTO.type = MODIFYABLE; } else if (service instanceof QueryablePersistenceService) { serviceDTO.type = QUERYABLE; } else { serviceDTO.type = STANDARD; } dtoList.add(serviceDTO); } return dtoList; } private Response getServiceItemList(String serviceId) { // If serviceId is null, then use the default service PersistenceService service = null; if (serviceId == null) { service = persistenceServiceRegistry.getDefault(); } else { service = persistenceServiceRegistry.get(serviceId); } if (service == null) { logger.debug("Persistence service not found '{}'.", serviceId); return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Persistence service not found: " + serviceId); } if (!(service instanceof QueryablePersistenceService)) { logger.debug("Persistence service not queryable '{}'.", serviceId); return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Persistence service not queryable: " + serviceId); } QueryablePersistenceService qService = (QueryablePersistenceService) service; return JSONResponse.createResponse(Status.OK, qService.getItemInfo(), ""); } private Response deletePersistenceItemData(String serviceId, String itemName, String timeBegin, String timeEnd) { // For deleting, we must specify a service id - don't use the default service if (serviceId == null || serviceId.length() == 0) { logger.debug("Persistence service must be specified for delete operations."); return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Persistence service must be specified for delete operations."); } PersistenceService service = persistenceServiceRegistry.get(serviceId); if (service == null) { logger.debug("Persistence service not found '{}'.", serviceId); return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Persistence service not found: " + serviceId); } if (!(service instanceof ModifiablePersistenceService)) { logger.warn("Persistence service not modifiable '{}'.", serviceId); return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Persistence service not modifiable: " + serviceId); } ModifiablePersistenceService mService = (ModifiablePersistenceService) service; if (timeBegin == null | timeEnd == null) { return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "The start and end time must be set"); } Date dateTimeBegin = convertTime(timeBegin); Date dateTimeEnd = convertTime(timeEnd); if (dateTimeEnd.before(dateTimeBegin)) { return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Start time must be earlier than end time"); } FilterCriteria filter; // First, get the value at the start time. // This is necessary for values that don't change often otherwise data will start after the start of the graph // (or not at all if there's no change during the graph period) filter = new FilterCriteria(); filter.setBeginDate(dateTimeBegin); filter.setEndDate(dateTimeEnd); filter.setItemName(itemName); try { mService.remove(filter); } catch (IllegalArgumentException e) { return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Invalid filter parameters."); } return Response.status(Status.OK).build(); } private Response putItemState(String serviceId, String itemName, String value, String time) { // If serviceId is null, then use the default service PersistenceService service = null; if (serviceId == null) { serviceId = persistenceServiceRegistry.getDefaultId(); } service = persistenceServiceRegistry.get(serviceId); if (service == null) { logger.warn("Persistence service not found '{}'.", serviceId); return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Persistence service not found: " + serviceId); } Item item; try { if (itemRegistry == null) { logger.warn("Item registry not set."); return JSONResponse.createErrorResponse(Status.CONFLICT, "Item registry not set."); } item = itemRegistry.getItem(itemName); } catch (ItemNotFoundException e) { logger.warn("Item not found '{}'.", itemName); return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Item not found: " + itemName); } // Try to parse a State from the input State state = TypeParser.parseState(item.getAcceptedDataTypes(), value); if (state == null) { // State could not be parsed logger.warn("Can't persist item {} with invalid state '{}'.", itemName, value); return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "State could not be parsed: " + value); } Date dateTime = null; if (time != null && time.length() != 0) { dateTime = convertTime(time); } if (dateTime == null || dateTime.getTime() == 0) { logger.warn("Error with persistence store to {}. Time badly formatted {}.", itemName, time); return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Time badly formatted."); } if (!(service instanceof ModifiablePersistenceService)) { logger.warn("Persistence service not modifiable '{}'.", serviceId); return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Persistence service not modifiable: " + serviceId); } ModifiablePersistenceService mService = (ModifiablePersistenceService) service; mService.store(item, dateTime, state); return Response.status(Status.OK).build(); } @Override public boolean isSatisfied() { return itemRegistry != null && persistenceServiceRegistry != null; } }