Commit 5aa055d73d338931a6dd014a6c3a6d67c5c94258

Authored by Andrii Shvaika
1 parent 4899c3c3

Websocket refactoring

@@ -304,6 +304,11 @@ @@ -304,6 +304,11 @@
304 <groupId>com.github.ua-parser</groupId> 304 <groupId>com.github.ua-parser</groupId>
305 <artifactId>uap-java</artifactId> 305 <artifactId>uap-java</artifactId>
306 </dependency> 306 </dependency>
  307 + <dependency>
  308 + <groupId>org.java-websocket</groupId>
  309 + <artifactId>Java-WebSocket</artifactId>
  310 + <scope>test</scope>
  311 + </dependency>
307 </dependencies> 312 </dependencies>
308 313
309 <build> 314 <build>
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.subscription;
  17 +
  18 +import com.google.common.util.concurrent.Futures;
  19 +import com.google.common.util.concurrent.ListenableFuture;
  20 +import lombok.extern.slf4j.Slf4j;
  21 +import org.springframework.beans.factory.annotation.Autowired;
  22 +import org.springframework.context.annotation.Lazy;
  23 +import org.springframework.context.event.EventListener;
  24 +import org.springframework.stereotype.Service;
  25 +import org.thingsboard.common.util.ThingsBoardThreadFactory;
  26 +import org.thingsboard.server.common.data.EntityView;
  27 +import org.thingsboard.server.common.data.id.CustomerId;
  28 +import org.thingsboard.server.common.data.id.EntityViewId;
  29 +import org.thingsboard.server.common.data.id.TenantId;
  30 +import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
  31 +import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
  32 +import org.thingsboard.server.common.data.kv.TsKvEntry;
  33 +import org.thingsboard.server.common.data.page.PageData;
  34 +import org.thingsboard.server.common.data.query.EntityData;
  35 +import org.thingsboard.server.common.data.query.EntityDataQuery;
  36 +import org.thingsboard.server.common.data.query.TsValue;
  37 +import org.thingsboard.server.common.msg.queue.ServiceType;
  38 +import org.thingsboard.server.common.msg.queue.TbCallback;
  39 +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
  40 +import org.thingsboard.server.dao.entity.EntityService;
  41 +import org.thingsboard.server.dao.entityview.EntityViewService;
  42 +import org.thingsboard.server.dao.timeseries.TimeseriesService;
  43 +import org.thingsboard.server.queue.discovery.ClusterTopologyChangeEvent;
  44 +import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
  45 +import org.thingsboard.server.queue.discovery.PartitionService;
  46 +import org.thingsboard.server.queue.util.TbCoreComponent;
  47 +import org.thingsboard.server.service.queue.TbClusterService;
  48 +import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
  49 +import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
  50 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
  51 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd;
  52 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
  53 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd;
  54 +import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd;
  55 +import org.thingsboard.server.service.telemetry.cmd.v2.TimeSeriesCmd;
  56 +import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate;
  57 +
  58 +import javax.annotation.PostConstruct;
  59 +import javax.annotation.PreDestroy;
  60 +import java.util.ArrayList;
  61 +import java.util.HashMap;
  62 +import java.util.LinkedHashMap;
  63 +import java.util.List;
  64 +import java.util.Map;
  65 +import java.util.Set;
  66 +import java.util.concurrent.ConcurrentHashMap;
  67 +import java.util.concurrent.ExecutionException;
  68 +import java.util.concurrent.ExecutorService;
  69 +import java.util.concurrent.Executors;
  70 +import java.util.function.Function;
  71 +import java.util.stream.Collectors;
  72 +
  73 +@Slf4j
  74 +@TbCoreComponent
  75 +@Service
  76 +public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubscriptionService {
  77 +
  78 + private static final int DEFAULT_LIMIT = 100;
  79 + private final Set<TopicPartitionInfo> currentPartitions = ConcurrentHashMap.newKeySet();
  80 + private final Map<String, Map<Integer, TbSubscription>> subscriptionsBySessionId = new ConcurrentHashMap<>();
  81 +
  82 + @Autowired
  83 + private TelemetryWebSocketService wsService;
  84 +
  85 + @Autowired
  86 + private EntityViewService entityViewService;
  87 +
  88 + @Autowired
  89 + private EntityService entityService;
  90 +
  91 + @Autowired
  92 + private PartitionService partitionService;
  93 +
  94 + @Autowired
  95 + private TbClusterService clusterService;
  96 +
  97 + @Autowired
  98 + @Lazy
  99 + private SubscriptionManagerService subscriptionManagerService;
  100 +
  101 + @Autowired
  102 + private TimeseriesService tsService;
  103 +
  104 + private ExecutorService wsCallBackExecutor;
  105 +
  106 + @PostConstruct
  107 + public void initExecutor() {
  108 + wsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ws-entity-sub-callback"));
  109 + }
  110 +
  111 + @PreDestroy
  112 + public void shutdownExecutor() {
  113 + if (wsCallBackExecutor != null) {
  114 + wsCallBackExecutor.shutdownNow();
  115 + }
  116 + }
  117 +
  118 + @Override
  119 + @EventListener(PartitionChangeEvent.class)
  120 + public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) {
  121 + if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) {
  122 + currentPartitions.clear();
  123 + currentPartitions.addAll(partitionChangeEvent.getPartitions());
  124 + }
  125 + }
  126 +
  127 + @Override
  128 + @EventListener(ClusterTopologyChangeEvent.class)
  129 + public void onApplicationEvent(ClusterTopologyChangeEvent event) {
  130 + if (event.getServiceQueueKeys().stream().anyMatch(key -> ServiceType.TB_CORE.equals(key.getServiceType()))) {
  131 + /*
  132 + * If the cluster topology has changed, we need to push all current subscriptions to SubscriptionManagerService again.
  133 + * Otherwise, the SubscriptionManagerService may "forget" those subscriptions in case of restart.
  134 + * Although this is resource consuming operation, it is cheaper than sending ping/pong commands periodically
  135 + * It is also cheaper then caching the subscriptions by entity id and then lookup of those caches every time we have new telemetry in SubscriptionManagerService.
  136 + * Even if we cache locally the list of active subscriptions by entity id, it is still time consuming operation to get them from cache
  137 + * Since number of subscriptions is usually much less then number of devices that are pushing data.
  138 +// */
  139 +// subscriptionsBySessionId.values().forEach(map -> map.values()
  140 +// .forEach(sub -> pushSubscriptionToManagerService(sub, false)));
  141 + }
  142 + }
  143 +
  144 + @Override
  145 + public void handleCmd(TelemetryWebSocketSessionRef session, EntityDataCmd cmd) {
  146 + if (cmd.getHistoryCmd() != null) {
  147 + handleHistoryCmd(session, cmd.getCmdId(), cmd.getQuery(), cmd.getHistoryCmd());
  148 + } else if (cmd.getLatestCmd() != null) {
  149 + handleLatestCmd(session, cmd.getCmdId(), cmd.getQuery(), cmd.getLatestCmd());
  150 + } else {
  151 + handleTimeseriesCmd(session, cmd.getCmdId(), cmd.getQuery(), cmd.getTsCmd());
  152 + }
  153 + }
  154 +
  155 + private void handleTimeseriesCmd(TelemetryWebSocketSessionRef session, int cmdId, EntityDataQuery query, TimeSeriesCmd tsCmd) {
  156 + }
  157 +
  158 + private void handleLatestCmd(TelemetryWebSocketSessionRef session, int cmdId, EntityDataQuery query, LatestValueCmd latestCmd) {
  159 +
  160 + }
  161 +
  162 + private void handleHistoryCmd(TelemetryWebSocketSessionRef session, int cmdId, EntityDataQuery query, EntityHistoryCmd historyCmd) {
  163 + TenantId tenantId = session.getSecurityCtx().getTenantId();
  164 + CustomerId customerId = session.getSecurityCtx().getCustomerId();
  165 + PageData<EntityData> data = entityService.findEntityDataByQuery(tenantId, customerId, query);
  166 + List<ReadTsKvQuery> tsKvQueryList = historyCmd.getKeys().stream().map(key -> new BaseReadTsKvQuery(
  167 + key, historyCmd.getStartTs(), historyCmd.getEndTs(), historyCmd.getInterval(), getLimit(historyCmd.getLimit()), historyCmd.getAgg()
  168 + )).collect(Collectors.toList());
  169 + Map<EntityData, ListenableFuture<List<TsKvEntry>>> fetchResultMap = new HashMap<>();
  170 + data.getData().forEach(entityData -> fetchResultMap.put(entityData,
  171 + tsService.findAll(tenantId, entityData.getEntityId(), tsKvQueryList)));
  172 + Futures.allAsList(fetchResultMap.values()).addListener(() -> {
  173 + fetchResultMap.forEach((entityData, future) -> {
  174 + Map<String, List<TsValue>> keyData = new LinkedHashMap<>();
  175 + historyCmd.getKeys().forEach(key -> keyData.put(key, new ArrayList<>()));
  176 + try {
  177 + List<TsKvEntry> entityTsData = future.get();
  178 + if (entityTsData != null) {
  179 + entityTsData.forEach(entry -> keyData.get(entry.getKey()).add(new TsValue(entry.getTs(), entry.getValueAsString())));
  180 + }
  181 + keyData.forEach((k, v) -> entityData.getTimeseries().put(k, v.toArray(new TsValue[v.size()])));
  182 + } catch (InterruptedException | ExecutionException e) {
  183 + log.warn("[{}][{}][{}] Failed to fetch historical data", session.getSessionId(), cmdId, entityData.getEntityId(), e);
  184 + }
  185 + });
  186 + EntityDataUpdate update = new EntityDataUpdate(cmdId, data, null);
  187 + wsService.sendWsMsg(session.getSessionId(), update);
  188 + }, wsCallBackExecutor);
  189 + }
  190 +
  191 +
  192 + @Override
  193 + public void cancelSubscription(String sessionId, EntityDataUnsubscribeCmd subscriptionId) {
  194 +
  195 + }
  196 +
  197 +// //TODO 3.1: replace null callbacks with callbacks from websocket service.
  198 +// @Override
  199 +// public void addSubscription(TbSubscription subscription) {
  200 +// EntityId entityId = subscription.getEntityId();
  201 +// // Telemetry subscription on Entity Views are handled differently, because we need to allow only certain keys and time ranges;
  202 +// if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW) && TbSubscriptionType.TIMESERIES.equals(subscription.getType())) {
  203 +// subscription = resolveEntityViewSubscription((TbTimeseriesSubscription) subscription);
  204 +// }
  205 +// pushSubscriptionToManagerService(subscription, true);
  206 +// registerSubscription(subscription);
  207 +// }
  208 +
  209 +// private void pushSubscriptionToManagerService(TbSubscription subscription, boolean pushToLocalService) {
  210 +// TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, subscription.getTenantId(), subscription.getEntityId());
  211 +// if (currentPartitions.contains(tpi)) {
  212 +// // Subscription is managed on the same server;
  213 +// if (pushToLocalService) {
  214 +// subscriptionManagerService.addSubscription(subscription, TbCallback.EMPTY);
  215 +// }
  216 +// } else {
  217 +// // Push to the queue;
  218 +// TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toNewSubscriptionProto(subscription);
  219 +// clusterService.pushMsgToCore(tpi, subscription.getEntityId().getId(), toCoreMsg, null);
  220 +// }
  221 +// }
  222 +
  223 + @Override
  224 + public void onSubscriptionUpdate(String sessionId, SubscriptionUpdate update, TbCallback callback) {
  225 +// TbSubscription subscription = subscriptionsBySessionId
  226 +// .getOrDefault(sessionId, Collections.emptyMap()).get(update.getSubscriptionId());
  227 +// if (subscription != null) {
  228 +// switch (subscription.getType()) {
  229 +// case TIMESERIES:
  230 +// TbTimeseriesSubscription tsSub = (TbTimeseriesSubscription) subscription;
  231 +// update.getLatestValues().forEach((key, value) -> tsSub.getKeyStates().put(key, value));
  232 +// break;
  233 +// case ATTRIBUTES:
  234 +// TbAttributeSubscription attrSub = (TbAttributeSubscription) subscription;
  235 +// update.getLatestValues().forEach((key, value) -> attrSub.getKeyStates().put(key, value));
  236 +// break;
  237 +// }
  238 +// wsService.sendWsMsg(sessionId, update);
  239 +// }
  240 +// callback.onSuccess();
  241 + }
  242 +
  243 +// @Override
  244 +// public void cancelSubscription(String sessionId, int subscriptionId) {
  245 +// log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId);
  246 +// Map<Integer, TbSubscription> sessionSubscriptions = subscriptionsBySessionId.get(sessionId);
  247 +// if (sessionSubscriptions != null) {
  248 +// TbSubscription subscription = sessionSubscriptions.remove(subscriptionId);
  249 +// if (subscription != null) {
  250 +// if (sessionSubscriptions.isEmpty()) {
  251 +// subscriptionsBySessionId.remove(sessionId);
  252 +// }
  253 +// TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, subscription.getTenantId(), subscription.getEntityId());
  254 +// if (currentPartitions.contains(tpi)) {
  255 +// // Subscription is managed on the same server;
  256 +// subscriptionManagerService.cancelSubscription(sessionId, subscriptionId, TbCallback.EMPTY);
  257 +// } else {
  258 +// // Push to the queue;
  259 +// TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toCloseSubscriptionProto(subscription);
  260 +// clusterService.pushMsgToCore(tpi, subscription.getEntityId().getId(), toCoreMsg, null);
  261 +// }
  262 +// } else {
  263 +// log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId);
  264 +// }
  265 +// } else {
  266 +// log.debug("[{}] No session subscriptions found!", sessionId);
  267 +// }
  268 +// }
  269 +
  270 + @Override
  271 + public void cancelAllSessionSubscriptions(String sessionId) {
  272 +// Map<Integer, TbSubscription> subscriptions = subscriptionsBySessionId.get(sessionId);
  273 +// if (subscriptions != null) {
  274 +// Set<Integer> toRemove = new HashSet<>(subscriptions.keySet());
  275 +// toRemove.forEach(id -> cancelSubscription(sessionId, id));
  276 +// }
  277 + }
  278 +
  279 + private TbSubscription resolveEntityViewSubscription(TbTimeseriesSubscription subscription) {
  280 + EntityView entityView = entityViewService.findEntityViewById(TenantId.SYS_TENANT_ID, new EntityViewId(subscription.getEntityId().getId()));
  281 +
  282 + Map<String, Long> keyStates;
  283 + if (subscription.isAllKeys()) {
  284 + keyStates = entityView.getKeys().getTimeseries().stream().collect(Collectors.toMap(k -> k, k -> 0L));
  285 + } else {
  286 + keyStates = subscription.getKeyStates().entrySet()
  287 + .stream().filter(entry -> entityView.getKeys().getTimeseries().contains(entry.getKey()))
  288 + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
  289 + }
  290 +
  291 + return TbTimeseriesSubscription.builder()
  292 + .serviceId(subscription.getServiceId())
  293 + .sessionId(subscription.getSessionId())
  294 + .subscriptionId(subscription.getSubscriptionId())
  295 + .tenantId(subscription.getTenantId())
  296 + .entityId(entityView.getEntityId())
  297 + .startTime(entityView.getStartTimeMs())
  298 + .endTime(entityView.getEndTimeMs())
  299 + .allKeys(false)
  300 + .keyStates(keyStates).build();
  301 + }
  302 +
  303 + private void registerSubscription(TbSubscription subscription) {
  304 + Map<Integer, TbSubscription> sessionSubscriptions = subscriptionsBySessionId.computeIfAbsent(subscription.getSessionId(), k -> new ConcurrentHashMap<>());
  305 + sessionSubscriptions.put(subscription.getSubscriptionId(), subscription);
  306 + }
  307 +
  308 + private int getLimit(int limit) {
  309 + return limit == 0 ? DEFAULT_LIMIT : limit;
  310 + }
  311 +
  312 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.subscription;
  17 +
  18 +import org.thingsboard.server.common.msg.queue.TbCallback;
  19 +import org.thingsboard.server.queue.discovery.ClusterTopologyChangeEvent;
  20 +import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
  21 +import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
  22 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
  23 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd;
  24 +import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate;
  25 +
  26 +public interface TbEntityDataSubscriptionService {
  27 +
  28 + void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityDataCmd cmd);
  29 +
  30 + void cancelSubscription(String sessionId, EntityDataUnsubscribeCmd subscriptionId);
  31 +
  32 + void cancelAllSessionSubscriptions(String sessionId);
  33 +
  34 + void onSubscriptionUpdate(String sessionId, SubscriptionUpdate update, TbCallback callback);
  35 +
  36 + void onApplicationEvent(PartitionChangeEvent event);
  37 +
  38 + void onApplicationEvent(ClusterTopologyChangeEvent event);
  39 +}
