package com.devicehive.resource.impl; /* * #%L * DeviceHive Frontend Logic * %% * Copyright (C) 2016 DataArt * %% * 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. * #L% */ import com.devicehive.auth.HivePrincipal; import com.devicehive.configuration.Messages; import com.devicehive.json.strategies.JsonPolicyDef; import com.devicehive.json.strategies.JsonPolicyDef.Policy; import com.devicehive.model.DeviceCommand; import com.devicehive.model.ErrorResponse; import com.devicehive.model.wrappers.DeviceCommandWrapper; import com.devicehive.resource.DeviceCommandResource; import com.devicehive.resource.converters.TimestampQueryParamParser; import com.devicehive.resource.util.CommandResponseFilterAndSort; import com.devicehive.resource.util.ResponseFactory; import com.devicehive.service.DeviceCommandService; import com.devicehive.service.DeviceService; import com.devicehive.service.time.TimestampService; import com.devicehive.vo.DeviceVO; import com.devicehive.vo.UserVO; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import javax.ws.rs.container.AsyncResponse; import javax.ws.rs.container.CompletionCallback; import javax.ws.rs.container.Suspended; import javax.ws.rs.core.Response; import java.util.*; import java.util.concurrent.*; import java.util.function.BiConsumer; import java.util.stream.Collectors; import static javax.ws.rs.core.Response.Status.*; /** * {@inheritDoc} */ @Service public class DeviceCommandResourceImpl implements DeviceCommandResource { private static final Logger LOGGER = LoggerFactory.getLogger(DeviceCommandResourceImpl.class); @Autowired private DeviceCommandService commandService; @Autowired private DeviceService deviceService; @Autowired private TimestampService timestampService; /** * {@inheritDoc} */ @Override public void poll(final String deviceGuid, final String namesString, final String timestamp, final long timeout, final AsyncResponse asyncResponse) throws Exception { poll(timeout, deviceGuid, namesString, timestamp, asyncResponse); } @Override public void pollMany(final String deviceGuidsString, final String namesString, final String timestamp, final long timeout, final AsyncResponse asyncResponse) throws Exception { poll(timeout, deviceGuidsString, namesString, timestamp, asyncResponse); } private void poll(final long timeout, final String deviceGuidsCsv, final String namesCsv, final String timestamp, final AsyncResponse asyncResponse) throws InterruptedException { final HivePrincipal principal = (HivePrincipal) SecurityContextHolder.getContext() .getAuthentication().getPrincipal(); final Date ts = TimestampQueryParamParser.parse(timestamp == null ? timestampService.getDateAsString() : timestamp); final Response response = ResponseFactory.response( Response.Status.OK, Collections.emptyList(), JsonPolicyDef.Policy.COMMAND_LISTED); asyncResponse.setTimeoutHandler(asyncRes -> asyncRes.resume(response)); Set<String> availableDevices; if (deviceGuidsCsv == null) { availableDevices = deviceService.findByGuidWithPermissionsCheck(Collections.emptyList(), principal) .stream() .map(DeviceVO::getGuid) .collect(Collectors.toSet()); } else { availableDevices = Optional.ofNullable(StringUtils.split(deviceGuidsCsv, ',')) .map(Arrays::asList) .map(list -> deviceService.findByGuidWithPermissionsCheck(list, principal)) .map(list -> list.stream().map(DeviceVO::getGuid).collect(Collectors.toSet())) .orElse(Collections.emptySet()); } Set<String> names = Optional.ofNullable(StringUtils.split(namesCsv, ',')) .map(Arrays::asList) .map(list -> list.stream().collect(Collectors.toSet())) .orElse(Collections.emptySet()); BiConsumer<DeviceCommand, String> callback = (command, subscriptionId) -> { if (!asyncResponse.isDone()) { asyncResponse.resume(ResponseFactory.response( Response.Status.OK, Collections.singleton(command), Policy.COMMAND_LISTED)); } }; if (!availableDevices.isEmpty()) { Pair<String, CompletableFuture<List<DeviceCommand>>> pair = commandService .sendSubscribeRequest(availableDevices, names, ts, callback); pair.getRight().thenAccept(collection -> { if (!collection.isEmpty() && !asyncResponse.isDone()) { asyncResponse.resume(ResponseFactory.response( Response.Status.OK, collection, Policy.COMMAND_LISTED)); } if (timeout == 0) { asyncResponse.setTimeout(1, TimeUnit.MILLISECONDS); // setting timeout to 0 would cause // the thread to suspend indefinitely, see AsyncResponse docs } else { asyncResponse.setTimeout(timeout, TimeUnit.SECONDS); } }); asyncResponse.register(new CompletionCallback() { @Override public void onComplete(Throwable throwable) { commandService.sendUnsubscribeRequest(pair.getLeft(), null); } }); } else { if (!asyncResponse.isDone()) { asyncResponse.resume(response); } } } /** * Implementation of <a href="http://www.devicehive.com/restful#Reference/DeviceCommand/wait">DeviceHive RESTful * API: DeviceCommand: wait</a> * * @param timeout Waiting timeout in seconds (default: 30 seconds, maximum: 60 seconds). Specify 0 to disable * waiting. */ @Override public void wait(final String deviceGuid, final String commandId, final long timeout, final AsyncResponse asyncResponse) { LOGGER.debug("DeviceCommand wait requested, deviceId = {}, commandId = {}", deviceGuid, commandId); asyncResponse.setTimeoutHandler(asyncRes -> asyncRes.resume(ResponseFactory.response(Response.Status.NO_CONTENT))); if (deviceGuid == null || commandId == null) { LOGGER.warn("DeviceCommand wait request failed. BAD REQUEST: deviceGuid and commandId required", deviceGuid); asyncResponse.resume(ResponseFactory.response(Response.Status.BAD_REQUEST)); return; } DeviceVO device = deviceService.getDeviceWithNetworkAndDeviceClass(deviceGuid); if (device == null) { LOGGER.warn("DeviceCommand wait request failed. NOT FOUND: device {} not found", deviceGuid); asyncResponse.resume(ResponseFactory.response(Response.Status.NOT_FOUND)); return; } Optional<DeviceCommand> command = commandService.findOne(Long.valueOf(commandId), device.getGuid()).join(); if (!command.isPresent()) { LOGGER.warn("DeviceCommand wait request failed. NOT FOUND: No command found with id = {} for deviceId = {}", commandId, deviceGuid); asyncResponse.resume(ResponseFactory.response(Response.Status.NO_CONTENT)); return; } if (!command.get().getDeviceGuid().equals(device.getGuid())) { LOGGER.warn("DeviceCommand wait request failed. BAD REQUEST: Command with id = {} was not sent for device with guid = {}", commandId, deviceGuid); asyncResponse.resume(ResponseFactory.response(Response.Status.BAD_REQUEST)); return; } BiConsumer<DeviceCommand, String> callback = (com, subscriptionId) -> { if (!asyncResponse.isDone()) { asyncResponse.resume(ResponseFactory.response( Response.Status.OK, com, Policy.COMMAND_TO_DEVICE)); } }; if (!command.get().getIsUpdated()) { CompletableFuture<Pair<String, DeviceCommand>> future = commandService .sendSubscribeToUpdateRequest(Long.valueOf(commandId), deviceGuid, callback); future.thenAccept(pair -> { final DeviceCommand deviceCommand = pair.getRight(); if (!asyncResponse.isDone() && deviceCommand.getIsUpdated()) { asyncResponse.resume(ResponseFactory.response( Response.Status.OK, deviceCommand, Policy.COMMAND_TO_DEVICE)); } if (timeout == 0) { asyncResponse.setTimeout(1, TimeUnit.MILLISECONDS); // setting timeout to 0 would cause // the thread to suspend indefinitely, see AsyncResponse docs } else { asyncResponse.setTimeout(timeout, TimeUnit.SECONDS); } }); asyncResponse.register(new CompletionCallback() { @Override public void onComplete(Throwable throwable) { try { commandService.sendUnsubscribeRequest(future.get().getLeft(), null); } catch (InterruptedException | ExecutionException e) { if (!asyncResponse.isDone()) { asyncResponse.resume(ResponseFactory.response(Response.Status.INTERNAL_SERVER_ERROR)); } } } }); } else { if (!asyncResponse.isDone()) { asyncResponse.resume(ResponseFactory.response(Response.Status.OK, command.get(), Policy.COMMAND_TO_DEVICE)); } } } @Override public void query(String guid, String startTs, String endTs, String command, String status, String sortField, String sortOrderSt, Integer take, Integer skip, @Suspended final AsyncResponse asyncResponse) { LOGGER.debug("Device command query requested for device {}", guid); final Date timestampSt = TimestampQueryParamParser.parse(startTs); final Date timestampEnd = TimestampQueryParamParser.parse(endTs); DeviceVO device = deviceService.getDeviceWithNetworkAndDeviceClass(guid); if (device == null) { ErrorResponse errorCode = new ErrorResponse(NOT_FOUND.getStatusCode(), String.format(Messages.DEVICE_NOT_FOUND, guid)); Response response = ResponseFactory.response(NOT_FOUND, errorCode); asyncResponse.resume(response); } else { List<String> searchCommands = StringUtils.isNoneEmpty(command) ? Collections.singletonList(command) : Collections.EMPTY_LIST; commandService.find(Collections.singletonList(guid), searchCommands, timestampSt, timestampEnd, status) .thenApply(commands -> { final Comparator<DeviceCommand> comparator = CommandResponseFilterAndSort.buildDeviceCommandComparator(sortField); final Boolean reverse = sortOrderSt == null ? null : "desc".equalsIgnoreCase(sortOrderSt); final List<DeviceCommand> sortedDeviceCommands = CommandResponseFilterAndSort.orderAndLimit(new ArrayList<>(commands), comparator, reverse, skip, take); return ResponseFactory.response(OK, sortedDeviceCommands, Policy.COMMAND_LISTED); }) .thenAccept(asyncResponse::resume); } } /** * {@inheritDoc} */ @Override public void get(String guid, String commandId, @Suspended final AsyncResponse asyncResponse) { LOGGER.debug("Device command get requested. deviceId = {}, commandId = {}", guid, commandId); DeviceVO device = deviceService.getDeviceWithNetworkAndDeviceClass(guid); if (device == null) { Response response = ResponseFactory.response(NOT_FOUND, new ErrorResponse(NOT_FOUND.getStatusCode(), String.format(Messages.DEVICE_NOT_FOUND, guid))); asyncResponse.resume(response); return; } commandService.findOne(Long.valueOf(commandId), device.getGuid()) .thenApply(command -> { if (!command.isPresent()) { LOGGER.warn("Device command get failed. No command with id = {} found for device with guid = {}", commandId, guid); return ResponseFactory.response(NOT_FOUND, new ErrorResponse(NOT_FOUND.getStatusCode(), String.format(Messages.COMMAND_NOT_FOUND, commandId))); } if (!command.get().getDeviceGuid().equals(guid)) { LOGGER.debug("DeviceCommand wait request failed. Command with id = {} was not sent for device with guid = {}", commandId, guid); return ResponseFactory.response(BAD_REQUEST, new ErrorResponse(BAD_REQUEST.getStatusCode(), String.format(Messages.COMMAND_NOT_FOUND, commandId))); } LOGGER.debug("Device command get proceed successfully deviceId = {} commandId = {}", guid, commandId); return ResponseFactory.response(OK, command.get(), Policy.COMMAND_TO_DEVICE); }) .thenAccept(asyncResponse::resume); } /** * {@inheritDoc} */ @Override public void insert(String guid, DeviceCommandWrapper deviceCommand, @Suspended final AsyncResponse asyncResponse) { LOGGER.debug("Device command insert requested. deviceId = {}, command = {}", guid, deviceCommand.getCommand()); final HivePrincipal principal = (HivePrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); UserVO authUser = principal.getUser(); DeviceVO device = deviceService.getDeviceWithNetworkAndDeviceClass(guid); if (device == null) { LOGGER.warn("Device command insert failed. No device with guid = {} found", guid); ErrorResponse errorCode = new ErrorResponse(NOT_FOUND.getStatusCode(), String.format(Messages.DEVICE_NOT_FOUND, guid)); Response response = ResponseFactory.response(NOT_FOUND, errorCode); asyncResponse.resume(response); } else { DeviceCommand command = commandService.insert(deviceCommand, device, authUser).join(); if (command != null) { LOGGER.debug("Device command insertAll proceed successfully. deviceId = {} command = {}", guid, deviceCommand.getCommand()); Response jaxResponse = ResponseFactory.response(Response.Status.CREATED, command, Policy.COMMAND_TO_CLIENT); asyncResponse.resume(jaxResponse); } else { LOGGER.warn("Device command insert failed for device with guid = {}.", guid); ErrorResponse errorCode = new ErrorResponse(NOT_FOUND.getStatusCode(), String.format(Messages.COMMAND_NOT_FOUND, -1L)); Response jaxResponse = ResponseFactory.response(NOT_FOUND, errorCode); asyncResponse.resume(jaxResponse); } } } /** * {@inheritDoc} */ @Override public void update(String guid, Long commandId, DeviceCommandWrapper command, @Suspended final AsyncResponse asyncResponse) { LOGGER.debug("Device command update requested. command {}", command); DeviceVO device = deviceService.getDeviceWithNetworkAndDeviceClass(guid); if (device == null) { LOGGER.warn("Device command update failed. No device with guid = {} found", guid); ErrorResponse errorCode = new ErrorResponse(NOT_FOUND.getStatusCode(), String.format(Messages.DEVICE_NOT_FOUND, guid)); Response response = ResponseFactory.response(NOT_FOUND, errorCode); asyncResponse.resume(response); } else { Optional<DeviceCommand> savedCommand = commandService.findOne(commandId, guid).join(); if (!savedCommand.isPresent()) { LOGGER.warn("Device command update failed. No command with id = {} found for device with guid = {}", commandId, guid); Response response = ResponseFactory.response(NOT_FOUND, new ErrorResponse(NOT_FOUND.getStatusCode(), String.format(Messages.COMMAND_NOT_FOUND, commandId))); asyncResponse.resume(response); } else { LOGGER.debug("Device command update proceed successfully deviceId = {} commandId = {}", guid, commandId); commandService.update(savedCommand.get(), command); asyncResponse.resume(ResponseFactory.response(Response.Status.NO_CONTENT)); } } } }