Merge branch 'master' of github.com:thingsboard/thingsboard
This commit is contained in:
		
						commit
						c94ef878d9
					
				@ -29,7 +29,6 @@ import org.springframework.web.bind.annotation.RequestParam;
 | 
			
		||||
import org.springframework.web.bind.annotation.ResponseBody;
 | 
			
		||||
import org.springframework.web.bind.annotation.ResponseStatus;
 | 
			
		||||
import org.springframework.web.bind.annotation.RestController;
 | 
			
		||||
import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
 | 
			
		||||
import org.thingsboard.server.common.data.Customer;
 | 
			
		||||
import org.thingsboard.server.common.data.DataConstants;
 | 
			
		||||
import org.thingsboard.server.common.data.EntitySubtype;
 | 
			
		||||
@ -39,7 +38,6 @@ import org.thingsboard.server.common.data.audit.ActionType;
 | 
			
		||||
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
 | 
			
		||||
import org.thingsboard.server.common.data.exception.ThingsboardException;
 | 
			
		||||
import org.thingsboard.server.common.data.id.CustomerId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.DeviceId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.EntityId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.EntityViewId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
@ -47,7 +45,6 @@ import org.thingsboard.server.common.data.id.UUIDBased;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 | 
			
		||||
import org.thingsboard.server.common.data.page.TextPageData;
 | 
			
		||||
import org.thingsboard.server.common.data.page.TextPageLink;
 | 
			
		||||
import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
 | 
			
		||||
import org.thingsboard.server.dao.exception.IncorrectParameterException;
 | 
			
		||||
import org.thingsboard.server.dao.model.ModelConstants;
 | 
			
		||||
import org.thingsboard.server.service.security.model.SecurityUser;
 | 
			
		||||