@@ -49,8 +49,10 @@ import org.thingsboard.server.service.security.AccessValidator; @@ -49,8 +49,10 @@ import org.thingsboard.server.service.security.AccessValidator;
49 import org.thingsboard.server.service.security.ValidationCallback; 49 import org.thingsboard.server.service.security.ValidationCallback;
50 import org.thingsboard.server.service.security.ValidationResult; 50 import org.thingsboard.server.service.security.ValidationResult;
51 import org.thingsboard.server.service.security.ValidationResultCode; 51 import org.thingsboard.server.service.security.ValidationResultCode;
  52 +import org.thingsboard.server.service.security.model.SecurityUser;
52 import org.thingsboard.server.service.security.model.UserPrincipal; 53 import org.thingsboard.server.service.security.model.UserPrincipal;
53 import org.thingsboard.server.service.security.permission.Operation; 54 import org.thingsboard.server.service.security.permission.Operation;
  55 +import org.thingsboard.server.service.subscription.TbEntityDataSubscriptionService;
54 import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; 56 import org.thingsboard.server.service.subscription.TbLocalSubscriptionService;
55 import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; 57 import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope;
56 import org.thingsboard.server.service.subscription.TbAttributeSubscription; 58 import org.thingsboard.server.service.subscription.TbAttributeSubscription;
@@ -61,6 +63,9 @@ import org.thingsboard.server.service.telemetry.cmd.v1.SubscriptionCmd; @@ -61,6 +63,9 @@ import org.thingsboard.server.service.telemetry.cmd.v1.SubscriptionCmd;
61 import org.thingsboard.server.service.telemetry.cmd.v1.TelemetryPluginCmd; 63 import org.thingsboard.server.service.telemetry.cmd.v1.TelemetryPluginCmd;
62 import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; 64 import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper;
63 import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd; 65 import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd;
  66 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
  67 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd;
  68 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
64 import org.thingsboard.server.service.telemetry.exception.UnauthorizedException; 69 import org.thingsboard.server.service.telemetry.exception.UnauthorizedException;
65 import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; 70 import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode;
66 import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; 71 import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate;
@@ -104,7 +109,10 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @@ -104,7 +109,10 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
104 private final ConcurrentMap<String, WsSessionMetaData> wsSessionsMap = new ConcurrentHashMap<>(); 109 private final ConcurrentMap<String, WsSessionMetaData> wsSessionsMap = new ConcurrentHashMap<>();
105 110
106 @Autowired 111 @Autowired
107 - private TbLocalSubscriptionService subService; 112 + private TbLocalSubscriptionService oldSubService;
  113 +
  114 + @Autowired
  115 + private TbEntityDataSubscriptionService entityDataSubService;
108 116
109 @Autowired 117 @Autowired
110 private TelemetryWebSocketMsgEndpoint msgEndpoint; 118 private TelemetryWebSocketMsgEndpoint msgEndpoint;
@@ -164,7 +172,8 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @@ -164,7 +172,8 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
164 break; 172 break;
165 case CLOSED: 173 case CLOSED:
166 wsSessionsMap.remove(sessionId); 174 wsSessionsMap.remove(sessionId);
167 - subService.cancelAllSessionSubscriptions(sessionId); 175 + oldSubService.cancelAllSessionSubscriptions(sessionId);
  176 + entityDataSubService.cancelAllSessionSubscriptions(sessionId);
168 processSessionClose(sessionRef); 177 processSessionClose(sessionRef);
169 break; 178 break;
170 } 179 }
@@ -196,6 +205,12 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @@ -196,6 +205,12 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
196 if (cmdsWrapper.getHistoryCmds() != null) { 205 if (cmdsWrapper.getHistoryCmds() != null) {
197 cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(sessionRef, cmd)); 206 cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(sessionRef, cmd));
198 } 207 }
  208 + if (cmdsWrapper.getEntityDataCmds() != null) {
  209 + cmdsWrapper.getEntityDataCmds().forEach(cmd -> handleWsEntityDataCmd(sessionRef, cmd));
  210 + }
  211 + if (cmdsWrapper.getEntityDataUnsubscribeCmds() != null) {
  212 + cmdsWrapper.getEntityDataUnsubscribeCmds().forEach(cmd -> handleWsEntityDataUnsubscribeCmd(sessionRef, cmd));
  213 + }
199 } 214 }
200 } catch (IOException e) { 215 } catch (IOException e) {
201 log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e); 216 log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e);
@@ -204,11 +219,39 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @@ -204,11 +219,39 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
204 } 219 }
205 } 220 }
206 221
  222 + private void handleWsEntityDataCmd(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) {
  223 + String sessionId = sessionRef.getSessionId();
  224 + log.debug("[{}] Processing: {}", sessionId, cmd);
  225 +
  226 + if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)
  227 + && validateSubscriptionCmd(sessionRef, cmd)) {
  228 + entityDataSubService.handleCmd(sessionRef, cmd);
  229 + }
  230 + }
  231 +
  232 + private void handleWsEntityDataUnsubscribeCmd(TelemetryWebSocketSessionRef sessionRef, EntityDataUnsubscribeCmd cmd) {
  233 + String sessionId = sessionRef.getSessionId();
  234 + log.debug("[{}] Processing: {}", sessionId, cmd);
  235 +
  236 + if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)) {
  237 + entityDataSubService.cancelSubscription(sessionRef.getSessionId(), cmd);
  238 + }
  239 + }
  240 +
