diff --git a/application/pom.xml b/application/pom.xml
index e964f94373..a2da7aea18 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -304,6 +304,11 @@
com.github.ua-parser
uap-java
+
+ org.java-websocket
+ Java-WebSocket
+ test
+
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
new file mode 100644
index 0000000000..a076129631
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
@@ -0,0 +1,312 @@
+/**
+ * Copyright © 2016-2020 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.subscription;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Service;
+import org.thingsboard.common.util.ThingsBoardThreadFactory;
+import org.thingsboard.server.common.data.EntityView;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityViewId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
+import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.query.EntityData;
+import org.thingsboard.server.common.data.query.EntityDataQuery;
+import org.thingsboard.server.common.data.query.TsValue;
+import org.thingsboard.server.common.msg.queue.ServiceType;
+import org.thingsboard.server.common.msg.queue.TbCallback;
+import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
+import org.thingsboard.server.dao.entity.EntityService;
+import org.thingsboard.server.dao.entityview.EntityViewService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.queue.discovery.ClusterTopologyChangeEvent;
+import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
+import org.thingsboard.server.queue.discovery.PartitionService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.queue.TbClusterService;
+import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
+import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
+import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
+import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd;
+import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
+import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd;
+import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd;
+import org.thingsboard.server.service.telemetry.cmd.v2.TimeSeriesCmd;
+import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Slf4j
+@TbCoreComponent
+@Service
+public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubscriptionService {
+
+ private static final int DEFAULT_LIMIT = 100;
+ private final Set currentPartitions = ConcurrentHashMap.newKeySet();
+ private final Map> subscriptionsBySessionId = new ConcurrentHashMap<>();
+
+ @Autowired
+ private TelemetryWebSocketService wsService;
+
+ @Autowired
+ private EntityViewService entityViewService;
+
+ @Autowired
+ private EntityService entityService;
+
+ @Autowired
+ private PartitionService partitionService;
+
+ @Autowired
+ private TbClusterService clusterService;
+
+ @Autowired
+ @Lazy
+ private SubscriptionManagerService subscriptionManagerService;
+
+ @Autowired
+ private TimeseriesService tsService;
+
+ private ExecutorService wsCallBackExecutor;
+
+ @PostConstruct
+ public void initExecutor() {
+ wsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ws-entity-sub-callback"));
+ }
+
+ @PreDestroy
+ public void shutdownExecutor() {
+ if (wsCallBackExecutor != null) {
+ wsCallBackExecutor.shutdownNow();
+ }
+ }
+
+ @Override
+ @EventListener(PartitionChangeEvent.class)
+ public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) {
+ if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) {
+ currentPartitions.clear();
+ currentPartitions.addAll(partitionChangeEvent.getPartitions());
+ }
+ }
+
+ @Override
+ @EventListener(ClusterTopologyChangeEvent.class)
+ public void onApplicationEvent(ClusterTopologyChangeEvent event) {
+ if (event.getServiceQueueKeys().stream().anyMatch(key -> ServiceType.TB_CORE.equals(key.getServiceType()))) {
+ /*
+ * If the cluster topology has changed, we need to push all current subscriptions to SubscriptionManagerService again.
+ * Otherwise, the SubscriptionManagerService may "forget" those subscriptions in case of restart.
+ * Although this is resource consuming operation, it is cheaper than sending ping/pong commands periodically
+ * 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.
+ * Even if we cache locally the list of active subscriptions by entity id, it is still time consuming operation to get them from cache
+ * Since number of subscriptions is usually much less then number of devices that are pushing data.
+// */
+// subscriptionsBySessionId.values().forEach(map -> map.values()
+// .forEach(sub -> pushSubscriptionToManagerService(sub, false)));
+ }
+ }
+
+ @Override
+ public void handleCmd(TelemetryWebSocketSessionRef session, EntityDataCmd cmd) {
+ if (cmd.getHistoryCmd() != null) {
+ handleHistoryCmd(session, cmd.getCmdId(), cmd.getQuery(), cmd.getHistoryCmd());
+ } else if (cmd.getLatestCmd() != null) {
+ handleLatestCmd(session, cmd.getCmdId(), cmd.getQuery(), cmd.getLatestCmd());
+ } else {
+ handleTimeseriesCmd(session, cmd.getCmdId(), cmd.getQuery(), cmd.getTsCmd());
+ }
+ }
+
+ private void handleTimeseriesCmd(TelemetryWebSocketSessionRef session, int cmdId, EntityDataQuery query, TimeSeriesCmd tsCmd) {
+ }
+
+ private void handleLatestCmd(TelemetryWebSocketSessionRef session, int cmdId, EntityDataQuery query, LatestValueCmd latestCmd) {
+
+ }
+
+ private void handleHistoryCmd(TelemetryWebSocketSessionRef session, int cmdId, EntityDataQuery query, EntityHistoryCmd historyCmd) {
+ TenantId tenantId = session.getSecurityCtx().getTenantId();
+ CustomerId customerId = session.getSecurityCtx().getCustomerId();
+ PageData data = entityService.findEntityDataByQuery(tenantId, customerId, query);
+ List tsKvQueryList = historyCmd.getKeys().stream().map(key -> new BaseReadTsKvQuery(
+ key, historyCmd.getStartTs(), historyCmd.getEndTs(), historyCmd.getInterval(), getLimit(historyCmd.getLimit()), historyCmd.getAgg()
+ )).collect(Collectors.toList());
+ Map>> fetchResultMap = new HashMap<>();
+ data.getData().forEach(entityData -> fetchResultMap.put(entityData,
+ tsService.findAll(tenantId, entityData.getEntityId(), tsKvQueryList)));
+ Futures.allAsList(fetchResultMap.values()).addListener(() -> {
+ fetchResultMap.forEach((entityData, future) -> {
+ Map> keyData = new LinkedHashMap<>();
+ historyCmd.getKeys().forEach(key -> keyData.put(key, new ArrayList<>()));
+ try {
+ List entityTsData = future.get();
+ if (entityTsData != null) {
+ entityTsData.forEach(entry -> keyData.get(entry.getKey()).add(new TsValue(entry.getTs(), entry.getValueAsString())));
+ }
+ keyData.forEach((k, v) -> entityData.getTimeseries().put(k, v.toArray(new TsValue[v.size()])));
+ } catch (InterruptedException | ExecutionException e) {
+ log.warn("[{}][{}][{}] Failed to fetch historical data", session.getSessionId(), cmdId, entityData.getEntityId(), e);
+ }
+ });
+ EntityDataUpdate update = new EntityDataUpdate(cmdId, data, null);
+ wsService.sendWsMsg(session.getSessionId(), update);
+ }, wsCallBackExecutor);
+ }
+
+
+ @Override
+ public void cancelSubscription(String sessionId, EntityDataUnsubscribeCmd subscriptionId) {
+
+ }
+
+// //TODO 3.1: replace null callbacks with callbacks from websocket service.
+// @Override
+// public void addSubscription(TbSubscription subscription) {
+// EntityId entityId = subscription.getEntityId();
+// // Telemetry subscription on Entity Views are handled differently, because we need to allow only certain keys and time ranges;
+// if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW) && TbSubscriptionType.TIMESERIES.equals(subscription.getType())) {
+// subscription = resolveEntityViewSubscription((TbTimeseriesSubscription) subscription);
+// }
+// pushSubscriptionToManagerService(subscription, true);
+// registerSubscription(subscription);
+// }
+
+// private void pushSubscriptionToManagerService(TbSubscription subscription, boolean pushToLocalService) {
+// TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, subscription.getTenantId(), subscription.getEntityId());
+// if (currentPartitions.contains(tpi)) {
+// // Subscription is managed on the same server;
+// if (pushToLocalService) {
+// subscriptionManagerService.addSubscription(subscription, TbCallback.EMPTY);
+// }
+// } else {
+// // Push to the queue;
+// TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toNewSubscriptionProto(subscription);
+// clusterService.pushMsgToCore(tpi, subscription.getEntityId().getId(), toCoreMsg, null);
+// }
+// }
+
+ @Override
+ public void onSubscriptionUpdate(String sessionId, SubscriptionUpdate update, TbCallback callback) {
+// TbSubscription subscription = subscriptionsBySessionId
+// .getOrDefault(sessionId, Collections.emptyMap()).get(update.getSubscriptionId());
+// if (subscription != null) {
+// switch (subscription.getType()) {
+// case TIMESERIES:
+// TbTimeseriesSubscription tsSub = (TbTimeseriesSubscription) subscription;
+// update.getLatestValues().forEach((key, value) -> tsSub.getKeyStates().put(key, value));
+// break;
+// case ATTRIBUTES:
+// TbAttributeSubscription attrSub = (TbAttributeSubscription) subscription;
+// update.getLatestValues().forEach((key, value) -> attrSub.getKeyStates().put(key, value));
+// break;
+// }
+// wsService.sendWsMsg(sessionId, update);
+// }
+// callback.onSuccess();
+ }
+
+// @Override
+// public void cancelSubscription(String sessionId, int subscriptionId) {
+// log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId);
+// Map sessionSubscriptions = subscriptionsBySessionId.get(sessionId);
+// if (sessionSubscriptions != null) {
+// TbSubscription subscription = sessionSubscriptions.remove(subscriptionId);
+// if (subscription != null) {
+// if (sessionSubscriptions.isEmpty()) {
+// subscriptionsBySessionId.remove(sessionId);
+// }
+// TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, subscription.getTenantId(), subscription.getEntityId());
+// if (currentPartitions.contains(tpi)) {
+// // Subscription is managed on the same server;
+// subscriptionManagerService.cancelSubscription(sessionId, subscriptionId, TbCallback.EMPTY);
+// } else {
+// // Push to the queue;
+// TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toCloseSubscriptionProto(subscription);
+// clusterService.pushMsgToCore(tpi, subscription.getEntityId().getId(), toCoreMsg, null);
+// }
+// } else {
+// log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId);
+// }
+// } else {
+// log.debug("[{}] No session subscriptions found!", sessionId);
+// }
+// }
+
+ @Override
+ public void cancelAllSessionSubscriptions(String sessionId) {
+// Map subscriptions = subscriptionsBySessionId.get(sessionId);
+// if (subscriptions != null) {
+// Set toRemove = new HashSet<>(subscriptions.keySet());
+// toRemove.forEach(id -> cancelSubscription(sessionId, id));
+// }
+ }
+
+ private TbSubscription resolveEntityViewSubscription(TbTimeseriesSubscription subscription) {
+ EntityView entityView = entityViewService.findEntityViewById(TenantId.SYS_TENANT_ID, new EntityViewId(subscription.getEntityId().getId()));
+
+ Map keyStates;
+ if (subscription.isAllKeys()) {
+ keyStates = entityView.getKeys().getTimeseries().stream().collect(Collectors.toMap(k -> k, k -> 0L));
+ } else {
+ keyStates = subscription.getKeyStates().entrySet()
+ .stream().filter(entry -> entityView.getKeys().getTimeseries().contains(entry.getKey()))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ }
+
+ return TbTimeseriesSubscription.builder()
+ .serviceId(subscription.getServiceId())
+ .sessionId(subscription.getSessionId())
+ .subscriptionId(subscription.getSubscriptionId())
+ .tenantId(subscription.getTenantId())
+ .entityId(entityView.getEntityId())
+ .startTime(entityView.getStartTimeMs())
+ .endTime(entityView.getEndTimeMs())
+ .allKeys(false)
+ .keyStates(keyStates).build();
+ }
+
+ private void registerSubscription(TbSubscription subscription) {
+ Map sessionSubscriptions = subscriptionsBySessionId.computeIfAbsent(subscription.getSessionId(), k -> new ConcurrentHashMap<>());
+ sessionSubscriptions.put(subscription.getSubscriptionId(), subscription);
+ }
+
+ private int getLimit(int limit) {
+ return limit == 0 ? DEFAULT_LIMIT : limit;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java
new file mode 100644
index 0000000000..f0b5fb85b5
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016-2020 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.subscription;
+
+import org.thingsboard.server.common.msg.queue.TbCallback;
+import org.thingsboard.server.queue.discovery.ClusterTopologyChangeEvent;
+import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
+import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
+import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
+import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd;
+import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate;
+
+public interface TbEntityDataSubscriptionService {
+
+ void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityDataCmd cmd);
+
+ void cancelSubscription(String sessionId, EntityDataUnsubscribeCmd subscriptionId);
+
+ void cancelAllSessionSubscriptions(String sessionId);
+
+ void onSubscriptionUpdate(String sessionId, SubscriptionUpdate update, TbCallback callback);
+
+ void onApplicationEvent(PartitionChangeEvent event);
+
+ void onApplicationEvent(ClusterTopologyChangeEvent event);
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
index cfc6d45f0d..1a065c07fb 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
@@ -49,8 +49,10 @@ import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.ValidationCallback;
import org.thingsboard.server.service.security.ValidationResult;
import org.thingsboard.server.service.security.ValidationResultCode;
+import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.permission.Operation;
+import org.thingsboard.server.service.subscription.TbEntityDataSubscriptionService;
import org.thingsboard.server.service.subscription.TbLocalSubscriptionService;
import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope;
import org.thingsboard.server.service.subscription.TbAttributeSubscription;
@@ -61,6 +63,9 @@ import org.thingsboard.server.service.telemetry.cmd.v1.SubscriptionCmd;
import org.thingsboard.server.service.telemetry.cmd.v1.TelemetryPluginCmd;
import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper;
import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd;
+import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
+import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd;
+import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.telemetry.exception.UnauthorizedException;
import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode;
import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate;
@@ -104,7 +109,10 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
private final ConcurrentMap wsSessionsMap = new ConcurrentHashMap<>();
@Autowired
- private TbLocalSubscriptionService subService;
+ private TbLocalSubscriptionService oldSubService;
+
+ @Autowired
+ private TbEntityDataSubscriptionService entityDataSubService;
@Autowired
private TelemetryWebSocketMsgEndpoint msgEndpoint;
@@ -164,7 +172,8 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
break;
case CLOSED:
wsSessionsMap.remove(sessionId);
- subService.cancelAllSessionSubscriptions(sessionId);
+ oldSubService.cancelAllSessionSubscriptions(sessionId);
+ entityDataSubService.cancelAllSessionSubscriptions(sessionId);
processSessionClose(sessionRef);
break;
}
@@ -196,6 +205,12 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
if (cmdsWrapper.getHistoryCmds() != null) {
cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(sessionRef, cmd));
}
+ if (cmdsWrapper.getEntityDataCmds() != null) {
+ cmdsWrapper.getEntityDataCmds().forEach(cmd -> handleWsEntityDataCmd(sessionRef, cmd));
+ }
+ if (cmdsWrapper.getEntityDataUnsubscribeCmds() != null) {
+ cmdsWrapper.getEntityDataUnsubscribeCmds().forEach(cmd -> handleWsEntityDataUnsubscribeCmd(sessionRef, cmd));
+ }
}
} catch (IOException e) {
log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e);
@@ -204,11 +219,39 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
}
+ private void handleWsEntityDataCmd(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) {
+ String sessionId = sessionRef.getSessionId();
+ log.debug("[{}] Processing: {}", sessionId, cmd);
+
+ if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)
+ && validateSubscriptionCmd(sessionRef, cmd)) {
+ entityDataSubService.handleCmd(sessionRef, cmd);
+ }
+ }
+
+ private void handleWsEntityDataUnsubscribeCmd(TelemetryWebSocketSessionRef sessionRef, EntityDataUnsubscribeCmd cmd) {
+ String sessionId = sessionRef.getSessionId();
+ log.debug("[{}] Processing: {}", sessionId, cmd);
+
+ if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)) {
+ entityDataSubService.cancelSubscription(sessionRef.getSessionId(), cmd);
+ }
+ }
+
@Override
public void sendWsMsg(String sessionId, SubscriptionUpdate update) {
+ sendWsMsg(sessionId, update.getSubscriptionId(), update);
+ }
+
+ @Override
+ public void sendWsMsg(String sessionId, EntityDataUpdate update) {
+ sendWsMsg(sessionId, update.getCmdId(), update);
+ }
+
+ private void sendWsMsg(String sessionId, int cmdId, T update) {
WsSessionMetaData md = wsSessionsMap.get(sessionId);
if (md != null) {
- sendWsMsg(md.getSessionRef(), update);
+ sendWsMsg(md.getSessionRef(), cmdId, update);
}
}
@@ -356,7 +399,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
.allKeys(false)
.keyStates(subState)
.scope(scope).build();
- subService.addSubscription(sub);
+ oldSubService.addSubscription(sub);
}
@Override
@@ -453,7 +496,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
.allKeys(true)
.keyStates(subState)
.scope(scope).build();
- subService.addSubscription(sub);
+ oldSubService.addSubscription(sub);
}
@Override
@@ -534,7 +577,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
.entityId(entityId)
.allKeys(true)
.keyStates(subState).build();
- subService.addSubscription(sub);
+ oldSubService.addSubscription(sub);
}
@Override
@@ -571,7 +614,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
.entityId(entityId)
.allKeys(false)
.keyStates(subState).build();
- subService.addSubscription(sub);
+ oldSubService.addSubscription(sub);
}
@Override
@@ -590,12 +633,32 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
private void unsubscribe(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
- subService.cancelAllSessionSubscriptions(sessionId);
+ oldSubService.cancelAllSessionSubscriptions(sessionId);
} else {
- subService.cancelSubscription(sessionId, cmd.getCmdId());
+ oldSubService.cancelSubscription(sessionId, cmd.getCmdId());
}
}
+ private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) {
+ if (cmd.getCmdId() < 0) {
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+ "Cmd id is negative value!");
+ sendWsMsg(sessionRef, update);
+ return false;
+ } else if (cmd.getQuery() == null) {
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+ "Query is empty!");
+ sendWsMsg(sessionRef, update);
+ return false;
+ } else if (cmd.getHistoryCmd() == null && cmd.getLatestCmd() == null && cmd.getTsCmd() == null) {
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+ "No history, latest or timeseries command present!");
+ sendWsMsg(sessionRef, update);
+ return false;
+ }
+ return true;
+ }
+
private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) {
if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
@@ -607,10 +670,14 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
+ return validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId);
+ }
+
+ private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, int cmdId, String sessionId) {
WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
if (sessionMD == null) {
log.warn("[{}] Session meta data not found. ", sessionId);
- SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+ SubscriptionUpdate update = new SubscriptionUpdate(cmdId, SubscriptionErrorCode.INTERNAL_ERROR,
SESSION_META_DATA_NOT_FOUND);
sendWsMsg(sessionRef, update);
return false;
@@ -619,10 +686,18 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
}
+ private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, EntityDataUpdate update) {
+ sendWsMsg(sessionRef, update.getCmdId(), update);
+ }
+
private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, SubscriptionUpdate update) {
+ sendWsMsg(sessionRef, update.getSubscriptionId(), update);
+ }
+
+ private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, int cmdId, Object update) {
executor.submit(() -> {
try {
- msgEndpoint.send(sessionRef, update.getSubscriptionId(), jsonMapper.writeValueAsString(update));
+ msgEndpoint.send(sessionRef, cmdId, jsonMapper.writeValueAsString(update));
} catch (JsonProcessingException e) {
log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e);
} catch (IOException e) {
@@ -631,6 +706,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
});
}
+
private static Optional> getKeys(TelemetryPluginCmd cmd) {
if (!StringUtils.isEmpty(cmd.getKeys())) {
Set keys = new HashSet<>();
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java
index f4f7274ae9..d04ff71546 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.telemetry;
+import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate;
/**
@@ -27,4 +28,7 @@ public interface TelemetryWebSocketService {
void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg);
void sendWsMsg(String sessionId, SubscriptionUpdate update);
+
+ void sendWsMsg(String sessionId, EntityDataUpdate update);
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityHistoryCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityHistoryCmd.java
index e4d882eece..af7f6f4d34 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityHistoryCmd.java
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityHistoryCmd.java
@@ -15,10 +15,12 @@
*/
package org.thingsboard.server.service.telemetry.cmd.v2;
+import lombok.Data;
import org.thingsboard.server.common.data.kv.Aggregation;
import java.util.List;
+@Data
public class EntityHistoryCmd {
private List keys;
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
index dcaf434440..431ff385fc 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
@@ -91,409 +91,8 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC
@Configuration
@ComponentScan({"org.thingsboard.server"})
@WebAppConfiguration
-@SpringBootTest
+@SpringBootTest()
@Slf4j
-public abstract class AbstractControllerTest {
-
- protected ObjectMapper mapper = new ObjectMapper();
-
- protected static final String TEST_TENANT_NAME = "TEST TENANT";
-
- protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org";
- private static final String SYS_ADMIN_PASSWORD = "sysadmin";
-
- protected static final String TENANT_ADMIN_EMAIL = "testtenant@thingsboard.org";
- private static final String TENANT_ADMIN_PASSWORD = "tenant";
-
- protected static final String CUSTOMER_USER_EMAIL = "testcustomer@thingsboard.org";
- private static final String CUSTOMER_USER_PASSWORD = "customer";
-
- /** See {@link org.springframework.test.web.servlet.DefaultMvcResult#getAsyncResult(long)}
- * and {@link org.springframework.mock.web.MockAsyncContext#getTimeout()}
- */
- private static final long DEFAULT_TIMEOUT = -1L;
-
- protected MediaType contentType = MediaType.APPLICATION_JSON;
-
- protected MockMvc mockMvc;
-
- protected String token;
- protected String refreshToken;
- protected String username;
-
- private TenantId tenantId;
-
- @SuppressWarnings("rawtypes")
- private HttpMessageConverter mappingJackson2HttpMessageConverter;
-
- @SuppressWarnings("rawtypes")
- private HttpMessageConverter stringHttpMessageConverter;
-
- @Autowired
- private WebApplicationContext webApplicationContext;
-
- @Rule
- public TestRule watcher = new TestWatcher() {
- protected void starting(Description description) {
- log.info("Starting test: {}", description.getMethodName());
- }
-
- protected void finished(Description description) {
- log.info("Finished test: {}", description.getMethodName());
- }
- };
-
- @Autowired
- void setConverters(HttpMessageConverter>[] converters) {
-
- this.mappingJackson2HttpMessageConverter = Arrays.stream(converters)
- .filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter)
- .findAny()
- .get();
-
- this.stringHttpMessageConverter = Arrays.stream(converters)
- .filter(hmc -> hmc instanceof StringHttpMessageConverter)
- .findAny()
- .get();
-
- Assert.assertNotNull("the JSON message converter must not be null",
- this.mappingJackson2HttpMessageConverter);
- }
-
- @Before
- public void setup() throws Exception {
- log.info("Executing setup");
- if (this.mockMvc == null) {
- this.mockMvc = webAppContextSetup(webApplicationContext)
- .apply(springSecurity()).build();
- }
- loginSysAdmin();
-
- Tenant tenant = new Tenant();
- tenant.setTitle(TEST_TENANT_NAME);
- Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
- Assert.assertNotNull(savedTenant);
- tenantId = savedTenant.getId();
-
- User tenantAdmin = new User();
- tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
- tenantAdmin.setTenantId(tenantId);
- tenantAdmin.setEmail(TENANT_ADMIN_EMAIL);
-
- createUserAndLogin(tenantAdmin, TENANT_ADMIN_PASSWORD);
-
- Customer customer = new Customer();
- customer.setTitle("Customer");
- customer.setTenantId(tenantId);
- Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
-
- User customerUser = new User();
- customerUser.setAuthority(Authority.CUSTOMER_USER);
- customerUser.setTenantId(tenantId);
- customerUser.setCustomerId(savedCustomer.getId());
- customerUser.setEmail(CUSTOMER_USER_EMAIL);
-
- createUserAndLogin(customerUser, CUSTOMER_USER_PASSWORD);
-
- logout();
-
- log.info("Executed setup");
- }
-
- @After
- public void teardown() throws Exception {
- log.info("Executing teardown");
- loginSysAdmin();
- doDelete("/api/tenant/" + tenantId.getId().toString())
- .andExpect(status().isOk());
- log.info("Executed teardown");
- }
-
- protected void loginSysAdmin() throws Exception {
- login(SYS_ADMIN_EMAIL, SYS_ADMIN_PASSWORD);
- }
-
- protected void loginTenantAdmin() throws Exception {
- login(TENANT_ADMIN_EMAIL, TENANT_ADMIN_PASSWORD);
- }
-
- protected void loginCustomerUser() throws Exception {
- login(CUSTOMER_USER_EMAIL, CUSTOMER_USER_PASSWORD);
- }
-
- private Tenant savedDifferentTenant;
- protected void loginDifferentTenant() throws Exception {
- loginSysAdmin();
- Tenant tenant = new Tenant();
- tenant.setTitle("Different tenant");
- savedDifferentTenant = doPost("/api/tenant", tenant, Tenant.class);
- Assert.assertNotNull(savedDifferentTenant);
- User differentTenantAdmin = new User();
- differentTenantAdmin.setAuthority(Authority.TENANT_ADMIN);
- differentTenantAdmin.setTenantId(savedDifferentTenant.getId());
- differentTenantAdmin.setEmail("different_tenant@thingsboard.org");
-
- createUserAndLogin(differentTenantAdmin, "testPassword");
- }
-
- protected void deleteDifferentTenant() throws Exception {
- loginSysAdmin();
- doDelete("/api/tenant/" + savedDifferentTenant.getId().getId().toString())
- .andExpect(status().isOk());
- }
-
- protected User createUserAndLogin(User user, String password) throws Exception {
- User savedUser = doPost("/api/user", user, User.class);
- logout();
- doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken)
- .andExpect(status().isSeeOther())
- .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
- JsonNode activateRequest = new ObjectMapper().createObjectNode()
- .put("activateToken", TestMailService.currentActivateToken)
- .put("password", password);
- JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
- validateAndSetJwtToken(tokenInfo, user.getEmail());
- return savedUser;
- }
-
- protected void login(String username, String password) throws Exception {
- this.token = null;
- this.refreshToken = null;
- this.username = null;
- JsonNode tokenInfo = readResponse(doPost("/api/auth/login", new LoginRequest(username, password)).andExpect(status().isOk()), JsonNode.class);
- validateAndSetJwtToken(tokenInfo, username);
- }
-
- protected void refreshToken() throws Exception {
- this.token = null;
- JsonNode tokenInfo = readResponse(doPost("/api/auth/token", new RefreshTokenRequest(this.refreshToken)).andExpect(status().isOk()), JsonNode.class);
- validateAndSetJwtToken(tokenInfo, this.username);
- }
-
- protected void validateAndSetJwtToken(JsonNode tokenInfo, String username) {
- Assert.assertNotNull(tokenInfo);
- Assert.assertTrue(tokenInfo.has("token"));
- Assert.assertTrue(tokenInfo.has("refreshToken"));
- String token = tokenInfo.get("token").asText();
- String refreshToken = tokenInfo.get("refreshToken").asText();
- validateJwtToken(token, username);
- validateJwtToken(refreshToken, username);
- this.token = token;
- this.refreshToken = refreshToken;
- this.username = username;
- }
-
- protected void validateJwtToken(String token, String username) {
- Assert.assertNotNull(token);
- Assert.assertFalse(token.isEmpty());
- int i = token.lastIndexOf('.');
- Assert.assertTrue(i > 0);
- String withoutSignature = token.substring(0, i + 1);
- Jwt jwsClaims = Jwts.parser().parseClaimsJwt(withoutSignature);
- Claims claims = jwsClaims.getBody();
- String subject = claims.getSubject();
- Assert.assertEquals(username, subject);
- }
-
- protected void logout() throws Exception {
- this.token = null;
- this.refreshToken = null;
- this.username = null;
- }
-
- protected void setJwtToken(MockHttpServletRequestBuilder request) {
- if (this.token != null) {
- request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token);
- }
- }
-
- protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception {
- MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables);
- setJwtToken(getRequest);
- return mockMvc.perform(getRequest);
- }
-
- protected T doGet(String urlTemplate, Class responseClass, Object... urlVariables) throws Exception {
- return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass);
- }
-
- protected T doGetAsync(String urlTemplate, Class responseClass, Object... urlVariables) throws Exception {
- return readResponse(doGetAsync(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass);
- }
-
- protected ResultActions doGetAsync(String urlTemplate, Object... urlVariables) throws Exception {
- MockHttpServletRequestBuilder getRequest;
- getRequest = get(urlTemplate, urlVariables);
- setJwtToken(getRequest);
- return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn()));
- }
-
- protected T doGetTyped(String urlTemplate, TypeReference responseType, Object... urlVariables) throws Exception {
- return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseType);
- }
-
- protected T doGetTypedWithPageLink(String urlTemplate, TypeReference responseType,
- PageLink pageLink,
- Object... urlVariables) throws Exception {
- List