@ -174,7 +171,7 @@ public class EntityViewController extends BaseController {
 | 
			
		||||
            EntityView entityView = checkEntityViewId(entityViewId);
 | 
			
		||||
            entityViewService.deleteEntityView(entityViewId);
 | 
			
		||||
            logEntityAction(entityViewId, entityView, entityView.getCustomerId(),
 | 
			
		||||
                    ActionType.DELETED,null, strEntityViewId);
 | 
			
		||||
                    ActionType.DELETED, null, strEntityViewId);
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            logEntityAction(emptyId(EntityType.ENTITY_VIEW),
 | 
			
		||||
                    null,
 | 
			
		||||
@ -184,11 +181,24 @@ public class EntityViewController extends BaseController {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
 | 
			
		||||
    @RequestMapping(value = "/tenant/entityViews", params = {"entityViewName"}, method = RequestMethod.GET)
 | 
			
		||||
    @ResponseBody
 | 
			
		||||
    public EntityView getTenantEntityView(
 | 
			
		||||
            @RequestParam String entityViewName) throws ThingsboardException {
 | 
			
		||||
        try {
 | 
			
		||||
            TenantId tenantId = getCurrentUser().getTenantId();
 | 
			
		||||
            return checkNotNull(entityViewService.findEntityViewByTenantIdAndName(tenantId, entityViewName));
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            throw handleException(e);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
 | 
			
		||||
    @RequestMapping(value = "/customer/{customerId}/entityView/{entityViewId}", method = RequestMethod.POST)
 | 
			
		||||
    @ResponseBody
 | 
			
		||||
    public EntityView assignEntityViewToCustomer(@PathVariable(CUSTOMER_ID) String strCustomerId,
 | 
			
		||||
                                             @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
 | 
			
		||||
                                                 @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
 | 
			
		||||
        checkParameter(CUSTOMER_ID, strCustomerId);
 | 
			
		||||
        checkParameter(ENTITY_VIEW_ID, strEntityViewId);
 | 
			
		||||
        try {
 | 
			
		||||
 | 
			
		||||
@ -49,9 +49,11 @@ import org.thingsboard.server.common.data.kv.Aggregation;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.AttributeKey;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.KvEntry;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.LongDataEntry;
 | 
			
		||||
@ -60,12 +62,10 @@ import org.thingsboard.server.common.data.kv.StringDataEntry;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.TsKvEntry;
 | 
			
		||||
import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
 | 
			
		||||
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
 | 
			
		||||
import org.thingsboard.server.dao.attributes.AttributesService;
 | 
			
		||||
import org.thingsboard.server.dao.timeseries.TimeseriesService;
 | 
			
		||||
import org.thingsboard.server.service.security.AccessValidator;
 | 
			
		||||
import org.thingsboard.server.service.security.model.SecurityUser;
 | 
			
		||||
import org.thingsboard.server.service.telemetry.AttributeData;
 | 
			
		||||
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
 | 
			
		||||
import org.thingsboard.server.service.telemetry.TsData;
 | 
			
		||||
import org.thingsboard.server.service.telemetry.exception.InvalidParametersException;
 | 
			
		||||
import org.thingsboard.server.service.telemetry.exception.UncheckedApiException;
 | 
			
		||||
@ -249,6 +249,60 @@ public class TelemetryController extends BaseController {
 | 
			
		||||
        return saveTelemetry(entityId, requestBody, ttl);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
 | 
			
		||||
    @RequestMapping(value = "/{entityType}/{entityId}/timeseries/delete", method = RequestMethod.DELETE)
 | 
			
		||||
    @ResponseBody
 | 
			
		||||
    public DeferredResult<ResponseEntity> deleteEntityTimeseries(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
 | 
			
		||||
                                                                 @RequestParam(name = "keys") String keysStr,
 | 
			
		||||
                                                                 @RequestParam(name = "deleteAllDataForKeys", defaultValue = "false") boolean deleteAllDataForKeys,
 | 
			
		||||
                                                                 @RequestParam(name = "startTs", required = false) Long startTs,
 | 
			
		||||
                                                                 @RequestParam(name = "endTs", required = false) Long endTs,
 | 
			
		||||
                                                                 @RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted) throws ThingsboardException {
 | 
			
		||||
        EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
 | 
			
		||||
        return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private DeferredResult<ResponseEntity> deleteTimeseries(EntityId entityIdStr, String keysStr, boolean deleteAllDataForKeys,
 | 
			
		||||
                                                            Long startTs, Long endTs, boolean rewriteLatestIfDeleted) throws ThingsboardException {
 | 
			
		||||
        List<String> keys = toKeysList(keysStr);
 | 
			
		||||
        if (keys.isEmpty()) {
 | 
			
		||||
            return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST);
 | 
			
		||||
        }
 | 
			
		||||
        SecurityUser user = getCurrentUser();
 | 
			
		||||
 | 
			
		||||
        long deleteFromTs;
 | 
			
		||||
        long deleteToTs;
 | 
			
		||||
        if (deleteAllDataForKeys) {
 | 
			
		||||
            deleteFromTs = 0L;
 | 
			
		||||
            deleteToTs = System.currentTimeMillis();
 | 
			
		||||
        } else {
 | 
			
		||||
            deleteFromTs = startTs;
 | 
			
		||||
            deleteToTs = endTs;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return accessValidator.validateEntityAndCallback(user, entityIdStr, (result, entityId) -> {
 | 
			
		||||
            List<DeleteTsKvQuery> deleteTsKvQueries = new ArrayList<>();
 | 
			
		||||
            for (String key : keys) {
 | 
			
		||||
                deleteTsKvQueries.add(new BaseDeleteTsKvQuery(key, deleteFromTs, deleteToTs, rewriteLatestIfDeleted));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            ListenableFuture<List<Void>> future = tsService.remove(entityId, deleteTsKvQueries);
 | 
			
		||||
            Futures.addCallback(future, new FutureCallback<List<Void>>() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSuccess(@Nullable List<Void> tmp) {
 | 
			
		||||
                    logTimeseriesDeleted(user, entityId, keys, null);
 | 
			
		||||
                    result.setResult(new ResponseEntity<>(HttpStatus.OK));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onFailure(Throwable t) {
 | 
			
		||||
                    logTimeseriesDeleted(user, entityId, keys, t);
 | 
			
		||||
                    result.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
 | 
			
		||||
                }
 | 
			
		||||
            }, executor);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
 | 
			
		||||
    @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE)
 | 
			
		||||
    @ResponseBody
 | 
			
		||||
@ -506,6 +560,15 @@ public class TelemetryController extends BaseController {
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void logTimeseriesDeleted(SecurityUser user, EntityId entityId, List<String> keys, Throwable e) {
 | 
			
		||||
        try {
 | 
			
		||||
            logEntityAction(user, (UUIDBased & EntityId) entityId, null, null, ActionType.TIMESERIES_DELETED, toException(e),
 | 
			
		||||
                    keys);
 | 
			
		||||
        } catch (ThingsboardException te) {
 | 
			
		||||
            log.warn("Failed to log timeseries delete", te);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void logAttributesDeleted(SecurityUser user, EntityId entityId, String scope, List<String> keys, Throwable e) {
 | 
			
		||||
        try {
 | 
			
		||||
            logEntityAction(user, (UUIDBased & EntityId) entityId, null, null, ActionType.ATTRIBUTES_DELETED, toException(e),
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,7 @@ import org.apache.curator.framework.state.ConnectionStateListener;
 | 
			
		||||
import org.apache.curator.retry.RetryForever;
 | 
			
		||||
import org.apache.curator.utils.CloseableUtils;
 | 
			
		||||
import org.apache.zookeeper.CreateMode;
 | 
			
		||||
import org.apache.zookeeper.KeeperException;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Autowired;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Value;
 | 
			
		||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 | 
			
		||||
@ -51,6 +52,8 @@ import java.util.List;
 | 
			
		||||
import java.util.NoSuchElementException;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
import static org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent.Type.CHILD_REMOVED;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author Andrew Shvayka
 | 
			
		||||
 */
 | 
			
		||||
@ -128,19 +131,42 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void publishCurrentServer() {
 | 
			
		||||
    public synchronized void publishCurrentServer() {
 | 
			
		||||
        ServerInstance self = this.serverInstance.getSelf();
 | 
			
		||||
        if (currentServerExists()) {
 | 
			
		||||
            log.info("[{}:{}] ZK node for current instance already exists, NOT created new one: {}", self.getHost(), self.getPort(), nodePath);
 | 
			
		||||
        } else {
 | 
			
		||||
            try {
 | 
			
		||||
                log.info("[{}:{}] Creating ZK node for current instance", self.getHost(), self.getPort());
 | 
			
		||||
                nodePath = client.create()
 | 
			
		||||
                        .creatingParentsIfNeeded()
 | 
			
		||||
                        .withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(zkNodesDir + "/", SerializationUtils.serialize(self.getServerAddress()));
 | 
			
		||||
                log.info("[{}:{}] Created ZK node for current instance: {}", self.getHost(), self.getPort(), nodePath);
 | 
			
		||||
                client.getConnectionStateListenable().addListener(checkReconnect(self));
 | 
			
		||||
            } catch (Exception e) {
 | 
			
		||||
                log.error("Failed to create ZK node", e);
 | 
			
		||||
                throw new RuntimeException(e);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean currentServerExists() {
 | 
			
		||||
        if (nodePath == null) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        try {
 | 
			
		||||
            ServerInstance self = this.serverInstance.getSelf();
 | 
			
		||||
            log.info("[{}:{}] Creating ZK node for current instance", self.getHost(), self.getPort());
 | 
			
		||||
            nodePath = client.create()
 | 
			
		||||
                    .creatingParentsIfNeeded()
 | 
			
		||||
                    .withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(zkNodesDir + "/", SerializationUtils.serialize(self.getServerAddress()));
 | 
			
		||||
            log.info("[{}:{}] Created ZK node for current instance: {}", self.getHost(), self.getPort(), nodePath);
 | 
			
		||||
            client.getConnectionStateListenable().addListener(checkReconnect(self));
 | 
			
		||||
            ServerAddress registeredServerAdress = null;
 | 
			
		||||
            registeredServerAdress = SerializationUtils.deserialize(client.getData().forPath(nodePath));
 | 
			
		||||
            if (self.getServerAddress() != null && self.getServerAddress().equals(registeredServerAdress)) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        } catch (KeeperException.NoNodeException e) {
 | 
			
		||||
            log.info("ZK node does not exist: {}", nodePath);
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            log.error("Failed to create ZK node", e);
 | 
			
		||||
            throw new RuntimeException(e);
 | 
			
		||||
            log.error("Couldn't check if ZK node exists", e);
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ConnectionStateListener checkReconnect(ServerInstance self) {
 | 
			
		||||
@ -218,6 +244,10 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
 | 
			
		||||
            log.debug("Ignoring {} due to empty child's data", pathChildrenCacheEvent);
 | 
			
		||||
            return;
 | 
			
		||||
        } else if (nodePath != null && nodePath.equals(data.getPath())) {
 | 
			
		||||
            if (pathChildrenCacheEvent.getType() == CHILD_REMOVED) {
 | 
			
		||||
                log.info("ZK node for current instance is somehow deleted.");
 | 
			
		||||
                publishCurrentServer();
 | 
			
		||||
            }
 | 
			
		||||
            log.debug("Ignoring event about current server {}", pathChildrenCacheEvent);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,7 @@ public enum ActionType {
 | 
			
		||||
    UPDATED(false), // log entity
 | 
			
		||||
    ATTRIBUTES_UPDATED(false), // log attributes/values
 | 
			
		||||
    ATTRIBUTES_DELETED(false), // log attributes
 | 
			
		||||
    TIMESERIES_DELETED(false), // log timeseries
 | 
			
		||||
    RPC_CALL(false), // log method and params
 | 
			
		||||
    CREDENTIALS_UPDATED(false), // log new credentials
 | 
			
		||||
    ASSIGNED_TO_CUSTOMER(false), // log customer name
 | 
			
		||||
@ -32,11 +33,11 @@ public enum ActionType {
 | 
			
		||||
    SUSPENDED(false), // log string id
 | 
			
		||||
    CREDENTIALS_READ(true), // log device id
 | 
			
		||||
    ATTRIBUTES_READ(true), // log attributes
 | 
			
		||||
    RELATION_ADD_OR_UPDATE (false),
 | 
			
		||||
    RELATION_DELETED (false),
 | 
			
		||||
    RELATIONS_DELETED (false),
 | 
			
		||||
    ALARM_ACK (false),
 | 
			
		||||
    ALARM_CLEAR (false);
 | 
			
		||||
    RELATION_ADD_OR_UPDATE(false),
 | 
			
		||||
    RELATION_DELETED(false),
 | 
			
		||||
    RELATIONS_DELETED(false),
 | 
			
		||||
    ALARM_ACK(false),
 | 
			
		||||
    ALARM_CLEAR(false);
 | 
			
		||||
 | 
			
		||||
    private final boolean isRead;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -43,6 +43,8 @@ public interface EntityViewService {
 | 
			
		||||
 | 
			
		||||
    EntityView findEntityViewById(EntityViewId entityViewId);
 | 
			
		||||
 | 
			
		||||
    EntityView findEntityViewByTenantIdAndName(TenantId tenantId, String name);
 | 
			
		||||
 | 
			
		||||
    TextPageData<EntityView> findEntityViewByTenantId(TenantId tenantId, TextPageLink pageLink);
 | 
			
		||||
 | 
			
		||||
    TextPageData<EntityView> findEntityViewByTenantIdAndType(TenantId tenantId, TextPageLink pageLink, String type);
 | 
			
		||||
 | 
			
		||||
@ -29,8 +29,6 @@ import org.springframework.cache.annotation.Cacheable;
 | 
			
		||||
import org.springframework.cache.annotation.Caching;
 | 
			
		||||
import org.springframework.stereotype.Service;
 | 
			
		||||
import org.thingsboard.server.common.data.Customer;
 | 
			
		||||
import org.thingsboard.server.common.data.DataConstants;
 | 
			
		||||
import org.thingsboard.server.common.data.Device;
 | 
			
		||||
import org.thingsboard.server.common.data.EntitySubtype;
 | 
			
		||||
import org.thingsboard.server.common.data.EntityType;
 | 
			
		||||
import org.thingsboard.server.common.data.EntityView;
 | 
			
		||||
@ -40,12 +38,10 @@ import org.thingsboard.server.common.data.id.CustomerId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.EntityId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.EntityViewId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 | 
			
		||||
import org.thingsboard.server.common.data.page.TextPageData;
 | 
			
		||||
import org.thingsboard.server.common.data.page.TextPageLink;
 | 
			
		||||
import org.thingsboard.server.common.data.relation.EntityRelation;
 | 
			
		||||
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
 | 
			
		||||
import org.thingsboard.server.dao.attributes.AttributesService;
 | 
			
		||||
import org.thingsboard.server.dao.customer.CustomerDao;
 | 
			
		||||
import org.thingsboard.server.dao.entity.AbstractEntityService;
 | 
			
		||||
import org.thingsboard.server.dao.exception.DataValidationException;
 | 
			
		||||
@ -56,15 +52,13 @@ import org.thingsboard.server.dao.tenant.TenantDao;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.Comparator;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.concurrent.ExecutionException;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
import static org.thingsboard.server.common.data.CacheConstants.ENTITY_VIEW_CACHE;
 | 
			
		||||
import static org.thingsboard.server.common.data.CacheConstants.RELATIONS_CACHE;
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
 | 
			
		||||
import static org.thingsboard.server.dao.service.Validator.validateId;
 | 
			
		||||
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
 | 
			
		||||
@ -96,6 +90,7 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
 | 
			
		||||
 | 
			
		||||
    @Caching(evict = {
 | 
			
		||||
            @CacheEvict(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityView.tenantId, #entityView.entityId}"),
 | 
			
		||||
            @CacheEvict(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityView.tenantId, #entityView.name}"),
 | 
			
		||||
            @CacheEvict(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityView.id}")})
 | 
			
		||||
    @Override
 | 
			
		||||
    public EntityView saveEntityView(EntityView entityView) {
 | 
			
		||||
@ -137,6 +132,15 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
 | 
			
		||||
        return entityViewDao.findById(entityViewId.getId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Cacheable(cacheNames = ENTITY_VIEW_CACHE, key = "{#tenantId, #name}")
 | 
			
		||||
    @Override
 | 
			
		||||
    public EntityView findEntityViewByTenantIdAndName(TenantId tenantId, String name) {
 | 
			
		||||
        log.trace("Executing findEntityViewByTenantIdAndName [{}][{}]", tenantId, name);
 | 
			
		||||
        validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
 | 
			
		||||
        Optional<EntityView> entityViewOpt = entityViewDao.findEntityViewByTenantIdAndName(tenantId.getId(), name);
 | 
			
		||||
        return entityViewOpt.orElse(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public TextPageData<EntityView> findEntityViewByTenantId(TenantId tenantId, TextPageLink pageLink) {
 | 
			
		||||
        log.trace("Executing findEntityViewsByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
 | 
			
		||||
@ -255,6 +259,7 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
 | 
			
		||||
        deleteEntityRelations(entityViewId);
 | 
			
		||||
        EntityView entityView = entityViewDao.findById(entityViewId.getId());
 | 
			
		||||
        cacheManager.getCache(ENTITY_VIEW_CACHE).evict(Arrays.asList(entityView.getTenantId(), entityView.getEntityId()));
 | 
			
		||||
        cacheManager.getCache(ENTITY_VIEW_CACHE).evict(Arrays.asList(entityView.getTenantId(), entityView.getName()));
 | 
			
		||||
        entityViewDao.removeById(entityViewId.getId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ package org.thingsboard.server.dao.sql.timeseries;
 | 
			
		||||
 | 
			
		||||
import com.google.common.base.Function;
 | 
			
		||||
import com.google.common.collect.Lists;
 | 
			
		||||
import com.google.common.util.concurrent.FutureCallback;
 | 
			
		||||
import com.google.common.util.concurrent.Futures;
 | 
			
		||||
import com.google.common.util.concurrent.ListenableFuture;
 | 
			
		||||
import com.google.common.util.concurrent.ListeningExecutorService;
 | 
			
		||||
@ -31,6 +32,7 @@ import org.springframework.stereotype.Component;
 | 
			
		||||
import org.thingsboard.server.common.data.UUIDConverter;
 | 
			
		||||
import org.thingsboard.server.common.data.id.EntityId;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.Aggregation;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
 | 
			
		||||
@ -41,9 +43,9 @@ import org.thingsboard.server.dao.model.sql.TsKvEntity;
 | 
			
		||||
import org.thingsboard.server.dao.model.sql.TsKvLatestCompositeKey;
 | 
			
		||||
import org.thingsboard.server.dao.model.sql.TsKvLatestEntity;
 | 
			
		||||
import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService;
 | 
			
		||||
import org.thingsboard.server.dao.timeseries.SimpleListenableFuture;
 | 
			
		||||
import org.thingsboard.server.dao.timeseries.TimeseriesDao;
 | 
			
		||||
import org.thingsboard.server.dao.timeseries.TsInsertExecutorType;
 | 
			
		||||
import org.thingsboard.server.dao.util.SqlDao;
 | 
			
		||||
import org.thingsboard.server.dao.util.SqlTsDao;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
@ -53,6 +55,7 @@ import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import java.util.concurrent.ExecutionException;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
@ -64,6 +67,8 @@ import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
 | 
			
		||||
@SqlTsDao
 | 
			
		||||
public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService implements TimeseriesDao {
 | 
			
		||||
 | 
			
		||||
    private static final String DESC_ORDER = "DESC";
 | 
			
		||||
 | 
			
		||||
    @Value("${sql.ts_inserts_executor_type}")
 | 
			
		||||
    private String insertExecutorType;
 | 
			
		||||
 | 
			
		||||
@ -326,14 +331,72 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ListenableFuture<Void> removeLatest(EntityId entityId, DeleteTsKvQuery query) {
 | 
			
		||||
        TsKvLatestEntity latestEntity = new TsKvLatestEntity();
 | 
			
		||||
        latestEntity.setEntityType(entityId.getEntityType());
 | 
			
		||||
        latestEntity.setEntityId(fromTimeUUID(entityId.getId()));
 | 
			
		||||
        latestEntity.setKey(query.getKey());
 | 
			
		||||
        return service.submit(() -> {
 | 
			
		||||
            tsKvLatestRepository.delete(latestEntity);
 | 
			
		||||
            return null;
 | 
			
		||||
        ListenableFuture<TsKvEntry> latestFuture = findLatest(entityId, query.getKey());
 | 
			
		||||
 | 
			
		||||
        ListenableFuture<Boolean> booleanFuture = Futures.transform(latestFuture, tsKvEntry -> {
 | 
			
		||||
            long ts = tsKvEntry.getTs();
 | 
			
		||||
            return ts > query.getStartTs() && ts <= query.getEndTs();
 | 
			
		||||
        }, service);
 | 
			
		||||
 | 
			
		||||
        ListenableFuture<Void> removedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> {
 | 
			
		||||
            if (isRemove) {
 | 
			
		||||
                TsKvLatestEntity latestEntity = new TsKvLatestEntity();
 | 
			
		||||
                latestEntity.setEntityType(entityId.getEntityType());
 | 
			
		||||
                latestEntity.setEntityId(fromTimeUUID(entityId.getId()));
 | 
			
		||||
                latestEntity.setKey(query.getKey());
 | 
			
		||||
                return service.submit(() -> {
 | 
			
		||||
                    tsKvLatestRepository.delete(latestEntity);
 | 
			
		||||
                    return null;
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            return Futures.immediateFuture(null);
 | 
			
		||||
        }, service);
 | 
			
		||||
 | 
			
		||||
        final SimpleListenableFuture<Void> resultFuture = new SimpleListenableFuture<>();
 | 
			
		||||
        Futures.addCallback(removedLatestFuture, new FutureCallback<Void>() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onSuccess(@Nullable Void result) {
 | 
			
		||||
                if (query.getRewriteLatestIfDeleted()) {
 | 
			
		||||
                    ListenableFuture<Void> savedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> {
 | 
			
		||||
                        if (isRemove) {
 | 
			
		||||
                            return getNewLatestEntryFuture(entityId, query);
 | 
			
		||||
                        }
 | 
			
		||||
                        return Futures.immediateFuture(null);
 | 
			
		||||
                    }, service);
 | 
			
		||||
 | 
			
		||||
                    try {
 | 
			
		||||
                        resultFuture.set(savedLatestFuture.get());
 | 
			
		||||
                    } catch (InterruptedException | ExecutionException e) {
 | 
			
		||||
                        log.warn("Could not get latest saved value for [{}], {}", entityId, query.getKey(), e);
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    resultFuture.set(null);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onFailure(Throwable t) {
 | 
			
		||||
                log.warn("[{}] Failed to process remove of the latest value", entityId, t);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        return resultFuture;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<Void> getNewLatestEntryFuture(EntityId entityId, DeleteTsKvQuery query) {
 | 
			
		||||
        long startTs = 0;
 | 
			
		||||
        long endTs = query.getStartTs() - 1;
 | 
			
		||||
        ReadTsKvQuery findNewLatestQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, endTs - startTs, 1,
 | 
			
		||||
                Aggregation.NONE, DESC_ORDER);
 | 
			
		||||
        ListenableFuture<List<TsKvEntry>> future = findAllAsync(entityId, findNewLatestQuery);
 | 
			
		||||
 | 
			
		||||
        return Futures.transformAsync(future, entryList -> {
 | 
			
		||||
            if (entryList.size() == 1) {
 | 
			
		||||
                return saveLatest(entityId, entryList.get(0));
 | 
			
		||||
            } else {
 | 
			
		||||
                log.trace("Could not find new latest value for [{}], key - {}", entityId, query.getKey());
 | 
			
		||||
            }
 | 
			
		||||
            return Futures.immediateFuture(null);
 | 
			
		||||
        }, service);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 | 
			
		||||
@ -47,7 +47,7 @@ public interface TsKvRepository extends CrudRepository<TsKvEntity, TsKvComposite
 | 
			
		||||
    @Modifying
 | 
			
		||||
    @Query("DELETE FROM TsKvEntity tskv WHERE tskv.entityId = :entityId " +
 | 
			
		||||
            "AND tskv.entityType = :entityType AND tskv.key = :entityKey " +
 | 
			
		||||
            "AND tskv.ts > :startTs AND tskv.ts < :endTs")
 | 
			
		||||
            "AND tskv.ts > :startTs AND tskv.ts <= :endTs")
 | 
			
		||||
    void delete(@Param("entityId") String entityId,
 | 
			
		||||
                @Param("entityType") EntityType entityType,
 | 
			
		||||
                @Param("entityKey") String key,
 | 
			
		||||
 | 
			
		||||
@ -48,7 +48,6 @@ import org.thingsboard.server.common.data.kv.StringDataEntry;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.TsKvEntry;
 | 
			
		||||
import org.thingsboard.server.dao.model.ModelConstants;
 | 
			
		||||
import org.thingsboard.server.dao.nosql.CassandraAbstractAsyncDao;
 | 
			
		||||
import org.thingsboard.server.dao.util.NoSqlDao;
 | 
			
		||||
import org.thingsboard.server.dao.util.NoSqlTsDao;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
@ -62,6 +61,7 @@ import java.util.Arrays;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.concurrent.ExecutionException;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
 | 
			
		||||
@ -434,14 +434,14 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
 | 
			
		||||
    public ListenableFuture<Void> removeLatest(EntityId entityId, DeleteTsKvQuery query) {
 | 
			
		||||
        ListenableFuture<TsKvEntry> latestEntryFuture = findLatest(entityId, query.getKey());
 | 
			
		||||
 | 
			
		||||
        ListenableFuture<Boolean> booleanFuture = Futures.transformAsync(latestEntryFuture, latestEntry -> {
 | 
			
		||||
        ListenableFuture<Boolean> booleanFuture = Futures.transform(latestEntryFuture, latestEntry -> {
 | 
			
		||||
            long ts = latestEntry.getTs();
 | 
			
		||||
            if (ts >= query.getStartTs() && ts <= query.getEndTs()) {
 | 
			
		||||
                return Futures.immediateFuture(true);
 | 
			
		||||
            if (ts > query.getStartTs() && ts <= query.getEndTs()) {
 | 
			
		||||
                return true;
 | 
			
		||||
            } else {
 | 
			
		||||
                log.trace("Won't be deleted latest value for [{}], key - {}", entityId, query.getKey());
 | 
			
		||||
            }
 | 
			
		||||
            return Futures.immediateFuture(false);
 | 
			
		||||
            return false;
 | 
			
		||||
        }, readResultsProcessingExecutor);
 | 
			
		||||
 | 
			
		||||
        ListenableFuture<Void> removedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> {
 | 
			
		||||
@ -451,18 +451,34 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
 | 
			
		||||
            return Futures.immediateFuture(null);
 | 
			
		||||
        }, readResultsProcessingExecutor);
 | 
			
		||||
 | 
			
		||||
        if (query.getRewriteLatestIfDeleted()) {
 | 
			
		||||
            ListenableFuture<Void> savedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> {
 | 
			
		||||
                if (isRemove) {
 | 
			
		||||
                    return getNewLatestEntryFuture(entityId, query);
 | 
			
		||||
                }
 | 
			
		||||
                return Futures.immediateFuture(null);
 | 
			
		||||
            }, readResultsProcessingExecutor);
 | 
			
		||||
        final SimpleListenableFuture<Void> resultFuture = new SimpleListenableFuture<>();
 | 
			
		||||
        Futures.addCallback(removedLatestFuture, new FutureCallback<Void>() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onSuccess(@Nullable Void result) {
 | 
			
		||||
                if (query.getRewriteLatestIfDeleted()) {
 | 
			
		||||
                    ListenableFuture<Void> savedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> {
 | 
			
		||||
                        if (isRemove) {
 | 
			
		||||
                            return getNewLatestEntryFuture(entityId, query);
 | 
			
		||||
                        }
 | 
			
		||||
                        return Futures.immediateFuture(null);
 | 
			
		||||
                    }, readResultsProcessingExecutor);
 | 
			
		||||
 | 
			
		||||
            return Futures.transformAsync(Futures.allAsList(Arrays.asList(savedLatestFuture, removedLatestFuture)),
 | 
			
		||||
                    list -> Futures.immediateFuture(null), readResultsProcessingExecutor);
 | 
			
		||||
        }
 | 
			
		||||
        return removedLatestFuture;
 | 
			
		||||
                    try {
 | 
			
		||||
                        resultFuture.set(savedLatestFuture.get());
 | 
			
		||||
                    } catch (InterruptedException | ExecutionException e) {
 | 
			
		||||
                        log.warn("Could not get latest saved value for [{}], {}", entityId, query.getKey(), e);
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    resultFuture.set(null);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onFailure(Throwable t) {
 | 
			
		||||
                log.warn("[{}] Failed to process remove of the latest value", entityId, t);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        return resultFuture;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<Void> getNewLatestEntryFuture(EntityId entityId, DeleteTsKvQuery query) {
 | 
			
		||||
 | 
			
		||||
@ -152,7 +152,7 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testDeleteDeviceTsData() throws Exception {
 | 
			
		||||
    public void testDeleteDeviceTsDataWithoutOverwritingLatest() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(UUIDs.timeBased());
 | 
			
		||||
 | 
			
		||||
        saveEntries(deviceId, 10000);
 | 
			
		||||
@ -171,6 +171,26 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
        Assert.assertEquals(null, latest.get(0).getValueAsString());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testDeleteDeviceTsDataWithOverwritingLatest() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(UUIDs.timeBased());
 | 
			
		||||
 | 
			
		||||
        saveEntries(deviceId, 10000);
 | 
			
		||||
        saveEntries(deviceId, 20000);
 | 
			
		||||
        saveEntries(deviceId, 30000);
 | 
			
		||||
        saveEntries(deviceId, 40000);
 | 
			
		||||
 | 
			
		||||
        tsService.remove(deviceId, Collections.singletonList(
 | 
			
		||||
                new BaseDeleteTsKvQuery(STRING_KEY, 25000, 45000, true))).get();
 | 
			
		||||
 | 
			
		||||
        List<TsKvEntry> list = tsService.findAll(deviceId, Collections.singletonList(
 | 
			
		||||
                new BaseReadTsKvQuery(STRING_KEY, 5000, 45000, 10000, 10, Aggregation.NONE))).get();
 | 
			
		||||
        Assert.assertEquals(2, list.size());
 | 
			
		||||
 | 
			
		||||
        List<TsKvEntry> latest = tsService.findLatest(deviceId, Collections.singletonList(STRING_KEY)).get();
 | 
			
		||||
        Assert.assertEquals(20000, latest.get(0).getTs());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testFindDeviceTsData() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(UUIDs.timeBased());
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										21
									
								
								msa/black-box-tests/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								msa/black-box-tests/README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
 | 
			
		||||
## Black box tests execution
 | 
			
		||||
To run the black box tests with using Docker, the local Docker images of Thingsboard's microservices should be built. <br />
 | 
			
		||||
- Build the local Docker images in the directory with the Thingsboard's main [pom.xml](./../../pom.xml):
 | 
			
		||||
        
 | 
			
		||||
        mvn clean install -Ddockerfile.skip=false
 | 
			
		||||
- Verify that the new local images were built: 
 | 
			
		||||
 | 
			
		||||
        docker image ls
 | 
			
		||||
As result, in REPOSITORY column, next images should be present:
 | 
			
		||||
        
 | 
			
		||||
        thingsboard/tb-coap-transport
 | 
			
		||||
        thingsboard/tb-http-transport
 | 
			
		||||
        thingsboard/tb-mqtt-transport
 | 
			
		||||
        thingsboard/tb-node
 | 
			
		||||
        thingsboard/tb-web-ui
 | 
			
		||||
        thingsboard/tb-js-executor
 | 
			
		||||
 | 
			
		||||
- Run the black box tests in the [msa/black-box-tests](../black-box-tests) directory:
 | 
			
		||||
 | 
			
		||||
        mvn clean install -DblackBoxTests.skip=false
 | 
			
		||||
							
								
								
									
										104
									
								
								msa/black-box-tests/pom.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								msa/black-box-tests/pom.xml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,104 @@
 | 
			
		||||
<!--
 | 
			
		||||
 | 
			
		||||
    Copyright © 2016-2018 The Thingsboard Authors
 | 
			
		||||
 | 
			
		||||
    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.
 | 
			
		||||
 | 
			
		||||
-->
 | 
			
		||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
			
		||||
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 | 
			
		||||
    <modelVersion>4.0.0</modelVersion>
 | 
			
		||||
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.thingsboard</groupId>
 | 
			
		||||
        <version>2.2.0-SNAPSHOT</version>
 | 
			
		||||
        <artifactId>msa</artifactId>
 | 
			
		||||
    </parent>
 | 
			
		||||
    <groupId>org.thingsboard.msa</groupId>
 | 
			
		||||
    <artifactId>black-box-tests</artifactId>
 | 
			
		||||
 | 
			
		||||
    <name>ThingsBoard Black Box Tests</name>
 | 
			
		||||
    <url>https://thingsboard.io</url>
 | 
			
		||||
    <description>Project for ThingsBoard black box testing with using Docker</description>
 | 
			
		||||
 | 
			
		||||
    <properties>
 | 
			
		||||
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 | 
			
		||||
        <main.dir>${basedir}/../..</main.dir>
 | 
			
		||||
        <blackBoxTests.skip>true</blackBoxTests.skip>
 | 
			
		||||
        <testcontainers.version>1.9.1</testcontainers.version>
 | 
			
		||||
        <java-websocket.version>1.3.9</java-websocket.version>
 | 
			
		||||
        <httpclient.version>4.5.6</httpclient.version>
 | 
			
		||||
    </properties>
 | 
			
		||||
 | 
			
		||||
    <dependencies>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.testcontainers</groupId>
 | 
			
		||||
            <artifactId>testcontainers</artifactId>
 | 
			
		||||
            <version>${testcontainers.version}</version>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.java-websocket</groupId>
 | 
			
		||||
            <artifactId>Java-WebSocket</artifactId>
 | 
			
		||||
            <version>${java-websocket.version}</version>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.apache.httpcomponents</groupId>
 | 
			
		||||
            <artifactId>httpclient</artifactId>
 | 
			
		||||
            <version>${httpclient.version}</version>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>io.takari.junit</groupId>
 | 
			
		||||
            <artifactId>takari-cpsuite</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>ch.qos.logback</groupId>
 | 
			
		||||
            <artifactId>logback-classic</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.google.code.gson</groupId>
 | 
			
		||||
            <artifactId>gson</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.apache.commons</groupId>
 | 
			
		||||
            <artifactId>commons-lang3</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.google.guava</groupId>
 | 
			
		||||
            <artifactId>guava</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.thingsboard</groupId>
 | 
			
		||||
            <artifactId>netty-mqtt</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.thingsboard</groupId>
 | 
			
		||||
            <artifactId>tools</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
    </dependencies>
 | 
			
		||||
 | 
			
		||||
    <build>
 | 
			
		||||
        <plugins>
 | 
			
		||||
            <plugin>
 | 
			
		||||
                <groupId>org.apache.maven.plugins</groupId>
 | 
			
		||||
                <artifactId>maven-surefire-plugin</artifactId>
 | 
			
		||||
                <configuration>
 | 
			
		||||
                    <includes>
 | 
			
		||||
                        <include>**/*TestSuite.java</include>
 | 
			
		||||
                    </includes>
 | 
			
		||||
                    <skipTests>${blackBoxTests.skip}</skipTests>
 | 
			
		||||
                </configuration>
 | 
			
		||||
            </plugin>
 | 
			
		||||
        </plugins>
 | 
			
		||||
    </build>
 | 
			
		||||
 | 
			
		||||
</project>
 | 
			
		||||
@ -0,0 +1,175 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2018 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * 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 org.thingsboard.server.msa;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.databind.ObjectMapper;
 | 
			
		||||
import com.google.common.collect.ImmutableMap;
 | 
			
		||||
import com.google.gson.JsonArray;
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.apache.commons.lang3.RandomStringUtils;
 | 
			
		||||
import org.apache.http.config.Registry;
 | 
			
		||||
import org.apache.http.config.RegistryBuilder;
 | 
			
		||||
import org.apache.http.conn.socket.ConnectionSocketFactory;
 | 
			
		||||
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
 | 
			
		||||
import org.apache.http.conn.ssl.TrustStrategy;
 | 
			
		||||
import org.apache.http.conn.ssl.X509HostnameVerifier;
 | 
			
		||||
import org.apache.http.impl.client.CloseableHttpClient;
 | 
			
		||||
import org.apache.http.impl.client.HttpClients;
 | 
			
		||||
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
 | 
			
		||||
import org.apache.http.ssl.SSLContextBuilder;
 | 
			
		||||
import org.apache.http.ssl.SSLContexts;
 | 
			
		||||
import org.junit.*;
 | 
			
		||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
 | 
			
		||||
import org.thingsboard.client.tools.RestClient;
 | 
			
		||||
import org.thingsboard.server.common.data.Device;
 | 
			
		||||
import org.thingsboard.server.common.data.EntityType;
 | 
			
		||||
import org.thingsboard.server.common.data.id.DeviceId;
 | 
			
		||||
import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
 | 
			
		||||
 | 
			
		||||
import javax.net.ssl.*;
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.security.cert.X509Certificate;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Random;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
public abstract class AbstractContainerTest {
 | 
			
		||||
    protected static final String HTTPS_URL = "https://localhost";
 | 
			
		||||
    protected static final String WSS_URL = "wss://localhost";
 | 
			
		||||
    protected static RestClient restClient;
 | 
			
		||||
    protected ObjectMapper mapper = new ObjectMapper();
 | 
			
		||||
 | 
			
		||||
    @BeforeClass
 | 
			
		||||
    public static void before() throws Exception {
 | 
			
		||||
        restClient = new RestClient(HTTPS_URL);
 | 
			
		||||
        restClient.getRestTemplate().setRequestFactory(getRequestFactoryForSelfSignedCert());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected Device createDevice(String name) {
 | 
			
		||||
        return restClient.createDevice(name + RandomStringUtils.randomAlphanumeric(7), "DEFAULT");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected WsClient subscribeToWebSocket(DeviceId deviceId, String scope, CmdsType property) throws Exception {
 | 
			
		||||
        WsClient wsClient = new WsClient(new URI(WSS_URL + "/api/ws/plugins/telemetry?token=" + restClient.getToken()));
 | 
			
		||||
        SSLContextBuilder builder = SSLContexts.custom();
 | 
			
		||||
        builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true);
 | 
			
		||||
        wsClient.setSocket(builder.build().getSocketFactory().createSocket());
 | 
			
		||||
        wsClient.connectBlocking();
 | 
			
		||||
 | 
			
		||||
        JsonObject cmdsObject = new JsonObject();
 | 
			
		||||
        cmdsObject.addProperty("entityType", EntityType.DEVICE.name());
 | 
			
		||||
        cmdsObject.addProperty("entityId", deviceId.toString());
 | 
			
		||||
        cmdsObject.addProperty("scope", scope);
 | 
			
		||||
        cmdsObject.addProperty("cmdId", new Random().nextInt(100));
 | 
			
		||||
 | 
			
		||||
        JsonArray cmd = new JsonArray();
 | 
			
		||||
        cmd.add(cmdsObject);
 | 
			
		||||
        JsonObject wsRequest = new JsonObject();
 | 
			
		||||
        wsRequest.add(property.toString(), cmd);
 | 
			
		||||
        wsClient.send(wsRequest.toString());
 | 
			
		||||
        return wsClient;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected Map<String, Long> getExpectedLatestValues(long ts) {
 | 
			
		||||
        return ImmutableMap.<String, Long>builder()
 | 
			
		||||
                .put("booleanKey", ts)
 | 
			
		||||
                .put("stringKey", ts)
 | 
			
		||||
                .put("doubleKey", ts)
 | 
			
		||||
                .put("longKey", ts)
 | 
			
		||||
                .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected boolean verify(WsTelemetryResponse wsTelemetryResponse, String key, Long expectedTs, String expectedValue) {
 | 
			
		||||
        List<Object> list = wsTelemetryResponse.getDataValuesByKey(key);
 | 
			
		||||
        return expectedTs.equals(list.get(0)) && expectedValue.equals(list.get(1));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected boolean verify(WsTelemetryResponse wsTelemetryResponse, String key, String expectedValue) {
 | 
			
		||||
        List<Object> list = wsTelemetryResponse.getDataValuesByKey(key);
 | 
			
		||||
        return expectedValue.equals(list.get(1));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected JsonObject createPayload(long ts) {
 | 
			
		||||
        JsonObject values = createPayload();
 | 
			
		||||
        JsonObject payload = new JsonObject();
 | 
			
		||||
        payload.addProperty("ts", ts);
 | 
			
		||||
        payload.add("values", values);
 | 
			
		||||
        return payload;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected JsonObject createPayload() {
 | 
			
		||||
        JsonObject values = new JsonObject();
 | 
			
		||||
        values.addProperty("stringKey", "value1");
 | 
			
		||||
        values.addProperty("booleanKey", true);
 | 
			
		||||
        values.addProperty("doubleKey", 42.0);
 | 
			
		||||
        values.addProperty("longKey", 73L);
 | 
			
		||||
 | 
			
		||||
        return values;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected enum CmdsType {
 | 
			
		||||
        TS_SUB_CMDS("tsSubCmds"),
 | 
			
		||||
        HISTORY_CMDS("historyCmds"),
 | 
			
		||||
        ATTR_SUB_CMDS("attrSubCmds");
 | 
			
		||||
 | 
			
		||||
        private final String text;
 | 
			
		||||
 | 
			
		||||
        CmdsType(final String text) {
 | 
			
		||||
            this.text = text;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public String toString() {
 | 
			
		||||
            return text;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static HttpComponentsClientHttpRequestFactory getRequestFactoryForSelfSignedCert() throws Exception {
 | 
			
		||||
        SSLContextBuilder builder = SSLContexts.custom();
 | 
			
		||||
        builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true);
 | 
			
		||||
        SSLContext sslContext = builder.build();
 | 
			
		||||
        SSLConnectionSocketFactory sslSelfSigned = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void verify(String host, SSLSocket ssl) {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public void verify(String host, X509Certificate cert) {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public void verify(String host, String[] cns, String[] subjectAlts) {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public boolean verify(String s, SSLSession sslSession) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
 | 
			
		||||
                .<ConnectionSocketFactory>create()
 | 
			
		||||
                .register("https", sslSelfSigned)
 | 
			
		||||
                .build();
 | 
			
		||||
 | 
			
		||||
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
 | 
			
		||||
        CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
 | 
			
		||||
        return new HttpComponentsClientHttpRequestFactory(httpClient);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,39 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2018 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * 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 org.thingsboard.server.msa;
 | 
			
		||||
 | 
			
		||||
import org.junit.ClassRule;
 | 
			
		||||
import org.junit.extensions.cpsuite.ClasspathSuite;
 | 
			
		||||
import org.junit.runner.RunWith;
 | 
			
		||||
import org.testcontainers.containers.DockerComposeContainer;
 | 
			
		||||
import org.testcontainers.containers.wait.strategy.Wait;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.time.Duration;
 | 
			
		||||
 | 
			
		||||
@RunWith(ClasspathSuite.class)
 | 
			
		||||
@ClasspathSuite.ClassnameFilters({"org.thingsboard.server.msa.*Test"})
 | 
			
		||||
public class ContainerTestSuite {
 | 
			
		||||
 | 
			
		||||
    @ClassRule
 | 
			
		||||
    public static DockerComposeContainer composeContainer = new DockerComposeContainer(
 | 
			
		||||
            new File("./../../docker/docker-compose.yml"),
 | 
			
		||||
            new File("./../../docker/docker-compose.postgres.yml"))
 | 
			
		||||
            .withPull(false)
 | 
			
		||||
            .withLocalCompose(true)
 | 
			
		||||
            .withTailChildContainers(true)
 | 
			
		||||
            .withExposedService("tb-web-ui1", 8080, Wait.forHttp("/login").withStartupTimeout(Duration.ofSeconds(120)));
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,76 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2018 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * 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 org.thingsboard.server.msa;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.databind.ObjectMapper;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.java_websocket.client.WebSocketClient;
 | 
			
		||||
import org.java_websocket.handshake.ServerHandshake;
 | 
			
		||||
import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.util.concurrent.CountDownLatch;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class WsClient extends WebSocketClient {
 | 
			
		||||
    private static final ObjectMapper mapper = new ObjectMapper();
 | 
			
		||||
    private WsTelemetryResponse message;
 | 
			
		||||
 | 
			
		||||
    private CountDownLatch latch = new CountDownLatch(1);;
 | 
			
		||||
 | 
			
		||||
    public WsClient(URI serverUri) {
 | 
			
		||||
        super(serverUri);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onOpen(ServerHandshake serverHandshake) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onMessage(String message) {
 | 
			
		||||
        try {
 | 
			
		||||
            WsTelemetryResponse response = mapper.readValue(message, WsTelemetryResponse.class);
 | 
			
		||||
            if (!response.getData().isEmpty()) {
 | 
			
		||||
                this.message = response;
 | 
			
		||||
                latch.countDown();
 | 
			
		||||
            }
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            log.error("ws message can't be read");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onClose(int code, String reason, boolean remote) {
 | 
			
		||||
        log.info("ws is closed, due to [{}]", reason);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onError(Exception ex) {
 | 
			
		||||
        ex.printStackTrace();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public WsTelemetryResponse getLastMessage() {
 | 
			
		||||
        try {
 | 
			
		||||
            latch.await(10, TimeUnit.SECONDS);
 | 
			
		||||
            return this.message;
 | 
			
		||||
        } catch (InterruptedException e) {
 | 
			
		||||
            log.error("Timeout, ws message wasn't received");
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,57 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2018 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * 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 org.thingsboard.server.msa.connectivity;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.Sets;
 | 
			
		||||
import org.junit.Assert;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.springframework.http.ResponseEntity;
 | 
			
		||||
import org.thingsboard.server.common.data.Device;
 | 
			
		||||
import org.thingsboard.server.common.data.security.DeviceCredentials;
 | 
			
		||||
import org.thingsboard.server.msa.AbstractContainerTest;
 | 
			
		||||
import org.thingsboard.server.msa.WsClient;
 | 
			
		||||
import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
 | 
			
		||||
 | 
			
		||||
public class HttpClientTest extends AbstractContainerTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void telemetryUpload() throws Exception {
 | 
			
		||||
        restClient.login("tenant@thingsboard.org", "tenant");
 | 
			
		||||
 | 
			
		||||
        Device device = createDevice("http_");
 | 
			
		||||
        DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId());
 | 
			
		||||
 | 
			
		||||
        WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS);
 | 
			
		||||
        ResponseEntity deviceTelemetryResponse = restClient.getRestTemplate()
 | 
			
		||||
                .postForEntity(HTTPS_URL + "/api/v1/{credentialsId}/telemetry",
 | 
			
		||||
                        mapper.readTree(createPayload().toString()),
 | 
			
		||||
                        ResponseEntity.class,
 | 
			
		||||
                        deviceCredentials.getCredentialsId());
 | 
			
		||||
        Assert.assertTrue(deviceTelemetryResponse.getStatusCode().is2xxSuccessful());
 | 
			
		||||
        WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage();
 | 
			
		||||
        wsClient.closeBlocking();
 | 
			
		||||
 | 
			
		||||
        Assert.assertEquals(Sets.newHashSet("booleanKey", "stringKey", "doubleKey", "longKey"),
 | 
			
		||||
                actualLatestTelemetry.getLatestValues().keySet());
 | 
			
		||||
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", Boolean.TRUE.toString()));
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", "value1"));
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", Double.toString(42.0)));
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "longKey", Long.toString(73)));
 | 
			
		||||
 | 
			
		||||
        restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,392 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2018 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * 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 org.thingsboard.server.msa.connectivity;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.databind.JsonNode;
 | 
			
		||||
import com.google.common.collect.Sets;
 | 
			
		||||
import com.google.common.util.concurrent.ListenableFuture;
 | 
			
		||||
import com.google.common.util.concurrent.ListeningExecutorService;
 | 
			
		||||
import com.google.common.util.concurrent.MoreExecutors;
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import io.netty.buffer.ByteBuf;
 | 
			
		||||
import io.netty.buffer.Unpooled;
 | 
			
		||||
import io.netty.handler.codec.mqtt.MqttQoS;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.apache.commons.lang3.RandomStringUtils;
 | 
			
		||||
import org.junit.*;
 | 
			
		||||
import org.springframework.core.ParameterizedTypeReference;
 | 
			
		||||
import org.springframework.http.HttpMethod;
 | 
			
		||||
import org.springframework.http.ResponseEntity;
 | 
			
		||||
import org.thingsboard.mqtt.MqttClient;
 | 
			
		||||
import org.thingsboard.mqtt.MqttClientConfig;
 | 
			
		||||
import org.thingsboard.mqtt.MqttHandler;
 | 
			
		||||
import org.thingsboard.server.common.data.Device;
 | 
			
		||||
import org.thingsboard.server.common.data.id.RuleChainId;
 | 
			
		||||
import org.thingsboard.server.common.data.page.TextPageData;
 | 
			
		||||
import org.thingsboard.server.common.data.rule.NodeConnectionInfo;
 | 
			
		||||
import org.thingsboard.server.common.data.rule.RuleChain;
 | 
			
		||||
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
 | 
			
		||||
import org.thingsboard.server.common.data.rule.RuleNode;
 | 
			
		||||
import org.thingsboard.server.common.data.security.DeviceCredentials;
 | 
			
		||||
import org.thingsboard.server.msa.AbstractContainerTest;
 | 
			
		||||
import org.thingsboard.server.msa.WsClient;
 | 
			
		||||
import org.thingsboard.server.msa.mapper.AttributesResponse;
 | 
			
		||||
import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import java.util.concurrent.*;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class MqttClientTest extends AbstractContainerTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void telemetryUpload() throws Exception {
 | 
			
		||||
        restClient.login("tenant@thingsboard.org", "tenant");
 | 
			
		||||
        Device device = createDevice("mqtt_");
 | 
			
		||||
        DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId());
 | 
			
		||||
 | 
			
		||||
        WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS);
 | 
			
		||||
        MqttClient mqttClient = getMqttClient(deviceCredentials, null);
 | 
			
		||||
        mqttClient.publish("v1/devices/me/telemetry", Unpooled.wrappedBuffer(createPayload().toString().getBytes()));
 | 
			
		||||
        WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage();
 | 
			
		||||
        wsClient.closeBlocking();
 | 
			
		||||
 | 
			
		||||
        Assert.assertEquals(4, actualLatestTelemetry.getData().size());
 | 
			
		||||
        Assert.assertEquals(Sets.newHashSet("booleanKey", "stringKey", "doubleKey", "longKey"),
 | 
			
		||||
                actualLatestTelemetry.getLatestValues().keySet());
 | 
			
		||||
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", Boolean.TRUE.toString()));
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", "value1"));
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", Double.toString(42.0)));
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "longKey", Long.toString(73)));
 | 
			
		||||
 | 
			
		||||
        restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void telemetryUploadWithTs() throws Exception {
 | 
			
		||||
        long ts = 1451649600512L;
 | 
			
		||||
 | 
			
		||||
        restClient.login("tenant@thingsboard.org", "tenant");
 | 
			
		||||
        Device device = createDevice("mqtt_");
 | 
			
		||||
        DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId());
 | 
			
		||||
 | 
			
		||||
        WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS);
 | 
			
		||||
        MqttClient mqttClient = getMqttClient(deviceCredentials, null);
 | 
			
		||||
        mqttClient.publish("v1/devices/me/telemetry", Unpooled.wrappedBuffer(createPayload(ts).toString().getBytes()));
 | 
			
		||||
        WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage();
 | 
			
		||||
        wsClient.closeBlocking();
 | 
			
		||||
 | 
			
		||||
        Assert.assertEquals(4, actualLatestTelemetry.getData().size());
 | 
			
		||||
        Assert.assertEquals(getExpectedLatestValues(ts), actualLatestTelemetry.getLatestValues());
 | 
			
		||||
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", ts, Boolean.TRUE.toString()));
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", ts, "value1"));
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", ts, Double.toString(42.0)));
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "longKey", ts, Long.toString(73)));
 | 
			
		||||
 | 
			
		||||
        restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void publishAttributeUpdateToServer() throws Exception {
 | 
			
		||||
        restClient.login("tenant@thingsboard.org", "tenant");
 | 
			
		||||
        Device device = createDevice("mqtt_");
 | 
			
		||||
        DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId());
 | 
			
		||||
 | 
			
		||||
        WsClient wsClient = subscribeToWebSocket(device.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS);
 | 
			
		||||
        MqttMessageListener listener = new MqttMessageListener();
 | 
			
		||||
        MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
 | 
			
		||||
        JsonObject clientAttributes = new JsonObject();
 | 
			
		||||
        clientAttributes.addProperty("attr1", "value1");
 | 
			
		||||
        clientAttributes.addProperty("attr2", true);
 | 
			
		||||
        clientAttributes.addProperty("attr3", 42.0);
 | 
			
		||||
        clientAttributes.addProperty("attr4", 73);
 | 
			
		||||
        mqttClient.publish("v1/devices/me/attributes", Unpooled.wrappedBuffer(clientAttributes.toString().getBytes()));
 | 
			
		||||
        WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage();
 | 
			
		||||
        wsClient.closeBlocking();
 | 
			
		||||
 | 
			
		||||
        Assert.assertEquals(4, actualLatestTelemetry.getData().size());
 | 
			
		||||
        Assert.assertEquals(Sets.newHashSet("attr1", "attr2", "attr3", "attr4"),
 | 
			
		||||
                actualLatestTelemetry.getLatestValues().keySet());
 | 
			
		||||
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "attr1", "value1"));
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "attr2", Boolean.TRUE.toString()));
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "attr3", Double.toString(42.0)));
 | 
			
		||||
        Assert.assertTrue(verify(actualLatestTelemetry, "attr4", Long.toString(73)));
 | 
			
		||||
 | 
			
		||||
        restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void requestAttributeValuesFromServer() throws Exception {
 | 
			
		||||
        restClient.login("tenant@thingsboard.org", "tenant");
 | 
			
		||||
        Device device = createDevice("mqtt_");
 | 
			
		||||
        DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId());
 | 
			
		||||
 | 
			
		||||
        MqttMessageListener listener = new MqttMessageListener();
 | 
			
		||||
        MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
 | 
			
		||||
 | 
			
		||||
        // Add a new client attribute
 | 
			
		||||
        JsonObject clientAttributes = new JsonObject();
 | 
			
		||||
        String clientAttributeValue = RandomStringUtils.randomAlphanumeric(8);
 | 
			
		||||
        clientAttributes.addProperty("clientAttr", clientAttributeValue);
 | 
			
		||||
        mqttClient.publish("v1/devices/me/attributes", Unpooled.wrappedBuffer(clientAttributes.toString().getBytes()));
 | 
			
		||||
 | 
			
		||||
        // Add a new shared attribute
 | 
			
		||||
        JsonObject sharedAttributes = new JsonObject();
 | 
			
		||||
        String sharedAttributeValue = RandomStringUtils.randomAlphanumeric(8);
 | 
			
		||||
        sharedAttributes.addProperty("sharedAttr", sharedAttributeValue);
 | 
			
		||||
        ResponseEntity sharedAttributesResponse = restClient.getRestTemplate()
 | 
			
		||||
                .postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE",
 | 
			
		||||
                        mapper.readTree(sharedAttributes.toString()), ResponseEntity.class,
 | 
			
		||||
                        device.getId());
 | 
			
		||||
        Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful());
 | 
			
		||||
 | 
			
		||||
        // Subscribe to attributes response
 | 
			
		||||
        mqttClient.on("v1/devices/me/attributes/response/+", listener, MqttQoS.AT_LEAST_ONCE);
 | 
			
		||||
        // Request attributes
 | 
			
		||||
        JsonObject request = new JsonObject();
 | 
			
		||||
        request.addProperty("clientKeys", "clientAttr");
 | 
			
		||||
        request.addProperty("sharedKeys", "sharedAttr");
 | 
			
		||||
        mqttClient.publish("v1/devices/me/attributes/request/" + new Random().nextInt(100), Unpooled.wrappedBuffer(request.toString().getBytes()));
 | 
			
		||||
        MqttEvent event = listener.getEvents().poll(10, TimeUnit.SECONDS);
 | 
			
		||||
        AttributesResponse attributes = mapper.readValue(Objects.requireNonNull(event).getMessage(), AttributesResponse.class);
 | 
			
		||||
 | 
			
		||||
        Assert.assertEquals(1, attributes.getClient().size());
 | 
			
		||||
        Assert.assertEquals(clientAttributeValue, attributes.getClient().get("clientAttr"));
 | 
			
		||||
 | 
			
		||||
        Assert.assertEquals(1, attributes.getShared().size());
 | 
			
		||||
        Assert.assertEquals(sharedAttributeValue, attributes.getShared().get("sharedAttr"));
 | 
			
		||||
 | 
			
		||||
        restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void subscribeToAttributeUpdatesFromServer() throws Exception {
 | 
			
		||||
        restClient.login("tenant@thingsboard.org", "tenant");
 | 
			
		||||
        Device device = createDevice("mqtt_");
 | 
			
		||||
        DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId());
 | 
			
		||||
 | 
			
		||||
        MqttMessageListener listener = new MqttMessageListener();
 | 
			
		||||
        MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
 | 
			
		||||
        mqttClient.on("v1/devices/me/attributes", listener, MqttQoS.AT_LEAST_ONCE);
 | 
			
		||||
 | 
			
		||||
        String sharedAttributeName = "sharedAttr";
 | 
			
		||||
 | 
			
		||||
        // Add a new shared attribute
 | 
			
		||||
        JsonObject sharedAttributes = new JsonObject();
 | 
			
		||||
        String sharedAttributeValue = RandomStringUtils.randomAlphanumeric(8);
 | 
			
		||||
        sharedAttributes.addProperty(sharedAttributeName, sharedAttributeValue);
 | 
			
		||||
        ResponseEntity sharedAttributesResponse = restClient.getRestTemplate()
 | 
			
		||||
                .postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE",
 | 
			
		||||
                        mapper.readTree(sharedAttributes.toString()), ResponseEntity.class,
 | 
			
		||||
                        device.getId());
 | 
			
		||||
        Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful());
 | 
			
		||||
 | 
			
		||||
        MqttEvent event = listener.getEvents().poll(10, TimeUnit.SECONDS);
 | 
			
		||||
        Assert.assertEquals(sharedAttributeValue,
 | 
			
		||||
                mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText());
 | 
			
		||||
 | 
			
		||||
        // Update the shared attribute value
 | 
			
		||||
        JsonObject updatedSharedAttributes = new JsonObject();
 | 
			
		||||
        String updatedSharedAttributeValue = RandomStringUtils.randomAlphanumeric(8);
 | 
			
		||||
        updatedSharedAttributes.addProperty(sharedAttributeName, updatedSharedAttributeValue);
 | 
			
		||||
        ResponseEntity updatedSharedAttributesResponse = restClient.getRestTemplate()
 | 
			
		||||
                .postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE",
 | 
			
		||||
                        mapper.readTree(updatedSharedAttributes.toString()), ResponseEntity.class,
 | 
			
		||||
                        device.getId());
 | 
			
		||||
        Assert.assertTrue(updatedSharedAttributesResponse.getStatusCode().is2xxSuccessful());
 | 
			
		||||
 | 
			
		||||
        event = listener.getEvents().poll(10, TimeUnit.SECONDS);
 | 
			
		||||
        Assert.assertEquals(updatedSharedAttributeValue,
 | 
			
		||||
                mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText());
 | 
			
		||||
 | 
			
		||||
        restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void serverSideRpc() throws Exception {
 | 
			
		||||
        restClient.login("tenant@thingsboard.org", "tenant");
 | 
			
		||||
        Device device = createDevice("mqtt_");
 | 
			
		||||
        DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId());
 | 
			
		||||
 | 
			
		||||
        MqttMessageListener listener = new MqttMessageListener();
 | 
			
		||||
        MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
 | 
			
		||||
        mqttClient.on("v1/devices/me/rpc/request/+", listener, MqttQoS.AT_LEAST_ONCE);
 | 
			
		||||
 | 
			
		||||
        // Send an RPC from the server
 | 
			
		||||
        JsonObject serverRpcPayload = new JsonObject();
 | 
			
		||||
        serverRpcPayload.addProperty("method", "getValue");
 | 
			
		||||
        serverRpcPayload.addProperty("params", true);
 | 
			
		||||
        ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
 | 
			
		||||
        ListenableFuture<ResponseEntity> future = service.submit(() -> {
 | 
			
		||||
            try {
 | 
			
		||||
                return restClient.getRestTemplate()
 | 
			
		||||
                        .postForEntity(HTTPS_URL + "/api/plugins/rpc/twoway/{deviceId}",
 | 
			
		||||
                                mapper.readTree(serverRpcPayload.toString()), String.class,
 | 
			
		||||
                                device.getId());
 | 
			
		||||
            } catch (IOException e) {
 | 
			
		||||
                return ResponseEntity.badRequest().build();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Wait for RPC call from the server and send the response
 | 
			
		||||
        MqttEvent requestFromServer = listener.getEvents().poll(10, TimeUnit.SECONDS);
 | 
			
		||||
 | 
			
		||||
        Assert.assertEquals("{\"method\":\"getValue\",\"params\":true}", Objects.requireNonNull(requestFromServer).getMessage());
 | 
			
		||||
 | 
			
		||||
        Integer requestId = Integer.valueOf(Objects.requireNonNull(requestFromServer).getTopic().substring("v1/devices/me/rpc/request/".length()));
 | 
			
		||||
        JsonObject clientResponse = new JsonObject();
 | 
			
		||||
        clientResponse.addProperty("response", "someResponse");
 | 
			
		||||
        // Send a response to the server's RPC request
 | 
			
		||||
        mqttClient.publish("v1/devices/me/rpc/response/" + requestId, Unpooled.wrappedBuffer(clientResponse.toString().getBytes()));
 | 
			
		||||
 | 
			
		||||
        ResponseEntity serverResponse = future.get(5, TimeUnit.SECONDS);
 | 
			
		||||
        Assert.assertTrue(serverResponse.getStatusCode().is2xxSuccessful());
 | 
			
		||||
        Assert.assertEquals(clientResponse.toString(), serverResponse.getBody());
 | 
			
		||||
 | 
			
		||||
        restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void clientSideRpc() throws Exception {
 | 
			
		||||
        restClient.login("tenant@thingsboard.org", "tenant");
 | 
			
		||||
        Device device = createDevice("mqtt_");
 | 
			
		||||
        DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId());
 | 
			
		||||
 | 
			
		||||
        MqttMessageListener listener = new MqttMessageListener();
 | 
			
		||||
        MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
 | 
			
		||||
        mqttClient.on("v1/devices/me/rpc/request/+", listener, MqttQoS.AT_LEAST_ONCE);
 | 
			
		||||
 | 
			
		||||
        // Get the default rule chain id to make it root again after test finished
 | 
			
		||||
        RuleChainId defaultRuleChainId = getDefaultRuleChainId();
 | 
			
		||||
 | 
			
		||||
        // Create a new root rule chain
 | 
			
		||||
        RuleChainId ruleChainId = createRootRuleChainForRpcResponse();
 | 
			
		||||
 | 
			
		||||
        // Send the request to the server
 | 
			
		||||
        JsonObject clientRequest = new JsonObject();
 | 
			
		||||
        clientRequest.addProperty("method", "getResponse");
 | 
			
		||||
        clientRequest.addProperty("params", true);
 | 
			
		||||
        Integer requestId = 42;
 | 
			
		||||
        mqttClient.publish("v1/devices/me/rpc/request/" + requestId, Unpooled.wrappedBuffer(clientRequest.toString().getBytes()));
 | 
			
		||||
 | 
			
		||||
        // Check the response from the server
 | 
			
		||||
        TimeUnit.SECONDS.sleep(1);
 | 
			
		||||
        MqttEvent responseFromServer = listener.getEvents().poll(1, TimeUnit.SECONDS);
 | 
			
		||||
        Integer responseId = Integer.valueOf(Objects.requireNonNull(responseFromServer).getTopic().substring("v1/devices/me/rpc/response/".length()));
 | 
			
		||||
        Assert.assertEquals(requestId, responseId);
 | 
			
		||||
        Assert.assertEquals("requestReceived", mapper.readTree(responseFromServer.getMessage()).get("response").asText());
 | 
			
		||||
 | 
			
		||||
        // Make the default rule chain a root again
 | 
			
		||||
        ResponseEntity<RuleChain> rootRuleChainResponse = restClient.getRestTemplate()
 | 
			
		||||
                .postForEntity(HTTPS_URL + "/api/ruleChain/{ruleChainId}/root",
 | 
			
		||||
                        null,
 | 
			
		||||
                        RuleChain.class,
 | 
			
		||||
                        defaultRuleChainId);
 | 
			
		||||
        Assert.assertTrue(rootRuleChainResponse.getStatusCode().is2xxSuccessful());
 | 
			
		||||
 | 
			
		||||
        // Delete the created rule chain
 | 
			
		||||
        restClient.getRestTemplate().delete(HTTPS_URL + "/api/ruleChain/{ruleChainId}", ruleChainId);
 | 
			
		||||
        restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private RuleChainId createRootRuleChainForRpcResponse() throws Exception {
 | 
			
		||||
        RuleChain newRuleChain = new RuleChain();
 | 
			
		||||
        newRuleChain.setName("testRuleChain");
 | 
			
		||||
        ResponseEntity<RuleChain> ruleChainResponse = restClient.getRestTemplate()
 | 
			
		||||
                .postForEntity(HTTPS_URL + "/api/ruleChain",
 | 
			
		||||
                        newRuleChain,
 | 
			
		||||
                        RuleChain.class);
 | 
			
		||||
        Assert.assertTrue(ruleChainResponse.getStatusCode().is2xxSuccessful());
 | 
			
		||||
        RuleChain ruleChain = ruleChainResponse.getBody();
 | 
			
		||||
 | 
			
		||||
        JsonNode configuration = mapper.readTree(this.getClass().getClassLoader().getResourceAsStream("RpcResponseRuleChainMetadata.json"));
 | 
			
		||||
        RuleChainMetaData ruleChainMetaData = new RuleChainMetaData();
 | 
			
		||||
        ruleChainMetaData.setRuleChainId(ruleChain.getId());
 | 
			
		||||
        ruleChainMetaData.setFirstNodeIndex(configuration.get("firstNodeIndex").asInt());
 | 
			
		||||
        ruleChainMetaData.setNodes(Arrays.asList(mapper.treeToValue(configuration.get("nodes"), RuleNode[].class)));
 | 
			
		||||
        ruleChainMetaData.setConnections(Arrays.asList(mapper.treeToValue(configuration.get("connections"), NodeConnectionInfo[].class)));
 | 
			
		||||
 | 
			
		||||
        ResponseEntity<RuleChainMetaData> ruleChainMetadataResponse = restClient.getRestTemplate()
 | 
			
		||||
                .postForEntity(HTTPS_URL + "/api/ruleChain/metadata",
 | 
			
		||||
                        ruleChainMetaData,
 | 
			
		||||
                        RuleChainMetaData.class);
 | 
			
		||||
        Assert.assertTrue(ruleChainMetadataResponse.getStatusCode().is2xxSuccessful());
 | 
			
		||||
 | 
			
		||||
        // Set a new rule chain as root
 | 
			
		||||
        ResponseEntity<RuleChain> rootRuleChainResponse = restClient.getRestTemplate()
 | 
			
		||||
                .postForEntity(HTTPS_URL + "/api/ruleChain/{ruleChainId}/root",
 | 
			
		||||
                        null,
 | 
			
		||||
                        RuleChain.class,
 | 
			
		||||
                        ruleChain.getId());
 | 
			
		||||
        Assert.assertTrue(rootRuleChainResponse.getStatusCode().is2xxSuccessful());
 | 
			
		||||
 | 
			
		||||
        return ruleChain.getId();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private RuleChainId getDefaultRuleChainId() {
 | 
			
		||||
        ResponseEntity<TextPageData<RuleChain>> ruleChains = restClient.getRestTemplate().exchange(
 | 
			
		||||
                HTTPS_URL + "/api/ruleChains?limit=40&textSearch=",
 | 
			
		||||
                HttpMethod.GET,
 | 
			
		||||
                null,
 | 
			
		||||
                new ParameterizedTypeReference<TextPageData<RuleChain>>() {
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
        Optional<RuleChain> defaultRuleChain = ruleChains.getBody().getData()
 | 
			
		||||
                .stream()
 | 
			
		||||
                .filter(RuleChain::isRoot)
 | 
			
		||||
                .findFirst();
 | 
			
		||||
        if (!defaultRuleChain.isPresent()) {
 | 
			
		||||
            Assert.fail("Root rule chain wasn't found");
 | 
			
		||||
        }
 | 
			
		||||
        return defaultRuleChain.get().getId();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private MqttClient getMqttClient(DeviceCredentials deviceCredentials, MqttMessageListener listener) throws InterruptedException {
 | 
			
		||||
        MqttClientConfig clientConfig = new MqttClientConfig();
 | 
			
		||||
        clientConfig.setClientId("MQTT client from test");
 | 
			
		||||
        clientConfig.setUsername(deviceCredentials.getCredentialsId());
 | 
			
		||||
        MqttClient mqttClient = MqttClient.create(clientConfig, listener);
 | 
			
		||||
        mqttClient.connect("localhost", 1883).sync();
 | 
			
		||||
        return mqttClient;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Data
 | 
			
		||||
    private class MqttMessageListener implements MqttHandler {
 | 
			
		||||
        private final BlockingQueue<MqttEvent> events;
 | 
			
		||||
 | 
			
		||||
        private MqttMessageListener() {
 | 
			
		||||
            events = new ArrayBlockingQueue<>(100);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onMessage(String topic, ByteBuf message) {
 | 
			
		||||
            log.info("MQTT message [{}], topic [{}]", message.toString(StandardCharsets.UTF_8), topic);
 | 
			
		||||
            events.add(new MqttEvent(topic, message.toString(StandardCharsets.UTF_8)));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Data
 | 
			
		||||
    private class MqttEvent {
 | 
			
		||||
        private final String topic;
 | 
			
		||||
        private final String message;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,26 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2018 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * 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 org.thingsboard.server.msa.mapper;
 | 
			
		||||
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
@Data
 | 
			
		||||
public class AttributesResponse {
 | 
			
		||||
    private Map<String, Object> client;
 | 
			
		||||
    private Map<String, Object> shared;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,40 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2018 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * 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 org.thingsboard.server.msa.mapper;
 | 
			
		||||
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
 | 
			
		||||
import java.io.Serializable;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
@Data
 | 
			
		||||
public class WsTelemetryResponse implements Serializable {
 | 
			
		||||
    private int subscriptionId;
 | 
			
		||||
    private int errorCode;
 | 
			
		||||
    private String errorMsg;
 | 
			
		||||
    private Map<String, List<List<Object>>> data;
 | 
			
		||||
    private Map<String, Object> latestValues;
 | 
			
		||||
 | 
			
		||||
    public List<Object> getDataValuesByKey(String key) {
 | 
			
		||||
        return data.entrySet().stream()
 | 
			
		||||
                .filter(e -> e.getKey().equals(key))
 | 
			
		||||
                .flatMap(e -> e.getValue().stream().flatMap(Collection::stream))
 | 
			
		||||
                .collect(Collectors.toList());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,59 @@
 | 
			
		||||
{
 | 
			
		||||
  "firstNodeIndex": 0,
 | 
			
		||||
  "nodes": [
 | 
			
		||||
    {
 | 
			
		||||
      "additionalInfo": {
 | 
			
		||||
        "layoutX": 325,
 | 
			
		||||
        "layoutY": 150
 | 
			
		||||
      },
 | 
			
		||||
      "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode",
 | 
			
		||||
      "name": "msgTypeSwitch",
 | 
			
		||||
      "debugMode": true,
 | 
			
		||||
      "configuration": {
 | 
			
		||||
        "version": 0
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "additionalInfo": {
 | 
			
		||||
        "layoutX": 60,
 | 
			
		||||
        "layoutY": 300
 | 
			
		||||
      },
 | 
			
		||||
      "type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode",
 | 
			
		||||
      "name": "formResponse",
 | 
			
		||||
      "debugMode": true,
 | 
			
		||||
      "configuration": {
 | 
			
		||||
        "jsScript": "if (msg.method == \"getResponse\") {\n    return {msg: {\"response\": \"requestReceived\"}, metadata: metadata, msgType: msgType};\n}\n\nreturn {msg: msg, metadata: metadata, msgType: msgType};"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "additionalInfo": {
 | 
			
		||||
        "layoutX": 450,
 | 
			
		||||
        "layoutY": 300
 | 
			
		||||
      },
 | 
			
		||||
      "type": "org.thingsboard.rule.engine.rpc.TbSendRPCReplyNode",
 | 
			
		||||
      "name": "rpcReply",
 | 
			
		||||
      "debugMode": true,
 | 
			
		||||
      "configuration": {
 | 
			
		||||
        "requestIdMetaDataAttribute": "requestId"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "connections": [
 | 
			
		||||
    {
 | 
			
		||||
      "fromIndex": 0,
 | 
			
		||||
      "toIndex": 1,
 | 
			
		||||
      "type": "RPC Request from Device"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "fromIndex": 1,
 | 
			
		||||
      "toIndex": 2,
 | 
			
		||||
      "type": "Success"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "fromIndex": 1,
 | 
			
		||||
      "toIndex": 2,
 | 
			
		||||
      "type": "Failure"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "ruleChainConnections": null
 | 
			
		||||
}
 | 
			
		||||
@ -16,7 +16,7 @@
 | 
			
		||||
 | 
			
		||||
-->
 | 
			
		||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
			
		||||
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 | 
			
		||||
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 | 
			
		||||
    <modelVersion>4.0.0</modelVersion>
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.thingsboard</groupId>
 | 
			
		||||
@ -41,6 +41,7 @@
 | 
			
		||||
        <module>web-ui</module>
 | 
			
		||||
        <module>tb-node</module>
 | 
			
		||||
        <module>transport</module>
 | 
			
		||||
        <module>black-box-tests</module>
 | 
			
		||||
    </modules>
 | 
			
		||||
 | 
			
		||||
    <build>
 | 
			
		||||
 | 
			
		||||
@ -25,11 +25,23 @@ Where:
 | 
			
		||||
- `-v ~/.mytb-data:/data`   - mounts the host's dir `~/.mytb-data` to ThingsBoard DataBase data directory
 | 
			
		||||
- `--name mytb`             - friendly local name of this machine
 | 
			
		||||
- `thingsboard/tb`          - docker image, can be also `thingsboard/tb-postgres` or `thingsboard/tb-cassandra`
 | 
			
		||||
    
 | 
			
		||||
After executing this command you can open `http://{your-host-ip}:9090` in you browser (for ex. `http://localhost:9090`). You should see ThingsBoard login page.
 | 
			
		||||
 | 
			
		||||
> **NOTE**: **Windows** users should use docker managed volume instead of host's dir. Create docker volume (for ex. `mytb-data`) before executing `docker run` command:
 | 
			
		||||
> ```
 | 
			
		||||
> $ docker create volume mytb-data
 | 
			
		||||
> ```
 | 
			
		||||
> After you can execute docker run command using `mytb-data` volume instead of `~/.mytb-data`.
 | 
			
		||||
> In order to get access to necessary resources from external IP/Host on **Windows** machine, please execute the following commands:
 | 
			
		||||
> ```
 | 
			
		||||
> $ VBoxManage controlvm "default" natpf1 "tcp-port9090,tcp,,9090,,9090"  
 | 
			
		||||
> $ VBoxManage controlvm "default" natpf1 "tcp-port1883,tcp,,1883,,1883"
 | 
			
		||||
> $ VBoxManage controlvm "default" natpf1 "tcp-port5683,tcp,,5683,,5683"
 | 
			
		||||
> ```
 | 
			
		||||
 | 
			
		||||
After executing `docker run` command you can open `http://{your-host-ip}:9090` in you browser (for ex. `http://localhost:9090`). You should see ThingsBoard login page.
 | 
			
		||||
Use the following default credentials:
 | 
			
		||||
 | 
			
		||||
- **Systen Administrator**: sysadmin@thingsboard.org / sysadmin
 | 
			
		||||
- **System Administrator**: sysadmin@thingsboard.org / sysadmin
 | 
			
		||||
- **Tenant Administrator**: tenant@thingsboard.org / tenant
 | 
			
		||||
- **Customer User**: customer@thingsboard.org / customer
 | 
			
		||||
    
 | 
			
		||||
@ -39,21 +51,21 @@ You can detach from session terminal with `Ctrl-p` `Ctrl-q` - the container will
 | 
			
		||||
 | 
			
		||||
To reattach to the terminal (to see ThingsBoard logs) run:
 | 
			
		||||
 | 
			
		||||
`
 | 
			
		||||
```
 | 
			
		||||
$ docker attach mytb
 | 
			
		||||
`
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
To stop the container:
 | 
			
		||||
 | 
			
		||||
`
 | 
			
		||||
```
 | 
			
		||||
$ docker stop mytb
 | 
			
		||||
`
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
To start the container:
 | 
			
		||||
 | 
			
		||||
`
 | 
			
		||||
```
 | 
			
		||||
$ docker start mytb
 | 
			
		||||
`
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Upgrading
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -15,10 +15,13 @@
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import tinycolor from 'tinycolor2';
 | 
			
		||||
import moment from 'moment';
 | 
			
		||||
 | 
			
		||||
export default angular.module('thingsboard.thirdpartyFix', [])
 | 
			
		||||
    .factory('Fullscreen', Fullscreen)
 | 
			
		||||
    .factory('$mdColorPicker', mdColorPicker)
 | 
			
		||||
    .provider('$mdpDatePicker', mdpDatePicker)
 | 
			
		||||
    .provider('$mdpTimePicker', mdpTimePicker)
 | 
			
		||||
    .name;
 | 
			
		||||
 | 
			
		||||
/*@ngInject*/
 | 
			
		||||
@ -193,3 +196,264 @@ function mdColorPicker($q, $mdDialog, mdColorPickerHistory) {
 | 
			
		||||
 | 
			
		||||
    /* eslint-enable angular/definedundefined */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DatePickerCtrl($scope, $mdDialog, $mdMedia, $timeout, currentDate, options) {
 | 
			
		||||
    var self = this;
 | 
			
		||||
 | 
			
		||||
    this.date = moment(currentDate);
 | 
			
		||||
    this.minDate = options.minDate && moment(options.minDate).isValid() ? moment(options.minDate) : null;
 | 
			
		||||
    this.maxDate = options.maxDate && moment(options.maxDate).isValid() ? moment(options.maxDate) : null;
 | 
			
		||||
    this.displayFormat = options.displayFormat || "ddd, MMM DD";
 | 
			
		||||
    this.dateFilter = angular.isFunction(options.dateFilter) ? options.dateFilter : null;
 | 
			
		||||
    this.selectingYear = false;
 | 
			
		||||
 | 
			
		||||
    // validate min and max date
 | 
			
		||||
    if (this.minDate && this.maxDate) {
 | 
			
		||||
        if (this.maxDate.isBefore(this.minDate)) {
 | 
			
		||||
            this.maxDate = moment(this.minDate).add(1, 'days');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.date) {
 | 
			
		||||
        // check min date
 | 
			
		||||
        if (this.minDate && this.date.isBefore(this.minDate)) {
 | 
			
		||||
            this.date = moment(this.minDate);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // check max date
 | 
			
		||||
        if (this.maxDate && this.date.isAfter(this.maxDate)) {
 | 
			
		||||
            this.date = moment(this.maxDate);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.yearItems = {
 | 
			
		||||
        currentIndex_: 0,
 | 
			
		||||
        PAGE_SIZE: 5,
 | 
			
		||||
        START: (self.minDate ? self.minDate.year() : 1900),
 | 
			
		||||
        END: (self.maxDate ? self.maxDate.year() : 0),
 | 
			
		||||
        getItemAtIndex: function(index) {
 | 
			
		||||
            if(this.currentIndex_ < index)
 | 
			
		||||
                this.currentIndex_ = index;
 | 
			
		||||
 | 
			
		||||
            return this.START + index;
 | 
			
		||||
        },
 | 
			
		||||
        getLength: function() {
 | 
			
		||||
            return Math.min(
 | 
			
		||||
                this.currentIndex_ + Math.floor(this.PAGE_SIZE / 2),
 | 
			
		||||
                Math.abs(this.START - this.END) + 1
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    $scope.$mdMedia = $mdMedia;
 | 
			
		||||
    $scope.year = this.date.year();
 | 
			
		||||
 | 
			
		||||
    this.selectYear = function(year) {
 | 
			
		||||
        self.date.year(year);
 | 
			
		||||
        $scope.year = year;
 | 
			
		||||
        self.selectingYear = false;
 | 
			
		||||
        self.animate();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.showYear = function() {
 | 
			
		||||
        self.yearTopIndex = (self.date.year() - self.yearItems.START) + Math.floor(self.yearItems.PAGE_SIZE / 2);
 | 
			
		||||
        self.yearItems.currentIndex_ = (self.date.year() - self.yearItems.START) + 1;
 | 
			
		||||
        self.selectingYear = true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.showCalendar = function() {
 | 
			
		||||
        self.selectingYear = false;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.cancel = function() {
 | 
			
		||||
        $mdDialog.cancel();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.confirm = function() {
 | 
			
		||||
        var date = this.date;
 | 
			
		||||
 | 
			
		||||
        if (this.minDate && this.date.isBefore(this.minDate)) {
 | 
			
		||||
            date = moment(this.minDate);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.maxDate && this.date.isAfter(this.maxDate)) {
 | 
			
		||||
            date = moment(this.maxDate);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $mdDialog.hide(date.toDate());
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.animate = function() {
 | 
			
		||||
        self.animating = true;
 | 
			
		||||
        $timeout(angular.noop).then(function() {
 | 
			
		||||
            self.animating = false;
 | 
			
		||||
        })
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*@ngInject*/
 | 
			
		||||
function mdpDatePicker() {
 | 
			
		||||
    var LABEL_OK = "OK",
 | 
			
		||||
        LABEL_CANCEL = "Cancel",
 | 
			
		||||
        DISPLAY_FORMAT = "ddd, MMM DD";
 | 
			
		||||
 | 
			
		||||
    this.setDisplayFormat = function(format) {
 | 
			
		||||
        DISPLAY_FORMAT = format;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.setOKButtonLabel = function(label) {
 | 
			
		||||
        LABEL_OK = label;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.setCancelButtonLabel = function(label) {
 | 
			
		||||
        LABEL_CANCEL = label;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /*@ngInject*/
 | 
			
		||||
    this.$get = function($mdDialog) {
 | 
			
		||||
        var datePicker = function(currentDate, options) {
 | 
			
		||||
            if (!angular.isDate(currentDate)) currentDate = Date.now();
 | 
			
		||||
            if (!angular.isObject(options)) options = {};
 | 
			
		||||
 | 
			
		||||
            options.displayFormat = DISPLAY_FORMAT;
 | 
			
		||||
 | 
			
		||||
            return $mdDialog.show({
 | 
			
		||||
                controller:  ['$scope', '$mdDialog', '$mdMedia', '$timeout', 'currentDate', 'options', DatePickerCtrl],
 | 
			
		||||
                controllerAs: 'datepicker',
 | 
			
		||||
                clickOutsideToClose: true,
 | 
			
		||||
                template: '<md-dialog aria-label="" class="mdp-datepicker" ng-class="{ \'portrait\': !$mdMedia(\'gt-xs\') }">' +
 | 
			
		||||
                '<md-dialog-content layout="row" layout-wrap>' +
 | 
			
		||||
                '<div layout="column" layout-align="start center">' +
 | 
			
		||||
                '<md-toolbar layout-align="start start" flex class="mdp-datepicker-date-wrapper md-hue-1 md-primary" layout="column">' +
 | 
			
		||||
                '<span class="mdp-datepicker-year" ng-click="datepicker.showYear()" ng-class="{ \'active\': datepicker.selectingYear }">{{ datepicker.date.format(\'YYYY\') }}</span>' +
 | 
			
		||||
                '<span class="mdp-datepicker-date" ng-click="datepicker.showCalendar()" ng-class="{ \'active\': !datepicker.selectingYear }">{{ datepicker.date.format(datepicker.displayFormat) }}</span> ' +
 | 
			
		||||
                '</md-toolbar>' +
 | 
			
		||||
                '</div>' +
 | 
			
		||||
                '<div>' +
 | 
			
		||||
                '<div class="mdp-datepicker-select-year mdp-animation-zoom" layout="column" layout-align="center start" ng-if="datepicker.selectingYear">' +
 | 
			
		||||
                '<md-virtual-repeat-container md-auto-shrink md-top-index="datepicker.yearTopIndex">' +
 | 
			
		||||
                '<div flex md-virtual-repeat="item in datepicker.yearItems" md-on-demand class="repeated-year">' +
 | 
			
		||||
                '<span class="md-button" ng-click="datepicker.selectYear(item)" md-ink-ripple ng-class="{ \'md-primary current\': item == year }">{{ item }}</span>' +
 | 
			
		||||
                '</div>' +
 | 
			
		||||
                '</md-virtual-repeat-container>' +
 | 
			
		||||
                '</div>' +
 | 
			
		||||
                '<mdp-calendar ng-if="!datepicker.selectingYear" class="mdp-animation-zoom" date="datepicker.date" min-date="datepicker.minDate" date-filter="datepicker.dateFilter" max-date="datepicker.maxDate"></mdp-calendar>' +
 | 
			
		||||
                '<md-dialog-actions layout="row">' +
 | 
			
		||||
                '<span flex></span>' +
 | 
			
		||||
                '<md-button ng-click="datepicker.cancel()" aria-label="' + LABEL_CANCEL + '">' + LABEL_CANCEL + '</md-button>' +
 | 
			
		||||
                '<md-button ng-click="datepicker.confirm()" class="md-primary" aria-label="' + LABEL_OK + '">' + LABEL_OK + '</md-button>' +
 | 
			
		||||
                '</md-dialog-actions>' +
 | 
			
		||||
                '</div>' +
 | 
			
		||||
                '</md-dialog-content>' +
 | 
			
		||||
                '</md-dialog>',
 | 
			
		||||
                targetEvent: options.targetEvent,
 | 
			
		||||
                locals: {
 | 
			
		||||
                    currentDate: currentDate,
 | 
			
		||||
                    options: options
 | 
			
		||||
                },
 | 
			
		||||
                multiple: true
 | 
			
		||||
            });
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return datePicker;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TimePickerCtrl($scope, $mdDialog, time, autoSwitch, $mdMedia) {
 | 
			
		||||
    var self = this;
 | 
			
		||||
    this.VIEW_HOURS = 1;
 | 
			
		||||
    this.VIEW_MINUTES = 2;
 | 
			
		||||
    this.currentView = this.VIEW_HOURS;
 | 
			
		||||
    this.time = moment(time);
 | 
			
		||||
    this.autoSwitch = !!autoSwitch;
 | 
			
		||||
 | 
			
		||||
    this.clockHours = parseInt(this.time.format("h"));
 | 
			
		||||
    this.clockMinutes = parseInt(this.time.minutes());
 | 
			
		||||
 | 
			
		||||
    $scope.$mdMedia = $mdMedia;
 | 
			
		||||
 | 
			
		||||
    this.switchView = function() {
 | 
			
		||||
        self.currentView = self.currentView == self.VIEW_HOURS ? self.VIEW_MINUTES : self.VIEW_HOURS;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.setAM = function() {
 | 
			
		||||
        if(self.time.hours() >= 12)
 | 
			
		||||
            self.time.hour(self.time.hour() - 12);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.setPM = function() {
 | 
			
		||||
        if(self.time.hours() < 12)
 | 
			
		||||
            self.time.hour(self.time.hour() + 12);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.cancel = function() {
 | 
			
		||||
        $mdDialog.cancel();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.confirm = function() {
 | 
			
		||||
        $mdDialog.hide(this.time.toDate());
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*@ngInject*/
 | 
			
		||||
function mdpTimePicker() {
 | 
			
		||||
    var LABEL_OK = "OK",
 | 
			
		||||
        LABEL_CANCEL = "Cancel";
 | 
			
		||||
 | 
			
		||||
    this.setOKButtonLabel = function(label) {
 | 
			
		||||
        LABEL_OK = label;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.setCancelButtonLabel = function(label) {
 | 
			
		||||
        LABEL_CANCEL = label;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /*@ngInject*/
 | 
			
		||||
    this.$get = function($mdDialog) {
 | 
			
		||||
        var timePicker = function(time, options) {
 | 
			
		||||
            if(!angular.isDate(time)) time = Date.now();
 | 
			
		||||
            if (!angular.isObject(options)) options = {};
 | 
			
		||||
 | 
			
		||||
            return $mdDialog.show({
 | 
			
		||||
                controller:  ['$scope', '$mdDialog', 'time', 'autoSwitch', '$mdMedia', TimePickerCtrl],
 | 
			
		||||
                controllerAs: 'timepicker',
 | 
			
		||||
                clickOutsideToClose: true,
 | 
			
		||||
                template: '<md-dialog aria-label="" class="mdp-timepicker" ng-class="{ \'portrait\': !$mdMedia(\'gt-xs\') }">' +
 | 
			
		||||
                '<md-dialog-content layout-gt-xs="row" layout-wrap>' +
 | 
			
		||||
                '<md-toolbar layout-gt-xs="column" layout-xs="row" layout-align="center center" flex class="mdp-timepicker-time md-hue-1 md-primary">' +
 | 
			
		||||
                '<div class="mdp-timepicker-selected-time">' +
 | 
			
		||||
                '<span ng-class="{ \'active\': timepicker.currentView == timepicker.VIEW_HOURS }" ng-click="timepicker.currentView = timepicker.VIEW_HOURS">{{ timepicker.time.format("h") }}</span>:' +
 | 
			
		||||
                '<span ng-class="{ \'active\': timepicker.currentView == timepicker.VIEW_MINUTES }" ng-click="timepicker.currentView = timepicker.VIEW_MINUTES">{{ timepicker.time.format("mm") }}</span>' +
 | 
			
		||||
                '</div>' +
 | 
			
		||||
                '<div layout="column" class="mdp-timepicker-selected-ampm">' +
 | 
			
		||||
                '<span ng-click="timepicker.setAM()" ng-class="{ \'active\': timepicker.time.hours() < 12 }">AM</span>' +
 | 
			
		||||
                '<span ng-click="timepicker.setPM()" ng-class="{ \'active\': timepicker.time.hours() >= 12 }">PM</span>' +
 | 
			
		||||
                '</div>' +
 | 
			
		||||
                '</md-toolbar>' +
 | 
			
		||||
                '<div>' +
 | 
			
		||||
                '<div class="mdp-clock-switch-container" ng-switch="timepicker.currentView" layout layout-align="center center">' +
 | 
			
		||||
                '<mdp-clock class="mdp-animation-zoom" auto-switch="timepicker.autoSwitch" time="timepicker.time" type="hours" ng-switch-when="1"></mdp-clock>' +
 | 
			
		||||
                '<mdp-clock class="mdp-animation-zoom" auto-switch="timepicker.autoSwitch" time="timepicker.time" type="minutes" ng-switch-when="2"></mdp-clock>' +
 | 
			
		||||
                '</div>' +
 | 
			
		||||
 | 
			
		||||
                '<md-dialog-actions layout="row">' +
 | 
			
		||||
                '<span flex></span>' +
 | 
			
		||||
                '<md-button ng-click="timepicker.cancel()" aria-label="' + LABEL_CANCEL + '">' + LABEL_CANCEL + '</md-button>' +
 | 
			
		||||
                '<md-button ng-click="timepicker.confirm()" class="md-primary" aria-label="' + LABEL_OK + '">' + LABEL_OK + '</md-button>' +
 | 
			
		||||
                '</md-dialog-actions>' +
 | 
			
		||||
                '</div>' +
 | 
			
		||||
                '</md-dialog-content>' +
 | 
			
		||||
                '</md-dialog>',
 | 
			
		||||
                targetEvent: options.targetEvent,
 | 
			
		||||
                locals: {
 | 
			
		||||
                    time: time,
 | 
			
		||||
                    autoSwitch: options.autoSwitch
 | 
			
		||||
                },
 | 
			
		||||
                multiple: true
 | 
			
		||||
            });
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return timePicker;
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -74,6 +74,7 @@
 | 
			
		||||
                        <md-icon aria-label="{{ 'dashboard.settings' | translate }}" class="material-icons">settings</md-icon>
 | 
			
		||||
                    </md-button>
 | 
			
		||||
                    <tb-dashboard-select   ng-show="!vm.isEdit && !vm.widgetEditMode && vm.displayDashboardsSelect()"
 | 
			
		||||
                                           md-theme="tb-dark"
 | 
			
		||||
                                           ng-model="vm.currentDashboardId"
 | 
			
		||||
                                           dashboards-scope="{{vm.currentDashboardScope}}"
 | 
			
		||||
                                           customer-id="vm.currentCustomerId">
 | 
			
		||||
 | 
			
		||||
@ -98,8 +98,8 @@ export default function EntityViewDirective($q, $compile, $templateCache, $filte
 | 
			
		||||
                if (newDate.getTime() > scope.maxStartTimeMs) {
 | 
			
		||||
                    scope.startTimeMs = angular.copy(scope.maxStartTimeMs);
 | 
			
		||||
                }
 | 
			
		||||
                updateMinMaxDates();
 | 
			
		||||
            }
 | 
			
		||||
            updateMinMaxDates();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        scope.$watch('endTimeMs', function (newDate) {
 | 
			
		||||
@ -107,18 +107,24 @@ export default function EntityViewDirective($q, $compile, $templateCache, $filte
 | 
			
		||||
                if (newDate.getTime() < scope.minEndTimeMs) {
 | 
			
		||||
                    scope.endTimeMs = angular.copy(scope.minEndTimeMs);
 | 
			
		||||
                }
 | 
			
		||||
                updateMinMaxDates();
 | 
			
		||||
            }
 | 
			
		||||
            updateMinMaxDates();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        function updateMinMaxDates() {
 | 
			
		||||
            if (scope.endTimeMs) {
 | 
			
		||||
                scope.maxStartTimeMs = angular.copy(new Date(scope.endTimeMs.getTime()));
 | 
			
		||||
                scope.entityView.endTimeMs = scope.endTimeMs.getTime();
 | 
			
		||||
            }
 | 
			
		||||
            if (scope.startTimeMs) {
 | 
			
		||||
                scope.minEndTimeMs = angular.copy(new Date(scope.startTimeMs.getTime()));
 | 
			
		||||
                scope.entityView.startTimeMs = scope.startTimeMs.getTime();
 | 
			
		||||
            if (scope.entityView) {
 | 
			
		||||
                if (scope.endTimeMs) {
 | 
			
		||||
                    scope.maxStartTimeMs = angular.copy(new Date(scope.endTimeMs.getTime()));
 | 
			
		||||
                    scope.entityView.endTimeMs = scope.endTimeMs.getTime();
 | 
			
		||||
                } else {
 | 
			
		||||
                    scope.entityView.endTimeMs = 0;
 | 
			
		||||
                }
 | 
			
		||||
                if (scope.startTimeMs) {
 | 
			
		||||
                    scope.minEndTimeMs = angular.copy(new Date(scope.startTimeMs.getTime()));
 | 
			
		||||
                    scope.entityView.startTimeMs = scope.startTimeMs.getTime();
 | 
			
		||||
                } else {
 | 
			
		||||
                    scope.entityView.startTimeMs = 0;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -774,6 +774,7 @@
 | 
			
		||||
    },
 | 
			
		||||
    "entity-view": {
 | 
			
		||||
        "entity-view": "Entity View",
 | 
			
		||||
        "entity-view-required": "Entity view is required.",
 | 
			
		||||
        "entity-views": "Entity Views",
 | 
			
		||||
        "management": "Entity View management",
 | 
			
		||||
        "view-entity-views": "View Entity Views",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user