207 @Override 241 @Override
208 public void sendWsMsg(String sessionId, SubscriptionUpdate update) { 242 public void sendWsMsg(String sessionId, SubscriptionUpdate update) {
  243 + sendWsMsg(sessionId, update.getSubscriptionId(), update);
  244 + }
  245 +
  246 + @Override
  247 + public void sendWsMsg(String sessionId, EntityDataUpdate update) {
  248 + sendWsMsg(sessionId, update.getCmdId(), update);
  249 + }
  250 +
  251 + private <T> void sendWsMsg(String sessionId, int cmdId, T update) {
209 WsSessionMetaData md = wsSessionsMap.get(sessionId); 252 WsSessionMetaData md = wsSessionsMap.get(sessionId);
210 if (md != null) { 253 if (md != null) {
211 - sendWsMsg(md.getSessionRef(), update); 254 + sendWsMsg(md.getSessionRef(), cmdId, update);
212 } 255 }
213 } 256 }
214 257
@@ -356,7 +399,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @@ -356,7 +399,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
356 .allKeys(false) 399 .allKeys(false)
357 .keyStates(subState) 400 .keyStates(subState)
358 .scope(scope).build(); 401 .scope(scope).build();
359 - subService.addSubscription(sub); 402 + oldSubService.addSubscription(sub);
360 } 403 }
361 404
362 @Override 405 @Override
@@ -453,7 +496,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @@ -453,7 +496,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
453 .allKeys(true) 496 .allKeys(true)
454 .keyStates(subState) 497 .keyStates(subState)
455 .scope(scope).build(); 498 .scope(scope).build();
456 - subService.addSubscription(sub); 499 + oldSubService.addSubscription(sub);
457 } 500 }
458 501
459 @Override 502 @Override
@@ -534,7 +577,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @@ -534,7 +577,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
534 .entityId(entityId) 577 .entityId(entityId)
535 .allKeys(true) 578 .allKeys(true)
536 .keyStates(subState).build(); 579 .keyStates(subState).build();
537 - subService.addSubscription(sub); 580 + oldSubService.addSubscription(sub);
538 } 581 }
539 582
540 @Override 583 @Override
@@ -571,7 +614,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @@ -571,7 +614,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
571 .entityId(entityId) 614 .entityId(entityId)
572 .allKeys(false) 615 .allKeys(false)
573 .keyStates(subState).build(); 616 .keyStates(subState).build();
574 - subService.addSubscription(sub); 617 + oldSubService.addSubscription(sub);
575 } 618 }
576 619
577 @Override 620 @Override
@@ -590,12 +633,32 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @@ -590,12 +633,32 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
590 633
591 private void unsubscribe(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { 634 private void unsubscribe(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
592 if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { 635 if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
593 - subService.cancelAllSessionSubscriptions(sessionId); 636 + oldSubService.cancelAllSessionSubscriptions(sessionId);
594 } else { 637 } else {
595 - subService.cancelSubscription(sessionId, cmd.getCmdId()); 638 + oldSubService.cancelSubscription(sessionId, cmd.getCmdId());
596 } 639 }
597 } 640 }
598 641
  642 + private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) {
  643 + if (cmd.getCmdId() < 0) {
  644 + SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
  645 + "Cmd id is negative value!");
  646 + sendWsMsg(sessionRef, update);
  647 + return false;
  648 + } else if (cmd.getQuery() == null) {
  649 + SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
  650 + "Query is empty!");
  651 + sendWsMsg(sessionRef, update);
  652 + return false;
  653 + } else if (cmd.getHistoryCmd() == null && cmd.getLatestCmd() == null && cmd.getTsCmd() == null) {
  654 + SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
  655 + "No history, latest or timeseries command present!");
  656 + sendWsMsg(sessionRef, update);
  657 + return false;
  658 + }
  659 + return true;
  660 + }
  661 +
599 private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) { 662 private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) {
600 if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { 663 if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
601 SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, 664 SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
@@ -607,10 +670,14 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @@ -607,10 +670,14 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
607 } 670 }
608 671
609 private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { 672 private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
  673 + return validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId);
  674 + }
  675 +
  676 + private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, int cmdId, String sessionId) {
610 WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId); 677 WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
611 if (sessionMD == null) { 678 if (sessionMD == null) {
612 log.warn("[{}] Session meta data not found. ", sessionId); 679 log.warn("[{}] Session meta data not found. ", sessionId);
613 - SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, 680 + SubscriptionUpdate update = new SubscriptionUpdate(cmdId, SubscriptionErrorCode.INTERNAL_ERROR,
614 SESSION_META_DATA_NOT_FOUND); 681 SESSION_META_DATA_NOT_FOUND);
615 sendWsMsg(sessionRef, update); 682 sendWsMsg(sessionRef, update);
616 return false; 683 return false;
@@ -619,10 +686,18 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @@ -619,10 +686,18 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
619 } 686 }
620 } 687 }
621 688
  689 + private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, EntityDataUpdate update) {
  690 + sendWsMsg(sessionRef, update.getCmdId(), update);
  691 + }
  692 +
622 private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, SubscriptionUpdate update) { 693 private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, SubscriptionUpdate update) {
  694 + sendWsMsg(sessionRef, update.getSubscriptionId(), update);
  695 + }
  696 +
  697 + private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, int cmdId, Object update) {
623 executor.submit(() -> { 698 executor.submit(() -> {
624 try { 699 try {
625 - msgEndpoint.send(sessionRef, update.getSubscriptionId(), jsonMapper.writeValueAsString(update)); 700 + msgEndpoint.send(sessionRef, cmdId, jsonMapper.writeValueAsString(update));
626 } catch (JsonProcessingException e) { 701 } catch (JsonProcessingException e) {
627 log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e); 702 log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e);
628 } catch (IOException e) { 703 } catch (IOException e) {
@@ -631,6 +706,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @@ -631,6 +706,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
631 }); 706 });
632 } 707 }
633 708
  709 +
634 private static Optional<Set<String>> getKeys(TelemetryPluginCmd cmd) { 710 private static Optional<Set<String>> getKeys(TelemetryPluginCmd cmd) {
635 if (!StringUtils.isEmpty(cmd.getKeys())) { 711 if (!StringUtils.isEmpty(cmd.getKeys())) {
636 Set<String> keys = new HashSet<>(); 712 Set<String> keys = new HashSet<>();
@@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
15 */ 15 */
16 package org.thingsboard.server.service.telemetry; 16 package org.thingsboard.server.service.telemetry;
17 17
  18 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
18 import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; 19 import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate;
19 20
20 /** 21 /**
@@ -27,4 +28,7 @@ public interface TelemetryWebSocketService { @@ -27,4 +28,7 @@ public interface TelemetryWebSocketService {
27 void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg); 28 void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg);
28 29
29 void sendWsMsg(String sessionId, SubscriptionUpdate update); 30 void sendWsMsg(String sessionId, SubscriptionUpdate update);
  31 +
  32 + void sendWsMsg(String sessionId, EntityDataUpdate update);
  33 +
30 } 34 }
@@ -15,10 +15,12 @@ @@ -15,10 +15,12 @@
15 */ 15 */
16 package org.thingsboard.server.service.telemetry.cmd.v2; 16 package org.thingsboard.server.service.telemetry.cmd.v2;
17 17
  18 +import lombok.Data;
18 import org.thingsboard.server.common.data.kv.Aggregation; 19 import org.thingsboard.server.common.data.kv.Aggregation;
19 20
20 import java.util.List; 21 import java.util.List;
21 22
  23 +@Data
22 public class EntityHistoryCmd { 24 public class EntityHistoryCmd {
23 25
24 private List<String> keys; 26 private List<String> keys;
@@ -91,409 +91,8 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC @@ -91,409 +91,8 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC
91 @Configuration 91 @Configuration
92 @ComponentScan({"org.thingsboard.server"}) 92 @ComponentScan({"org.thingsboard.server"})
93 @WebAppConfiguration 93 @WebAppConfiguration
94 -@SpringBootTest 94 +@SpringBootTest()
95 @Slf4j 95 @Slf4j
96 -public abstract class AbstractControllerTest {  
97 -  
98 - protected ObjectMapper mapper = new ObjectMapper();  
99 -  
100 - protected static final String TEST_TENANT_NAME = "TEST TENANT";  
101 -  
102 - protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org";  
103 - private static final String SYS_ADMIN_PASSWORD = "sysadmin";  
104 -  
105 - protected static final String TENANT_ADMIN_EMAIL = "testtenant@thingsboard.org";  
106 - private static final String TENANT_ADMIN_PASSWORD = "tenant";  
107 -  
108 - protected static final String CUSTOMER_USER_EMAIL = "testcustomer@thingsboard.org";  
109 - private static final String CUSTOMER_USER_PASSWORD = "customer";  
110 -  
111 - /** See {@link org.springframework.test.web.servlet.DefaultMvcResult#getAsyncResult(long)}  
112 - * and {@link org.springframework.mock.web.MockAsyncContext#getTimeout()}  
113 - */  
114 - private static final long DEFAULT_TIMEOUT = -1L;  
115 -  
116 - protected MediaType contentType = MediaType.APPLICATION_JSON;  
117 -  
118 - protected MockMvc mockMvc;  
119 -  
120 - protected String token;  
121 - protected String refreshToken;  
122 - protected String username;  
123 -  
124 - private TenantId tenantId;  
125 -  
126 - @SuppressWarnings("rawtypes")  
127 - private HttpMessageConverter mappingJackson2HttpMessageConverter;  
128 -  
129 - @SuppressWarnings("rawtypes")  
130 - private HttpMessageConverter stringHttpMessageConverter;  
131 -  
132 - @Autowired  
133 - private WebApplicationContext webApplicationContext;  
134 -  
135 - @Rule  
136 - public TestRule watcher = new TestWatcher() {  
137 - protected void starting(Description description) {  
138 - log.info("Starting test: {}", description.getMethodName());  
139 - }  
140 -  
141 - protected void finished(Description description) {  
142 - log.info("Finished test: {}", description.getMethodName());  
143 - }  
144 - };  
145 -  
146 - @Autowired  
147 - void setConverters(HttpMessageConverter<?>[] converters) {  
148 -  
149 - this.mappingJackson2HttpMessageConverter = Arrays.stream(converters)  
150 - .filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter)  
151 - .findAny()  
152 - .get();  
153 -  
154 - this.stringHttpMessageConverter = Arrays.stream(converters)  
155 - .filter(hmc -> hmc instanceof StringHttpMessageConverter)  
156 - .findAny()  
157 - .get();  
158 -  
159 - Assert.assertNotNull("the JSON message converter must not be null",  
160 - this.mappingJackson2HttpMessageConverter);  
161 - }  
162 -  
163 - @Before  
164 - public void setup() throws Exception {  
165 - log.info("Executing setup");  
166 - if (this.mockMvc == null) {  
167 - this.mockMvc = webAppContextSetup(webApplicationContext)  
168 - .apply(springSecurity()).build();  
169 - }  
170 - loginSysAdmin();  
171 -  
172 - Tenant tenant = new Tenant();  
173 - tenant.setTitle(TEST_TENANT_NAME);  
174 - Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);  
175 - Assert.assertNotNull(savedTenant);  
176 - tenantId = savedTenant.getId();  
177 -  
178 - User tenantAdmin = new User();  
179 - tenantAdmin.setAuthority(Authority.TENANT_ADMIN);  
180 - tenantAdmin.setTenantId(tenantId);  
181 - tenantAdmin.setEmail(TENANT_ADMIN_EMAIL);  
182 -  
183 - createUserAndLogin(tenantAdmin, TENANT_ADMIN_PASSWORD);  
184 -  
185 - Customer customer = new Customer();  
186 - customer.setTitle("Customer");  
187 - customer.setTenantId(tenantId);  
188 - Customer savedCustomer = doPost("/api/customer", customer, Customer.class);  
189 -  
190 - User customerUser = new User();  
191 - customerUser.setAuthority(Authority.CUSTOMER_USER);  
192 - customerUser.setTenantId(tenantId);  
193 - customerUser.setCustomerId(savedCustomer.getId());  
194 - customerUser.setEmail(CUSTOMER_USER_EMAIL);  
195 -  
196 - createUserAndLogin(customerUser, CUSTOMER_USER_PASSWORD);  
197 -  
198 - logout();  
199 -  
200 - log.info("Executed setup");  
201 - }  
202 -  
203 - @After  
204 - public void teardown() throws Exception {  
205 - log.info("Executing teardown");  
206 - loginSysAdmin();  
207 - doDelete("/api/tenant/" + tenantId.getId().toString())  
208 - .andExpect(status().isOk());  
209 - log.info("Executed teardown");  
210 - }  
211 -  
212 - protected void loginSysAdmin() throws Exception {  
213 - login(SYS_ADMIN_EMAIL, SYS_ADMIN_PASSWORD);  
214 - }  
215 -  
216 - protected void loginTenantAdmin() throws Exception {  
217 - login(TENANT_ADMIN_EMAIL, TENANT_ADMIN_PASSWORD);  
218 - }  
219 -  
220 - protected void loginCustomerUser() throws Exception {  
221 - login(CUSTOMER_USER_EMAIL, CUSTOMER_USER_PASSWORD);  
222 - }  
223 -  
224 - private Tenant savedDifferentTenant;  
225 - protected void loginDifferentTenant() throws Exception {  
226 - loginSysAdmin();  
227 - Tenant tenant = new Tenant();  
228 - tenant.setTitle("Different tenant");  
229 - savedDifferentTenant = doPost("/api/tenant", tenant, Tenant.class);  
230 - Assert.assertNotNull(savedDifferentTenant);  
231 - User differentTenantAdmin = new User();  
232 - differentTenantAdmin.setAuthority(Authority.TENANT_ADMIN);  
233 - differentTenantAdmin.setTenantId(savedDifferentTenant.getId());  
234 - differentTenantAdmin.setEmail("different_tenant@thingsboard.org");  
235 -  
236 - createUserAndLogin(differentTenantAdmin, "testPassword");  
237 - }  
238 -  
239 - protected void deleteDifferentTenant() throws Exception {  
240 - loginSysAdmin();  
241 - doDelete("/api/tenant/" + savedDifferentTenant.getId().getId().toString())  
242 - .andExpect(status().isOk());  
243 - }  
244 -  
245 - protected User createUserAndLogin(User user, String password) throws Exception {  
246 - User savedUser = doPost("/api/user", user, User.class);  
247 - logout();  
248 - doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken)  
249 - .andExpect(status().isSeeOther())  
250 - .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));  
251 - JsonNode activateRequest = new ObjectMapper().createObjectNode()  
252 - .put("activateToken", TestMailService.currentActivateToken)  
253 - .put("password", password);  
254 - JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);  
255 - validateAndSetJwtToken(tokenInfo, user.getEmail());  
256 - return savedUser;  
257 - }  
258 -  
259 - protected void login(String username, String password) throws Exception {  
260 - this.token = null;  
261 - this.refreshToken = null;  
262 - this.username = null;  
263 - JsonNode tokenInfo = readResponse(doPost("/api/auth/login", new LoginRequest(username, password)).andExpect(status().isOk()), JsonNode.class);  
264 - validateAndSetJwtToken(tokenInfo, username);  
265 - }  
266 -  
267 - protected void refreshToken() throws Exception {  
268 - this.token = null;  
269 - JsonNode tokenInfo = readResponse(doPost("/api/auth/token", new RefreshTokenRequest(this.refreshToken)).andExpect(status().isOk()), JsonNode.class);  
270 - validateAndSetJwtToken(tokenInfo, this.username);  
271 - }  
272 -  
273 - protected void validateAndSetJwtToken(JsonNode tokenInfo, String username) {  
274 - Assert.assertNotNull(tokenInfo);  
275 - Assert.assertTrue(tokenInfo.has("token"));  
276 - Assert.assertTrue(tokenInfo.has("refreshToken"));  
277 - String token = tokenInfo.get("token").asText();  
278 - String refreshToken = tokenInfo.get("refreshToken").asText();  
279 - validateJwtToken(token, username);  
280 - validateJwtToken(refreshToken, username);  
281 - this.token = token;  
282 - this.refreshToken = refreshToken;  
283 - this.username = username;  
284 - }  
285 -  
286 - protected void validateJwtToken(String token, String username) {  
287 - Assert.assertNotNull(token);  
288 - Assert.assertFalse(token.isEmpty());  
289 - int i = token.lastIndexOf('.');  
290 - Assert.assertTrue(i > 0);  
291 - String withoutSignature = token.substring(0, i + 1);  
292 - Jwt<Header, Claims> jwsClaims = Jwts.parser().parseClaimsJwt(withoutSignature);  
293 - Claims claims = jwsClaims.getBody();  
294 - String subject = claims.getSubject();  
295 - Assert.assertEquals(username, subject);  
296 - }  
297 -  
298 - protected void logout() throws Exception {  
299 - this.token = null;  
300 - this.refreshToken = null;  
301 - this.username = null;  
302 - }  
303 -  
304 - protected void setJwtToken(MockHttpServletRequestBuilder request) {  
305 - if (this.token != null) {  
306 - request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token);  
307 - }  
308 - }  
309 -  
310 - protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception {  
311 - MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables);  
312 - setJwtToken(getRequest);  
313 - return mockMvc.perform(getRequest);  
314 - }  
315 -  
316 - protected <T> T doGet(String urlTemplate, Class<T> responseClass, Object... urlVariables) throws Exception {  
317 - return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass);  
318 - }  
319 -  
320 - protected <T> T doGetAsync(String urlTemplate, Class<T> responseClass, Object... urlVariables) throws Exception {  
321 - return readResponse(doGetAsync(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass);  
322 - }  
323 -  
324 - protected ResultActions doGetAsync(String urlTemplate, Object... urlVariables) throws Exception {  
325 - MockHttpServletRequestBuilder getRequest;  
326 - getRequest = get(urlTemplate, urlVariables);  
327 - setJwtToken(getRequest);  
328 - return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn()));  
329 - }  
330 -  
331 - protected <T> T doGetTyped(String urlTemplate, TypeReference<T> responseType, Object... urlVariables) throws Exception {  
332 - return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseType);  
333 - }  
334 -  
335 - protected <T> T doGetTypedWithPageLink(String urlTemplate, TypeReference<T> responseType,  
336 - PageLink pageLink,  
337 - Object... urlVariables) throws Exception {  
338 - List<Object> pageLinkVariables = new ArrayList<>();  
339 - urlTemplate += "pageSize={pageSize}&page={page}";  
340 - pageLinkVariables.add(pageLink.getPageSize());  
341 - pageLinkVariables.add(pageLink.getPage());  
342 - if (StringUtils.isNotEmpty(pageLink.getTextSearch())) {  
343 - urlTemplate += "&textSearch={textSearch}";  
344 - pageLinkVariables.add(pageLink.getTextSearch());  
345 - }  
346 - if (pageLink.getSortOrder() != null) {  
347 - urlTemplate += "&sortProperty={sortProperty}&sortOrder={sortOrder}";  
348 - pageLinkVariables.add(pageLink.getSortOrder().getProperty());  
349 - pageLinkVariables.add(pageLink.getSortOrder().getDirection().name());  
350 - }  
351 -  
352 - Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()];  
353 - System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length);  
354 - System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size());  
355 -  
356 - return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType);  
357 - }  
358 -  
359 - protected <T> T doGetTypedWithTimePageLink(String urlTemplate, TypeReference<T> responseType,  
360 - TimePageLink pageLink,  
361 - Object... urlVariables) throws Exception {  
362 - List<Object> pageLinkVariables = new ArrayList<>();  
363 - urlTemplate += "pageSize={pageSize}&page={page}";  
364 - pageLinkVariables.add(pageLink.getPageSize());  
365 - pageLinkVariables.add(pageLink.getPage());  
366 - if (pageLink.getStartTime() != null) {  
367 - urlTemplate += "&startTime={startTime}";  
368 - pageLinkVariables.add(pageLink.getStartTime());  
369 - }  
370 - if (pageLink.getEndTime() != null) {  
371 - urlTemplate += "&endTime={endTime}";  
372 - pageLinkVariables.add(pageLink.getEndTime());  
373 - }  
374 - if (StringUtils.isNotEmpty(pageLink.getTextSearch())) {  
375 - urlTemplate += "&textSearch={textSearch}";  
376 - pageLinkVariables.add(pageLink.getTextSearch());  
377 - }  
378 - if (pageLink.getSortOrder() != null) {  
379 - urlTemplate += "&sortProperty={sortProperty}&sortOrder={sortOrder}";  
380 - pageLinkVariables.add(pageLink.getSortOrder().getProperty());  
381 - pageLinkVariables.add(pageLink.getSortOrder().getDirection().name());  
382 - }  
383 - Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()];  
384 - System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length);  
385 - System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size());  
386 -  
387 - return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType);  
388 - }  
389 -  
390 - protected <T> T doPost(String urlTemplate, Class<T> responseClass, String... params) throws Exception {  
391 - return readResponse(doPost(urlTemplate, params).andExpect(status().isOk()), responseClass);  
392 - }  
393 -  
394 - protected <T> T doPost(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, String... params) throws Exception {  
395 - return readResponse(doPost(urlTemplate, content, params).andExpect(resultMatcher), responseClass);  
396 - }  
397 -  
398 - protected <T> T doPost(String urlTemplate, T content, Class<T> responseClass, String... params) throws Exception {  
399 - return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass);  
400 - }  
401 -  
402 - protected <T,R> R doPostWithResponse(String urlTemplate, T content, Class<R> responseClass, String... params) throws Exception {  
403 - return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass);  
404 - }  
405 -  
406 - protected <T,R> R doPostWithTypedResponse(String urlTemplate, T content, TypeReference<R> responseType, String... params) throws Exception {  
407 - return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseType);  
408 - }  
409 -  
410 - protected <T> T doPostAsync(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, String... params) throws Exception {  
411 - return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass);  
412 - }  
413 -  
414 - protected <T> T doPostAsync(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, Long timeout, String... params) throws Exception {  
415 - return readResponse(doPostAsync(urlTemplate, content, timeout, params).andExpect(resultMatcher), responseClass);  
416 - }  
417 -  
418 - protected <T> T doDelete(String urlTemplate, Class<T> responseClass, String... params) throws Exception {  
419 - return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass);  
420 - }  
421 -  
422 - protected ResultActions doPost(String urlTemplate, String... params) throws Exception {  
423 - MockHttpServletRequestBuilder postRequest = post(urlTemplate);  
424 - setJwtToken(postRequest);  
425 - populateParams(postRequest, params);  
426 - return mockMvc.perform(postRequest);  
427 - }  
428 -  
429 - protected <T> ResultActions doPost(String urlTemplate, T content, String... params) throws Exception {  
430 - MockHttpServletRequestBuilder postRequest = post(urlTemplate);  
431 - setJwtToken(postRequest);  
432 - String json = json(content);  
433 - postRequest.contentType(contentType).content(json);  
434 - return mockMvc.perform(postRequest);  
435 - }  
436 -  
437 - protected <T> ResultActions doPostAsync(String urlTemplate, T content, Long timeout, String... params) throws Exception {  
438 - MockHttpServletRequestBuilder postRequest = post(urlTemplate);  
439 - setJwtToken(postRequest);  
440 - String json = json(content);  
441 - postRequest.contentType(contentType).content(json);  
442 - MvcResult result = mockMvc.perform(postRequest).andReturn();  
443 - result.getAsyncResult(timeout);  
444 - return mockMvc.perform(asyncDispatch(result));  
445 - }  
446 -  
447 - protected ResultActions doDelete(String urlTemplate, String... params) throws Exception {  
448 - MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate);  
449 - setJwtToken(deleteRequest);  
450 - populateParams(deleteRequest, params);  
451 - return mockMvc.perform(deleteRequest);  
452 - }  
453 -  
454 - protected void populateParams(MockHttpServletRequestBuilder request, String... params) {  
455 - if (params != null && params.length > 0) {  
456 - Assert.assertEquals(0, params.length % 2);  
457 - MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();  
458 - for (int i = 0; i < params.length; i += 2) {  
459 - paramsMap.add(params[i], params[i + 1]);  
460 - }  
461 - request.params(paramsMap);  
462 - }  
463 - }  
464 -  
465 - @SuppressWarnings("unchecked")  
466 - protected String json(Object o) throws IOException {  
467 - MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage();  
468 -  
469 - HttpMessageConverter converter = o instanceof String ? stringHttpMessageConverter : mappingJackson2HttpMessageConverter;  
470 - converter.write(o, MediaType.APPLICATION_JSON, mockHttpOutputMessage);  
471 - return mockHttpOutputMessage.getBodyAsString();  
472 - }  
473 -  
474 - @SuppressWarnings("unchecked")  
475 - protected <T> T readResponse(ResultActions result, Class<T> responseClass) throws Exception {  
476 - byte[] content = result.andReturn().getResponse().getContentAsByteArray();  
477 - MockHttpInputMessage mockHttpInputMessage = new MockHttpInputMessage(content);  
478 - HttpMessageConverter converter = responseClass.equals(String.class) ? stringHttpMessageConverter : mappingJackson2HttpMessageConverter;  
479 - return (T) converter.read(responseClass, mockHttpInputMessage);  
480 - }  
481 -  
482 - protected <T> T readResponse(ResultActions result, TypeReference<T> type) throws Exception {  
483 - byte[] content = result.andReturn().getResponse().getContentAsByteArray();  
484 - ObjectMapper mapper = new ObjectMapper();  
485 - return mapper.readerFor(type).readValue(content);  
486 - }  
487 -  
488 - public class IdComparator<D extends BaseData<? extends UUIDBased>> implements Comparator<D> {  
489 - @Override  
490 - public int compare(D o1, D o2) {  
491 - return o1.getId().getId().compareTo(o2.getId().getId());  
492 - }  
493 - }  
494 -  
495 - protected static <T> ResultMatcher statusReason(Matcher<T> matcher) {  
496 - return jsonPath("$.message", matcher);  
497 - } 96 +public abstract class AbstractControllerTest extends AbstractWebTest {
498 97
499 } 98 }
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.controller;
  17 +
  18 +import com.fasterxml.jackson.core.type.TypeReference;
  19 +import com.fasterxml.jackson.databind.JsonNode;
  20 +import com.fasterxml.jackson.databind.ObjectMapper;
  21 +import io.jsonwebtoken.Claims;
  22 +import io.jsonwebtoken.Header;
  23 +import io.jsonwebtoken.Jwt;
  24 +import io.jsonwebtoken.Jwts;
  25 +import lombok.extern.slf4j.Slf4j;
  26 +import org.apache.commons.lang3.StringUtils;
  27 +import org.hamcrest.Matcher;
  28 +import org.junit.After;
  29 +import org.junit.Assert;
  30 +import org.junit.Before;
  31 +import org.junit.Rule;
  32 +import org.junit.rules.TestRule;
  33 +import org.junit.rules.TestWatcher;
  34 +import org.junit.runner.Description;
  35 +import org.junit.runner.RunWith;
  36 +import org.springframework.beans.factory.annotation.Autowired;
  37 +import org.springframework.boot.test.context.SpringBootContextLoader;
  38 +import org.springframework.boot.test.context.SpringBootTest;
  39 +import org.springframework.context.annotation.ComponentScan;
  40 +import org.springframework.context.annotation.Configuration;
  41 +import org.springframework.http.HttpHeaders;
  42 +import org.springframework.http.MediaType;
  43 +import org.springframework.http.converter.HttpMessageConverter;
  44 +import org.springframework.http.converter.StringHttpMessageConverter;
  45 +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
  46 +import org.springframework.mock.http.MockHttpInputMessage;
  47 +import org.springframework.mock.http.MockHttpOutputMessage;
  48 +import org.springframework.test.annotation.DirtiesContext;
  49 +import org.springframework.test.context.ActiveProfiles;
  50 +import org.springframework.test.context.ContextConfiguration;
  51 +import org.springframework.test.context.junit4.SpringRunner;
  52 +import org.springframework.test.web.servlet.MockMvc;
  53 +import org.springframework.test.web.servlet.MvcResult;
  54 +import org.springframework.test.web.servlet.ResultActions;
  55 +import org.springframework.test.web.servlet.ResultMatcher;
  56 +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
  57 +import org.springframework.util.LinkedMultiValueMap;
  58 +import org.springframework.util.MultiValueMap;
  59 +import org.springframework.web.context.WebApplicationContext;
  60 +import org.thingsboard.server.common.data.BaseData;
  61 +import org.thingsboard.server.common.data.Customer;
  62 +import org.thingsboard.server.common.data.Tenant;
  63 +import org.thingsboard.server.common.data.User;
  64 +import org.thingsboard.server.common.data.id.TenantId;
  65 +import org.thingsboard.server.common.data.id.UUIDBased;
  66 +import org.thingsboard.server.common.data.page.PageLink;
  67 +import org.thingsboard.server.common.data.page.TimePageLink;
  68 +import org.thingsboard.server.common.data.security.Authority;
  69 +import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
  70 +import org.thingsboard.server.service.mail.TestMailService;
  71 +import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest;
  72 +import org.thingsboard.server.service.security.auth.rest.LoginRequest;
  73 +
  74 +import java.io.IOException;
  75 +import java.util.ArrayList;
  76 +import java.util.Arrays;
  77 +import java.util.Comparator;
  78 +import java.util.List;
  79 +
  80 +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
  81 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
  82 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
  83 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
  84 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
  85 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
  86 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
  87 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
  88 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
  89 +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
  90 +
  91 +@Slf4j
  92 +public abstract class AbstractWebTest {
  93 +
  94 + protected ObjectMapper mapper = new ObjectMapper();
  95 +
  96 + protected static final String TEST_TENANT_NAME = "TEST TENANT";
  97 +
  98 + protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org";
  99 + private static final String SYS_ADMIN_PASSWORD = "sysadmin";
  100 +
  101 + protected static final String TENANT_ADMIN_EMAIL = "testtenant@thingsboard.org";
  102 + private static final String TENANT_ADMIN_PASSWORD = "tenant";
  103 +
  104 + protected static final String CUSTOMER_USER_EMAIL = "testcustomer@thingsboard.org";
  105 + private static final String CUSTOMER_USER_PASSWORD = "customer";
  106 +
  107 + /** See {@link org.springframework.test.web.servlet.DefaultMvcResult#getAsyncResult(long)}
  108 + * and {@link org.springframework.mock.web.MockAsyncContext#getTimeout()}
  109 + */
  110 + private static final long DEFAULT_TIMEOUT = -1L;
  111 +
  112 + protected MediaType contentType = MediaType.APPLICATION_JSON;
  113 +
  114 + protected MockMvc mockMvc;
  115 +
  116 + protected String token;
  117 + protected String refreshToken;
  118 + protected String username;
  119 +
  120 + private TenantId tenantId;
  121 +
  122 + @SuppressWarnings("rawtypes")
  123 + private HttpMessageConverter mappingJackson2HttpMessageConverter;
  124 +
  125 + @SuppressWarnings("rawtypes")
  126 + private HttpMessageConverter stringHttpMessageConverter;
  127 +
  128 + @Autowired
  129 + private WebApplicationContext webApplicationContext;
  130 +
  131 + @Rule
  132 + public TestRule watcher = new TestWatcher() {
  133 + protected void starting(Description description) {
  134 + log.info("Starting test: {}", description.getMethodName());
  135 + }
  136 +
  137 + protected void finished(Description description) {
  138 + log.info("Finished test: {}", description.getMethodName());
  139 + }
  140 + };
  141 +
  142 + @Autowired
  143 + void setConverters(HttpMessageConverter<?>[] converters) {
  144 +
  145 + this.mappingJackson2HttpMessageConverter = Arrays.stream(converters)
  146 + .filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter)
  147 + .findAny()
  148 + .get();
  149 +
  150 + this.stringHttpMessageConverter = Arrays.stream(converters)
  151 + .filter(hmc -> hmc instanceof StringHttpMessageConverter)
  152 + .findAny()
  153 + .get();
  154 +
  155 + Assert.assertNotNull("the JSON message converter must not be null",
  156 + this.mappingJackson2HttpMessageConverter);
  157 + }
  158 +
  159 + @Before
  160 + public void setup() throws Exception {
  161 + log.info("Executing setup");
  162 + if (this.mockMvc == null) {
  163 + this.mockMvc = webAppContextSetup(webApplicationContext)
  164 + .apply(springSecurity()).build();
  165 + }
  166 + loginSysAdmin();
  167 +
  168 + Tenant tenant = new Tenant();
  169 + tenant.setTitle(TEST_TENANT_NAME);
  170 + Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
  171 + Assert.assertNotNull(savedTenant);
  172 + tenantId = savedTenant.getId();
  173 +
  174 + User tenantAdmin = new User();
  175 + tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
  176 + tenantAdmin.setTenantId(tenantId);
  177 + tenantAdmin.setEmail(TENANT_ADMIN_EMAIL);
  178 +
  179 + createUserAndLogin(tenantAdmin, TENANT_ADMIN_PASSWORD);
  180 +
  181 + Customer customer = new Customer();
  182 + customer.setTitle("Customer");
  183 + customer.setTenantId(tenantId);
  184 + Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
  185 +
  186 + User customerUser = new User();
  187 + customerUser.setAuthority(Authority.CUSTOMER_USER);
  188 + customerUser.setTenantId(tenantId);
  189 + customerUser.setCustomerId(savedCustomer.getId());
  190 + customerUser.setEmail(CUSTOMER_USER_EMAIL);
  191 +
  192 + createUserAndLogin(customerUser, CUSTOMER_USER_PASSWORD);
  193 +
  194 + logout();
  195 +
  196 + log.info("Executed setup");
  197 + }
  198 +
  199 + @After
  200 + public void teardown() throws Exception {
  201 + log.info("Executing teardown");
  202 + loginSysAdmin();
  203 + doDelete("/api/tenant/" + tenantId.getId().toString())
  204 + .andExpect(status().isOk());
  205 + log.info("Executed teardown");
  206 + }
  207 +
  208 + protected void loginSysAdmin() throws Exception {
  209 + login(SYS_ADMIN_EMAIL, SYS_ADMIN_PASSWORD);
  210 + }
  211 +
  212 + protected void loginTenantAdmin() throws Exception {
  213 + login(TENANT_ADMIN_EMAIL, TENANT_ADMIN_PASSWORD);
  214 + }
  215 +
  216 + protected void loginCustomerUser() throws Exception {
  217 + login(CUSTOMER_USER_EMAIL, CUSTOMER_USER_PASSWORD);
  218 + }
  219 +
  220 + private Tenant savedDifferentTenant;
  221 + protected void loginDifferentTenant() throws Exception {
  222 + loginSysAdmin();
  223 + Tenant tenant = new Tenant();
  224 + tenant.setTitle("Different tenant");
  225 + savedDifferentTenant = doPost("/api/tenant", tenant, Tenant.class);
  226 + Assert.assertNotNull(savedDifferentTenant);
  227 + User differentTenantAdmin = new User();
  228 + differentTenantAdmin.setAuthority(Authority.TENANT_ADMIN);
  229 + differentTenantAdmin.setTenantId(savedDifferentTenant.getId());
  230 + differentTenantAdmin.setEmail("different_tenant@thingsboard.org");
  231 +
  232 + createUserAndLogin(differentTenantAdmin, "testPassword");
  233 + }
  234 +
  235 + protected void deleteDifferentTenant() throws Exception {
  236 + loginSysAdmin();
  237 + doDelete("/api/tenant/" + savedDifferentTenant.getId().getId().toString())
  238 + .andExpect(status().isOk());
  239 + }
  240 +
  241 + protected User createUserAndLogin(User user, String password) throws Exception {
  242 + User savedUser = doPost("/api/user", user, User.class);
  243 + logout();
  244 + doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken)
  245 + .andExpect(status().isSeeOther())
  246 + .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
  247 + JsonNode activateRequest = new ObjectMapper().createObjectNode()
  248 + .put("activateToken", TestMailService.currentActivateToken)
  249 + .put("password", password);
  250 + JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
  251 + validateAndSetJwtToken(tokenInfo, user.getEmail());
  252 + return savedUser;
  253 + }
  254 +
  255 + protected void login(String username, String password) throws Exception {
  256 + this.token = null;
  257 + this.refreshToken = null;
  258 + this.username = null;
  259 + JsonNode tokenInfo = readResponse(doPost("/api/auth/login", new LoginRequest(username, password)).andExpect(status().isOk()), JsonNode.class);
  260 + validateAndSetJwtToken(tokenInfo, username);
  261 + }
  262 +
  263 + protected void refreshToken() throws Exception {
  264 + this.token = null;
  265 + JsonNode tokenInfo = readResponse(doPost("/api/auth/token", new RefreshTokenRequest(this.refreshToken)).andExpect(status().isOk()), JsonNode.class);
  266 + validateAndSetJwtToken(tokenInfo, this.username);
  267 + }
  268 +
  269 + protected void validateAndSetJwtToken(JsonNode tokenInfo, String username) {
  270 + Assert.assertNotNull(tokenInfo);
  271 + Assert.assertTrue(tokenInfo.has("token"));
  272 + Assert.assertTrue(tokenInfo.has("refreshToken"));
  273 + String token = tokenInfo.get("token").asText();
  274 + String refreshToken = tokenInfo.get("refreshToken").asText();
  275 + validateJwtToken(token, username);
  276 + validateJwtToken(refreshToken, username);
  277 + this.token = token;
  278 + this.refreshToken = refreshToken;
  279 + this.username = username;
  280 + }
  281 +
  282 + protected void validateJwtToken(String token, String username) {
  283 + Assert.assertNotNull(token);
  284 + Assert.assertFalse(token.isEmpty());
  285 + int i = token.lastIndexOf('.');
  286 + Assert.assertTrue(i > 0);
  287 + String withoutSignature = token.substring(0, i + 1);
  288 + Jwt<Header, Claims> jwsClaims = Jwts.parser().parseClaimsJwt(withoutSignature);
  289 + Claims claims = jwsClaims.getBody();
  290 + String subject = claims.getSubject();
  291 + Assert.assertEquals(username, subject);
  292 + }
  293 +
  294 + protected void logout() throws Exception {
  295 + this.token = null;
  296 + this.refreshToken = null;
  297 + this.username = null;
  298 + }
  299 +
  300 + protected void setJwtToken(MockHttpServletRequestBuilder request) {
  301 + if (this.token != null) {
  302 + request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token);
  303 + }
  304 + }
  305 +
  306 + protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception {
  307 + MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables);
  308 + setJwtToken(getRequest);
  309 + return mockMvc.perform(getRequest);
  310 + }
  311 +
  312 + protected <T> T doGet(String urlTemplate, Class<T> responseClass, Object... urlVariables) throws Exception {
  313 + return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass);
  314 + }
  315 +
  316 + protected <T> T doGetAsync(String urlTemplate, Class<T> responseClass, Object... urlVariables) throws Exception {
  317 + return readResponse(doGetAsync(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass);
  318 + }
  319 +
  320 + protected ResultActions doGetAsync(String urlTemplate, Object... urlVariables) throws Exception {
  321 + MockHttpServletRequestBuilder getRequest;
  322 + getRequest = get(urlTemplate, urlVariables);
  323 + setJwtToken(getRequest);
  324 + return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn()));
  325 + }
  326 +
  327 + protected <T> T doGetTyped(String urlTemplate, TypeReference<T> responseType, Object... urlVariables) throws Exception {
  328 + return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseType);
  329 + }
  330 +
  331 + protected <T> T doGetTypedWithPageLink(String urlTemplate, TypeReference<T> responseType,
  332 + PageLink pageLink,
  333 + Object... urlVariables) throws Exception {
  334 + List<Object> pageLinkVariables = new ArrayList<>();
  335 + urlTemplate += "pageSize={pageSize}&page={page}";
  336 + pageLinkVariables.add(pageLink.getPageSize());
  337 + pageLinkVariables.add(pageLink.getPage());
  338 + if (StringUtils.isNotEmpty(pageLink.getTextSearch())) {
  339 + urlTemplate += "&textSearch={textSearch}";
  340 + pageLinkVariables.add(pageLink.getTextSearch());
  341 + }
  342 + if (pageLink.getSortOrder() != null) {
  343 + urlTemplate += "&sortProperty={sortProperty}&sortOrder={sortOrder}";
  344 + pageLinkVariables.add(pageLink.getSortOrder().getProperty());
  345 + pageLinkVariables.add(pageLink.getSortOrder().getDirection().name());
  346 + }
  347 +
  348 + Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()];
  349 + System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length);
  350 + System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size());
  351 +
  352 + return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType);
  353 + }
  354 +
  355 + protected <T> T doGetTypedWithTimePageLink(String urlTemplate, TypeReference<T> responseType,
  356 + TimePageLink pageLink,
  357 + Object... urlVariables) throws Exception {
  358 + List<Object> pageLinkVariables = new ArrayList<>();
  359 + urlTemplate += "pageSize={pageSize}&page={page}";
  360 + pageLinkVariables.add(pageLink.getPageSize());
  361 + pageLinkVariables.add(pageLink.getPage());
  362 + if (pageLink.getStartTime() != null) {
  363 + urlTemplate += "&startTime={startTime}";
  364 + pageLinkVariables.add(pageLink.getStartTime());
  365 + }
  366 + if (pageLink.getEndTime() != null) {
  367 + urlTemplate += "&endTime={endTime}";
  368 + pageLinkVariables.add(pageLink.getEndTime());
  369 + }
  370 + if (StringUtils.isNotEmpty(pageLink.getTextSearch())) {
  371 + urlTemplate += "&textSearch={textSearch}";
  372 + pageLinkVariables.add(pageLink.getTextSearch());
  373 + }
  374 + if (pageLink.getSortOrder() != null) {
  375 + urlTemplate += "&sortProperty={sortProperty}&sortOrder={sortOrder}";
  376 + pageLinkVariables.add(pageLink.getSortOrder().getProperty());
  377 + pageLinkVariables.add(pageLink.getSortOrder().getDirection().name());
  378 + }
  379 + Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()];
  380 + System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length);
  381 + System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size());
  382 +
  383 + return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType);
  384 + }
  385 +
  386 + protected <T> T doPost(String urlTemplate, Class<T> responseClass, String... params) throws Exception {
  387 + return readResponse(doPost(urlTemplate, params).andExpect(status().isOk()), responseClass);
  388 + }
  389 +
  390 + protected <T> T doPost(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, String... params) throws Exception {
  391 + return readResponse(doPost(urlTemplate, content, params).andExpect(resultMatcher), responseClass);
  392 + }
  393 +
  394 + protected <T> T doPost(String urlTemplate, T content, Class<T> responseClass, String... params) throws Exception {
  395 + return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass);
  396 + }
  397 +
  398 + protected <T,R> R doPostWithResponse(String urlTemplate, T content, Class<R> responseClass, String... params) throws Exception {
  399 + return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass);
  400 + }
  401 +
  402 + protected <T,R> R doPostWithTypedResponse(String urlTemplate, T content, TypeReference<R> responseType, String... params) throws Exception {
  403 + return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseType);
  404 + }
  405 +
  406 + protected <T> T doPostAsync(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, String... params) throws Exception {
  407 + return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass);
  408 + }
  409 +
  410 + protected <T> T doPostAsync(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, Long timeout, String... params) throws Exception {
  411 + return readResponse(doPostAsync(urlTemplate, content, timeout, params).andExpect(resultMatcher), responseClass);
  412 + }
  413 +
  414 + protected <T> T doDelete(String urlTemplate, Class<T> responseClass, String... params) throws Exception {
  415 + return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass);
  416 + }
  417 +
  418 + protected ResultActions doPost(String urlTemplate, String... params) throws Exception {
  419 + MockHttpServletRequestBuilder postRequest = post(urlTemplate);
  420 + setJwtToken(postRequest);
  421 + populateParams(postRequest, params);
  422 + return mockMvc.perform(postRequest);
  423 + }
  424 +
  425 + protected <T> ResultActions doPost(String urlTemplate, T content, String... params) throws Exception {
  426 + MockHttpServletRequestBuilder postRequest = post(urlTemplate);
  427 + setJwtToken(postRequest);
  428 + String json = json(content);
  429 + postRequest.contentType(contentType).content(json);
  430 + return mockMvc.perform(postRequest);
  431 + }
  432 +
  433 + protected <T> ResultActions doPostAsync(String urlTemplate, T content, Long timeout, String... params) throws Exception {
  434 + MockHttpServletRequestBuilder postRequest = post(urlTemplate);
  435 + setJwtToken(postRequest);
  436 + String json = json(content);
  437 + postRequest.contentType(contentType).content(json);
  438 + MvcResult result = mockMvc.perform(postRequest).andReturn();
  439 + result.getAsyncResult(timeout);
  440 + return mockMvc.perform(asyncDispatch(result));
  441 + }
  442 +
  443 + protected ResultActions doDelete(String urlTemplate, String... params) throws Exception {
  444 + MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate);
  445 + setJwtToken(deleteRequest);
  446 + populateParams(deleteRequest, params);
  447 + return mockMvc.perform(deleteRequest);
  448 + }
  449 +
  450 + protected void populateParams(MockHttpServletRequestBuilder request, String... params) {
  451 + if (params != null && params.length > 0) {
  452 + Assert.assertEquals(0, params.length % 2);
  453 + MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();
  454 + for (int i = 0; i < params.length; i += 2) {
  455 + paramsMap.add(params[i], params[i + 1]);
  456 + }
  457 + request.params(paramsMap);
  458 + }
  459 + }
  460 +
  461 + @SuppressWarnings("unchecked")
  462 + protected String json(Object o) throws IOException {
  463 + MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage();
  464 +
  465 + HttpMessageConverter converter = o instanceof String ? stringHttpMessageConverter : mappingJackson2HttpMessageConverter;
  466 + converter.write(o, MediaType.APPLICATION_JSON, mockHttpOutputMessage);
  467 + return mockHttpOutputMessage.getBodyAsString();
  468 + }
  469 +
  470 + @SuppressWarnings("unchecked")
  471 + protected <T> T readResponse(ResultActions result, Class<T> responseClass) throws Exception {
  472 + byte[] content = result.andReturn().getResponse().getContentAsByteArray();
  473 + MockHttpInputMessage mockHttpInputMessage = new MockHttpInputMessage(content);
  474 + HttpMessageConverter converter = responseClass.equals(String.class) ? stringHttpMessageConverter : mappingJackson2HttpMessageConverter;
  475 + return (T) converter.read(responseClass, mockHttpInputMessage);
  476 + }
  477 +
  478 + protected <T> T readResponse(ResultActions result, TypeReference<T> type) throws Exception {
  479 + byte[] content = result.andReturn().getResponse().getContentAsByteArray();
  480 + ObjectMapper mapper = new ObjectMapper();
  481 + return mapper.readerFor(type).readValue(content);
  482 + }
  483 +
  484 + public class IdComparator<D extends BaseData<? extends UUIDBased>> implements Comparator<D> {
  485 + @Override
  486 + public int compare(D o1, D o2) {
  487 + return o1.getId().getId().compareTo(o2.getId().getId());
  488 + }
  489 + }
  490 +
  491 + protected static <T> ResultMatcher statusReason(Matcher<T> matcher) {
  492 + return jsonPath("$.message", matcher);
  493 + }
  494 +
  495 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.controller;
  17 +
  18 +import com.fasterxml.jackson.core.type.TypeReference;
  19 +import com.fasterxml.jackson.databind.JsonNode;
  20 +import com.fasterxml.jackson.databind.ObjectMapper;
  21 +import io.jsonwebtoken.Claims;
  22 +import io.jsonwebtoken.Header;
  23 +import io.jsonwebtoken.Jwt;
  24 +import io.jsonwebtoken.Jwts;
  25 +import lombok.extern.slf4j.Slf4j;
  26 +import org.apache.commons.lang3.StringUtils;
  27 +import org.hamcrest.Matcher;
  28 +import org.junit.After;
  29 +import org.junit.Assert;
  30 +import org.junit.Before;
  31 +import org.junit.Rule;
  32 +import org.junit.rules.TestRule;
  33 +import org.junit.rules.TestWatcher;
  34 +import org.junit.runner.Description;
  35 +import org.junit.runner.RunWith;
  36 +import org.springframework.beans.factory.annotation.Autowired;
  37 +import org.springframework.boot.test.context.SpringBootContextLoader;
  38 +import org.springframework.boot.test.context.SpringBootTest;
  39 +import org.springframework.boot.web.server.LocalServerPort;
  40 +import org.springframework.context.annotation.ComponentScan;
  41 +import org.springframework.context.annotation.Configuration;
  42 +import org.springframework.http.HttpHeaders;
  43 +import org.springframework.http.MediaType;
  44 +import org.springframework.http.converter.HttpMessageConverter;
  45 +import org.springframework.http.converter.StringHttpMessageConverter;
  46 +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
  47 +import org.springframework.mock.http.MockHttpInputMessage;
  48 +import org.springframework.mock.http.MockHttpOutputMessage;
  49 +import org.springframework.test.annotation.DirtiesContext;
  50 +import org.springframework.test.context.ActiveProfiles;
  51 +import org.springframework.test.context.ContextConfiguration;
  52 +import org.springframework.test.context.junit4.SpringRunner;
  53 +import org.springframework.test.web.servlet.MockMvc;
  54 +import org.springframework.test.web.servlet.MvcResult;
  55 +import org.springframework.test.web.servlet.ResultActions;
  56 +import org.springframework.test.web.servlet.ResultMatcher;
  57 +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
  58 +import org.springframework.util.LinkedMultiValueMap;
  59 +import org.springframework.util.MultiValueMap;
  60 +import org.springframework.web.context.WebApplicationContext;
  61 +import org.thingsboard.server.common.data.BaseData;
  62 +import org.thingsboard.server.common.data.Customer;
  63 +import org.thingsboard.server.common.data.Tenant;
  64 +import org.thingsboard.server.common.data.User;
  65 +import org.thingsboard.server.common.data.id.TenantId;
  66 +import org.thingsboard.server.common.data.id.UUIDBased;
  67 +import org.thingsboard.server.common.data.page.PageLink;
  68 +import org.thingsboard.server.common.data.page.TimePageLink;
  69 +import org.thingsboard.server.common.data.security.Authority;
  70 +import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
  71 +import org.thingsboard.server.service.mail.TestMailService;
  72 +import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest;
  73 +import org.thingsboard.server.service.security.auth.rest.LoginRequest;
  74 +
  75 +import java.io.IOException;
  76 +import java.net.URI;
  77 +import java.net.URISyntaxException;
  78 +import java.util.ArrayList;
  79 +import java.util.Arrays;
  80 +import java.util.Comparator;
  81 +import java.util.List;
  82 +
  83 +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
  84 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
  85 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
  86 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
  87 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
  88 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
  89 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
  90 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
  91 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
  92 +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
  93 +
  94 +@ActiveProfiles("test")
  95 +@RunWith(SpringRunner.class)
  96 +@ContextConfiguration(classes = AbstractControllerTest.class, loader = SpringBootContextLoader.class)
  97 +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
  98 +@Configuration
  99 +@ComponentScan({"org.thingsboard.server"})
  100 +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
  101 +@Slf4j
  102 +public abstract class AbstractWebsocketTest extends AbstractWebTest {
  103 +
  104 + protected static final String WS_URL = "ws://localhost:";
  105 +
  106 + @LocalServerPort
  107 + protected int wsPort;
  108 +
  109 + protected TbTestWebSocketClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException {
  110 + TbTestWebSocketClient wsClient = new TbTestWebSocketClient(new URI(WS_URL + wsPort + "/api/ws/plugins/telemetry?token=" + token));
  111 + Assert.assertTrue(wsClient.connectBlocking());
  112 + return wsClient;
  113 + }
  114 +
  115 +}
@@ -16,10 +16,16 @@ @@ -16,10 +16,16 @@
16 package org.thingsboard.server.controller; 16 package org.thingsboard.server.controller;
17 17
18 import com.fasterxml.jackson.core.type.TypeReference; 18 import com.fasterxml.jackson.core.type.TypeReference;
  19 +import com.google.gson.JsonArray;
  20 +import com.google.gson.JsonObject;
  21 +import org.apache.http.conn.ssl.TrustStrategy;
  22 +import org.apache.http.ssl.SSLContextBuilder;
  23 +import org.apache.http.ssl.SSLContexts;
19 import org.junit.After; 24 import org.junit.After;
20 import org.junit.Assert; 25 import org.junit.Assert;
21 import org.junit.Before; 26 import org.junit.Before;
22 import org.junit.Test; 27 import org.junit.Test;
  28 +import org.springframework.boot.web.server.LocalServerPort;
23 import org.thingsboard.server.common.data.DataConstants; 29 import org.thingsboard.server.common.data.DataConstants;
24 import org.thingsboard.server.common.data.Device; 30 import org.thingsboard.server.common.data.Device;
25 import org.thingsboard.server.common.data.EntityType; 31 import org.thingsboard.server.common.data.EntityType;
@@ -27,6 +33,7 @@ import org.thingsboard.server.common.data.Tenant; @@ -27,6 +33,7 @@ import org.thingsboard.server.common.data.Tenant;
27 import org.thingsboard.server.common.data.User; 33 import org.thingsboard.server.common.data.User;
28 import org.thingsboard.server.common.data.id.DeviceId; 34 import org.thingsboard.server.common.data.id.DeviceId;
29 import org.thingsboard.server.common.data.id.EntityId; 35 import org.thingsboard.server.common.data.id.EntityId;
  36 +import org.thingsboard.server.common.data.kv.Aggregation;
30 import org.thingsboard.server.common.data.page.PageData; 37 import org.thingsboard.server.common.data.page.PageData;
31 import org.thingsboard.server.common.data.query.DeviceTypeFilter; 38 import org.thingsboard.server.common.data.query.DeviceTypeFilter;
32 import org.thingsboard.server.common.data.query.EntityCountQuery; 39 import org.thingsboard.server.common.data.query.EntityCountQuery;
@@ -40,10 +47,16 @@ import org.thingsboard.server.common.data.query.EntityListFilter; @@ -40,10 +47,16 @@ import org.thingsboard.server.common.data.query.EntityListFilter;
40 import org.thingsboard.server.common.data.query.KeyFilter; 47 import org.thingsboard.server.common.data.query.KeyFilter;
41 import org.thingsboard.server.common.data.query.NumericFilterPredicate; 48 import org.thingsboard.server.common.data.query.NumericFilterPredicate;
42 import org.thingsboard.server.common.data.security.Authority; 49 import org.thingsboard.server.common.data.security.Authority;
  50 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
  51 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd;
43 52
  53 +import java.net.URI;
  54 +import java.net.URISyntaxException;
44 import java.util.ArrayList; 55 import java.util.ArrayList;
  56 +import java.util.Arrays;
45 import java.util.Collections; 57 import java.util.Collections;
46 import java.util.List; 58 import java.util.List;
  59 +import java.util.Random;
47 import java.util.stream.Collectors; 60 import java.util.stream.Collectors;
48 61
49 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 62 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -190,23 +203,23 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe @@ -190,23 +203,23 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe
190 List<Device> devices = new ArrayList<>(); 203 List<Device> devices = new ArrayList<>();
191 List<Long> temperatures = new ArrayList<>(); 204 List<Long> temperatures = new ArrayList<>();
192 List<Long> highTemperatures = new ArrayList<>(); 205 List<Long> highTemperatures = new ArrayList<>();
193 - for (int i=0;i<67;i++) { 206 + for (int i = 0; i < 67; i++) {
194 Device device = new Device(); 207 Device device = new Device();
195 - String name = "Device"+i; 208 + String name = "Device" + i;
196 device.setName(name); 209 device.setName(name);
197 device.setType("default"); 210 device.setType("default");
198 - device.setLabel("testLabel"+(int)(Math.random()*1000));  
199 - devices.add(doPost("/api/device?accessToken="+name, device, Device.class));  
200 - long temperature = (long)(Math.random()*100); 211 + device.setLabel("testLabel" + (int) (Math.random() * 1000));
  212 + devices.add(doPost("/api/device?accessToken=" + name, device, Device.class));
  213 + long temperature = (long) (Math.random() * 100);
201 temperatures.add(temperature); 214 temperatures.add(temperature);
202 if (temperature > 45) { 215 if (temperature > 45) {
203 highTemperatures.add(temperature); 216 highTemperatures.add(temperature);
204 } 217 }
205 } 218 }
206 - for (int i=0;i<devices.size();i++) { 219 + for (int i = 0; i < devices.size(); i++) {
207 Device device = devices.get(i); 220 Device device = devices.get(i);
208 - String payload = "{\"temperature\":"+temperatures.get(i)+"}";  
209 - doPost("/api/plugins/telemetry/"+device.getId()+"/"+ DataConstants.SHARED_SCOPE, payload, String.class, status().isOk()); 221 + String payload = "{\"temperature\":" + temperatures.get(i) + "}";
  222 + doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, payload, String.class, status().isOk());
210 } 223 }
211 Thread.sleep(1000); 224 Thread.sleep(1000);
212 225
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.controller;
  17 +
  18 +import org.junit.After;
  19 +import org.junit.Assert;
  20 +import org.junit.Before;
  21 +import org.junit.Test;
  22 +import org.springframework.beans.factory.annotation.Autowired;
  23 +import org.thingsboard.server.common.data.Device;
  24 +import org.thingsboard.server.common.data.Tenant;
  25 +import org.thingsboard.server.common.data.User;
  26 +import org.thingsboard.server.common.data.kv.Aggregation;
  27 +import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
  28 +import org.thingsboard.server.common.data.kv.LongDataEntry;
  29 +import org.thingsboard.server.common.data.kv.TsKvEntry;
  30 +import org.thingsboard.server.common.data.page.PageData;
  31 +import org.thingsboard.server.common.data.query.DeviceTypeFilter;
  32 +import org.thingsboard.server.common.data.query.EntityData;
  33 +import org.thingsboard.server.common.data.query.EntityDataPageLink;
  34 +import org.thingsboard.server.common.data.query.EntityDataQuery;
  35 +import org.thingsboard.server.common.data.query.TsValue;
  36 +import org.thingsboard.server.common.data.security.Authority;
  37 +import org.thingsboard.server.dao.timeseries.TimeseriesService;
  38 +import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper;
  39 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
  40 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
  41 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd;
  42 +
  43 +import java.util.Arrays;
  44 +import java.util.Collections;
  45 +import java.util.concurrent.TimeUnit;
  46 +
  47 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
  48 +
  49 +public class BaseWebsocketApiTest extends AbstractWebsocketTest {
  50 +
  51 + private Tenant savedTenant;
  52 + private User tenantAdmin;
  53 + private TbTestWebSocketClient wsClient;
  54 +
  55 + @Autowired
  56 + private TimeseriesService tsService;
  57 +
  58 + @Before
  59 + public void beforeTest() throws Exception {
  60 + loginSysAdmin();
  61 +
  62 + Tenant tenant = new Tenant();
  63 + tenant.setTitle("My tenant");
  64 + savedTenant = doPost("/api/tenant", tenant, Tenant.class);
  65 + Assert.assertNotNull(savedTenant);
  66 +
  67 + tenantAdmin = new User();
  68 + tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
  69 + tenantAdmin.setTenantId(savedTenant.getId());
  70 + tenantAdmin.setEmail("tenant2@thingsboard.org");
  71 + tenantAdmin.setFirstName("Joe");
  72 + tenantAdmin.setLastName("Downs");
  73 +
  74 + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
  75 +
  76 + wsClient = buildAndConnectWebSocketClient();
  77 + }
  78 +
  79 + @After
  80 + public void afterTest() throws Exception {
  81 + wsClient.close();
  82 +
  83 + loginSysAdmin();
  84 +
  85 + doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
  86 + .andExpect(status().isOk());
  87 + }
  88 +
  89 + @Test
  90 + public void testEntityDataHistoryWsCmd() throws Exception {
  91 + Device device = new Device();
  92 + device.setName("Device");
  93 + device.setType("default");
  94 + device.setLabel("testLabel" + (int) (Math.random() * 1000));
  95 + device = doPost("/api/device", device, Device.class);
  96 +
  97 + long now = System.currentTimeMillis();
  98 +
  99 + DeviceTypeFilter dtf = new DeviceTypeFilter();
  100 + dtf.setDeviceNameFilter("D");
  101 + dtf.setDeviceType("default");
  102 + EntityDataQuery edq = new EntityDataQuery(dtf, new EntityDataPageLink(1, 0, null, null), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
  103 +
  104 + EntityHistoryCmd historyCmd = new EntityHistoryCmd();
  105 + historyCmd.setKeys(Arrays.asList("temperature"));
  106 + historyCmd.setAgg(Aggregation.NONE);
  107 + historyCmd.setLimit(1000);
  108 + historyCmd.setStartTs(now - TimeUnit.HOURS.toMillis(1));
  109 + historyCmd.setEndTs(now);
  110 + EntityDataCmd cmd = new EntityDataCmd(1, edq, historyCmd, null, null);
  111 +
  112 + TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper();
  113 + wrapper.setEntityDataCmds(Collections.singletonList(cmd));
  114 +
  115 + wsClient.send(mapper.writeValueAsString(wrapper));
  116 + String msg = wsClient.waitForReply();
  117 + EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class);
  118 + Assert.assertEquals(1, update.getCmdId());
  119 + PageData<EntityData> pageData = update.getData();
  120 + Assert.assertNotNull(pageData);
  121 + Assert.assertEquals(1, pageData.getData().size());
  122 + Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId());
  123 + Assert.assertEquals(0, pageData.getData().get(0).getTimeseries().get("temperature").length);
  124 +
  125 + TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L));
  126 + TsKvEntry dataPoint2 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(2), new LongDataEntry("temperature", 42L));
  127 + TsKvEntry dataPoint3 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(3), new LongDataEntry("temperature", 42L));
  128 + tsService.save(device.getTenantId(), device.getId(), Arrays.asList(dataPoint1, dataPoint2, dataPoint3), 0).get();
  129 +
  130 + wsClient.send(mapper.writeValueAsString(wrapper));
  131 + msg = wsClient.waitForReply();
  132 + update = mapper.readValue(msg, EntityDataUpdate.class);
  133 + Assert.assertEquals(1, update.getCmdId());
  134 + pageData = update.getData();
  135 + Assert.assertNotNull(pageData);
  136 + Assert.assertEquals(1, pageData.getData().size());
  137 + Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId());
  138 + TsValue[] tsArray = pageData.getData().get(0).getTimeseries().get("temperature");
  139 + Assert.assertEquals(3, tsArray.length);
  140 + Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsArray[0]);
  141 + Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsArray[1]);
  142 + Assert.assertEquals(new TsValue(dataPoint3.getTs(), dataPoint3.getValueAsString()), tsArray[2]);
  143 + }
  144 +
  145 +}
@@ -26,6 +26,8 @@ import java.util.Arrays; @@ -26,6 +26,8 @@ import java.util.Arrays;
26 26
27 @RunWith(ClasspathSuite.class) 27 @RunWith(ClasspathSuite.class)
28 @ClasspathSuite.ClassnameFilters({ 28 @ClasspathSuite.ClassnameFilters({
  29 +// "org.thingsboard.server.controller.sql.WebsocketApiSqlTest",
  30 +// "org.thingsboard.server.controller.sql.EntityQueryControllerSqlTest",
29 "org.thingsboard.server.controller.sql.*Test", 31 "org.thingsboard.server.controller.sql.*Test",
30 }) 32 })
31 public class ControllerSqlTestSuite { 33 public class ControllerSqlTestSuite {
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.controller;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.java_websocket.client.WebSocketClient;
  20 +import org.java_websocket.handshake.ServerHandshake;
  21 +
  22 +import java.net.URI;
  23 +import java.nio.channels.NotYetConnectedException;
  24 +import java.util.concurrent.CountDownLatch;
  25 +import java.util.concurrent.TimeUnit;
  26 +
  27 +@Slf4j
  28 +public class TbTestWebSocketClient extends WebSocketClient {
  29 +
  30 + private volatile String lastMsg;
  31 + private volatile boolean replyReceived;
  32 + private CountDownLatch reply;
  33 +
  34 + public TbTestWebSocketClient(URI serverUri) {
  35 + super(serverUri);
  36 + }
  37 +
  38 + @Override
  39 + public void onOpen(ServerHandshake serverHandshake) {
  40 +
  41 + }
  42 +
  43 + @Override
  44 + public void onMessage(String s) {
  45 + if (!replyReceived) {
  46 + replyReceived = true;
  47 + lastMsg = s;
  48 + if (reply != null) {
  49 + reply.countDown();
  50 + }
  51 + }
  52 + }
  53 +
  54 + @Override
  55 + public void onClose(int i, String s, boolean b) {
  56 +
  57 + }
  58 +
  59 + @Override
  60 + public void onError(Exception e) {
  61 +
  62 + }
  63 +
  64 + @Override
  65 + public void send(String text) throws NotYetConnectedException {
  66 + reply = new CountDownLatch(1);
  67 + replyReceived = false;
  68 + super.send(text);
  69 + }
  70 +
  71 + public String waitForReply() {
  72 + try {
  73 + reply.await(3, TimeUnit.SECONDS);
  74 + } catch (InterruptedException e) {
  75 + log.warn("Failed to await reply", e);
  76 + }
  77 + return lastMsg;
  78 + }
  79 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.controller.sql;
  17 +
  18 +import org.thingsboard.server.controller.BaseEntityQueryControllerTest;
  19 +import org.thingsboard.server.controller.BaseWebsocketApiTest;
  20 +import org.thingsboard.server.dao.service.DaoSqlTest;
  21 +
  22 +@DaoSqlTest
  23 +public class WebsocketApiSqlTest extends BaseWebsocketApiTest {
  24 +}
@@ -37,7 +37,6 @@ @@ -37,7 +37,6 @@
37 <blackBoxTests.skip>true</blackBoxTests.skip> 37 <blackBoxTests.skip>true</blackBoxTests.skip>
38 <testcontainers.version>1.9.1</testcontainers.version> 38 <testcontainers.version>1.9.1</testcontainers.version>
39 <zeroturnaround.version>1.10</zeroturnaround.version> 39 <zeroturnaround.version>1.10</zeroturnaround.version>
40 - <java-websocket.version>1.3.9</java-websocket.version>  
41 <httpclient.version>4.5.6</httpclient.version> 40 <httpclient.version>4.5.6</httpclient.version>
42 </properties> 41 </properties>
43 42
@@ -55,7 +54,6 @@ @@ -55,7 +54,6 @@
55 <dependency> 54 <dependency>
56 <groupId>org.java-websocket</groupId> 55 <groupId>org.java-websocket</groupId>
57 <artifactId>Java-WebSocket</artifactId> 56 <artifactId>Java-WebSocket</artifactId>
58 - <version>${java-websocket.version}</version>  
59 </dependency> 57 </dependency>
60 <dependency> 58 <dependency>
61 <groupId>org.apache.httpcomponents</groupId> 59 <groupId>org.apache.httpcomponents</groupId>
@@ -105,6 +105,7 @@ @@ -105,6 +105,7 @@
105 <ua-parser.version>1.4.3</ua-parser.version> 105 <ua-parser.version>1.4.3</ua-parser.version>
106 <commons-beanutils.version>1.9.4</commons-beanutils.version> 106 <commons-beanutils.version>1.9.4</commons-beanutils.version>
107 <commons-collections.version>3.2.2</commons-collections.version> 107 <commons-collections.version>3.2.2</commons-collections.version>
  108 + <java-websocket.version>1.3.9</java-websocket.version>
108 </properties> 109 </properties>
109 110
110 <modules> 111 <modules>
@@ -1346,6 +1347,12 @@ @@ -1346,6 +1347,12 @@
1346 <artifactId>commons-collections</artifactId> 1347 <artifactId>commons-collections</artifactId>
1347 <version>${commons-collections.version}</version> 1348 <version>${commons-collections.version}</version>
1348 </dependency> 1349 </dependency>
  1350 + <dependency>
  1351 + <groupId>org.java-websocket</groupId>
  1352 + <artifactId>Java-WebSocket</artifactId>
  1353 + <version>${java-websocket.version}</version>
  1354 + <scope>test</scope>
  1355 + </dependency>
1349 </dependencies> 1356 </dependencies>
1350 </dependencyManagement> 1357 </dependencyManagement>
1351 1358