Browse Source

Merge branch 'master' into feature/state-chart

pull/10535/head
Igor Kulikov 2 years ago
parent
commit
77d2552d6d
  1. 5
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 45
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  3. 12
      application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java
  4. 17
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  5. 10
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  6. 11
      application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
  7. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  8. 16
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/BaseMsgConstructorFactory.java
  9. 42
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java
  10. 89
      application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java
  11. 18
      application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java
  12. 16
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  13. 62
      application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
  14. 23
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
  15. 16
      application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
  16. 5
      application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java
  17. 4
      application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesJsonIntegrationTest.java
  18. 3
      application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesProtoIntegrationTest.java
  19. 4
      application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java
  20. 3
      application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcDefaultIntegrationTest.java
  21. 3
      application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcJsonIntegrationTest.java
  22. 3
      application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcProtoIntegrationTest.java
  23. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java
  24. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java
  25. 3
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java
  26. 20
      common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java
  27. 22
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
  28. 287
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java
  29. 100
      common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServiceTest.java
  30. 11
      common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientTest.java
  31. 41
      common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java
  32. 16
      common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java
  33. 14
      common/util/src/test/java/org/thingsboard/common/util/JacksonUtilTest.java
  34. 9
      dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
  35. 17
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java
  36. 121
      dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
  37. 10
      dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java
  38. 9
      dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java
  39. 3
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java
  40. 2
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  41. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java
  42. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/event/RuleNodeDebugEventRepository.java
  43. 31
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
  44. 5
      dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
  45. 129
      dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java
  46. 34
      dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java
  47. 22
      dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java
  48. 31
      dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java
  49. 3
      pom.xml
  50. 2
      rest-client/src/main/java/org/thingsboard/rest/client/utils/RestJsonConverter.java
  51. 14
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java
  52. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/BaseTbMsgPushNodeConfiguration.java
  53. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToCloudNodeConfiguration.java
  54. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNode.java
  55. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNodeConfiguration.java
  56. 27
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java
  57. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java
  58. 30
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
  59. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java
  60. 14
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
  61. 120
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java
  62. 60
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java
  63. 16
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java
  64. 296
      transport/lwm2m/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java

5
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -19,6 +19,8 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@ -116,9 +118,6 @@ import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import org.thingsboard.server.service.transport.TbCoreToTransportService;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.concurrent.ConcurrentHashMap;

45
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -43,7 +43,6 @@ import org.thingsboard.server.common.data.id.RpcId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKey;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
@ -74,8 +73,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceSessionsCacheEntry;
import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto;
import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType;
import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseNotificationProto;
import org.thingsboard.server.gen.transport.TransportProtos.SessionEvent;
import org.thingsboard.server.gen.transport.TransportProtos.SessionEventMsg;
@ -254,14 +251,12 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
private boolean isSendNewRpcAvailable() {
switch (rpcSubmitStrategy) {
case SEQUENTIAL_ON_ACK_FROM_DEVICE:
return toDeviceRpcPendingMap.values().stream().filter(md -> !md.isDelivered()).findAny().isEmpty();
case SEQUENTIAL_ON_RESPONSE_FROM_DEVICE:
return toDeviceRpcPendingMap.values().stream().filter(ToDeviceRpcRequestMetadata::isDelivered).findAny().isEmpty();
default:
return true;
}
return switch (rpcSubmitStrategy) {
case SEQUENTIAL_ON_ACK_FROM_DEVICE -> toDeviceRpcPendingMap.values().stream().filter(md -> !md.isDelivered()).findAny().isEmpty();
case SEQUENTIAL_ON_RESPONSE_FROM_DEVICE ->
toDeviceRpcPendingMap.values().stream().filter(ToDeviceRpcRequestMetadata::isDelivered).findAny().isEmpty();
default -> true;
};
}
private void createRpc(ToDeviceRpcRequest request, RpcStatus status) {
@ -927,34 +922,6 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
return systemContext.getEdgeEventService().saveAsync(edgeEvent);
}
private KeyValueProto toKeyValueProto(KvEntry kvEntry) {
KeyValueProto.Builder builder = KeyValueProto.newBuilder();
builder.setKey(kvEntry.getKey());
switch (kvEntry.getDataType()) {
case BOOLEAN:
builder.setType(KeyValueType.BOOLEAN_V);
builder.setBoolV(kvEntry.getBooleanValue().get());
break;
case DOUBLE:
builder.setType(KeyValueType.DOUBLE_V);
builder.setDoubleV(kvEntry.getDoubleValue().get());
break;
case LONG:
builder.setType(KeyValueType.LONG_V);
builder.setLongV(kvEntry.getLongValue().get());
break;
case STRING:
builder.setType(KeyValueType.STRING_V);
builder.setStringV(kvEntry.getStrValue().get());
break;
case JSON:
builder.setType(KeyValueType.JSON_V);
builder.setJsonV(kvEntry.getJsonValue().get());
break;
}
return builder.build();
}
void restoreSessions() {
if (systemContext.isLocalCacheType()) {
return;

12
application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java

@ -122,15 +122,9 @@ public abstract class AbstractRpcController extends BaseController {
logRpcCall(rpcRequest, rpcError, null);
RpcError error = rpcError.get();
switch (error) {
case TIMEOUT:
responseWriter.setResult(new ResponseEntity<>(timeoutStatus));
break;
case NO_ACTIVE_CONNECTION:
responseWriter.setResult(new ResponseEntity<>(noActiveConnectionStatus));
break;
default:
responseWriter.setResult(new ResponseEntity<>(timeoutStatus));
break;
case TIMEOUT -> responseWriter.setResult(new ResponseEntity<>(timeoutStatus));
case NO_ACTIVE_CONNECTION -> responseWriter.setResult(new ResponseEntity<>(noActiveConnectionStatus));
default -> responseWriter.setResult(new ResponseEntity<>(timeoutStatus));
}
} else {
Optional<String> responseData = response.getResponse();

17
application/src/main/java/org/thingsboard/server/controller/RuleChainController.java

@ -47,8 +47,8 @@ import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.event.EventType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
@ -80,6 +80,7 @@ import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
@ -340,18 +341,8 @@ public class RuleChainController extends BaseController {
RuleNodeId ruleNodeId = new RuleNodeId(toUUID(strRuleNodeId));
checkRuleNode(ruleNodeId, Operation.READ);
TenantId tenantId = getCurrentUser().getTenantId();
List<EventInfo> events = eventService.findLatestEvents(tenantId, ruleNodeId, EventType.DEBUG_RULE_NODE, 2);
JsonNode result = null;
if (events != null) {
for (EventInfo event : events) {
JsonNode body = event.getBody();
if (body.has("type") && body.get("type").asText().equals("IN")) {
result = body;
break;
}
}
}
return result;
return Optional.ofNullable(eventService.findLatestDebugRuleNodeInEvent(tenantId, ruleNodeId))
.map(EventInfo::getBody).orElse(null);
}
@ApiOperation(value = "Is TBEL script executor enabled",

10
application/src/main/java/org/thingsboard/server/controller/TelemetryController.java

@ -25,7 +25,6 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@ -36,7 +35,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
@ -49,9 +47,6 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.kv.AggregationParams;
import org.thingsboard.server.common.data.kv.IntervalType;
import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType;
@ -65,6 +60,7 @@ import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.AggregationParams;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery;
@ -74,6 +70,7 @@ import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.IntervalType;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
@ -379,7 +376,7 @@ public class TelemetryController extends BaseController {
public DeferredResult<ResponseEntity> saveEntityAttributesV1(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"})) @PathVariable("scope")AttributeScope scope,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"})) @PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveAttributes(getTenantId(), entityId, scope, request);
@ -918,4 +915,5 @@ public class TelemetryController extends BaseController {
}
return entry.getValue();
}
}

11
application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java

@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasName;
@ -127,20 +128,20 @@ public class EntityActionService {
} else {
entityNode = JacksonUtil.newObjectNode();
if (actionType == ActionType.ATTRIBUTES_UPDATED) {
String scope = extractParameter(String.class, 0, additionalInfo);
AttributeScope scope = extractParameter(AttributeScope.class, 0, additionalInfo);
@SuppressWarnings("unchecked")
List<AttributeKvEntry> attributes = extractParameter(List.class, 1, additionalInfo);
metaData.putValue(DataConstants.SCOPE, scope);
metaData.putValue(DataConstants.SCOPE, scope.name());
if (attributes != null) {
for (AttributeKvEntry attr : attributes) {
JacksonUtil.addKvEntry(entityNode, attr);
}
}
} else if (actionType == ActionType.ATTRIBUTES_DELETED) {
String scope = extractParameter(String.class, 0, additionalInfo);
AttributeScope scope = extractParameter(AttributeScope.class, 0, additionalInfo);
@SuppressWarnings("unchecked")
List<String> keys = extractParameter(List.class, 1, additionalInfo);
metaData.putValue(DataConstants.SCOPE, scope);
metaData.putValue(DataConstants.SCOPE, scope.name());
ArrayNode attrsArrayNode = entityNode.putArray("attributes");
if (keys != null) {
keys.forEach(attrsArrayNode::add);
@ -249,7 +250,7 @@ public class EntityActionService {
return result;
}
private void addTimeseries(ObjectNode entityNode, List<TsKvEntry> timeseries) throws Exception {
private void addTimeseries(ObjectNode entityNode, List<TsKvEntry> timeseries) {
if (timeseries != null && !timeseries.isEmpty()) {
ArrayNode result = entityNode.putArray("timeseries");
Map<Long, List<TsKvEntry>> groupedTelemetry = timeseries.stream()

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -557,8 +557,7 @@ public final class EdgeGrpcSession implements Closeable {
}
case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED ->
downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent);
default ->
log.warn("[{}][{}] Unsupported action type [{}]", this.tenantId, this.sessionId, edgeEvent.getAction());
default -> log.warn("[{}][{}] Unsupported action type [{}]", this.tenantId, this.sessionId, edgeEvent.getAction());
}
} catch (Exception e) {
log.error("[{}][{}] Exception during converting edge event to downlink msg", this.tenantId, this.sessionId, e);

16
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/BaseMsgConstructorFactory.java

@ -31,16 +31,10 @@ public abstract class BaseMsgConstructorFactory<T extends MsgConstructor, U exte
protected U v2Constructor;
public MsgConstructor getMsgConstructorByEdgeVersion(EdgeVersion edgeVersion) {
switch (edgeVersion) {
case V_3_3_0:
case V_3_3_3:
case V_3_4_0:
case V_3_6_0:
case V_3_6_1:
return v1Constructor;
case V_3_6_2:
default:
return v2Constructor;
}
return switch (edgeVersion) {
case V_3_3_0, V_3_3_3, V_3_4_0, V_3_6_0, V_3_6_1 -> v1Constructor;
default -> v2Constructor;
};
}
}

42
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java

@ -23,10 +23,13 @@ import com.google.common.util.concurrent.SettableFuture;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
@ -53,7 +56,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.common.transport.util.JsonUtils;
import org.thingsboard.server.dao.model.ModelConstants;
@ -66,8 +68,6 @@ import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@ -317,36 +317,22 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor {
JsonNode body) {
EntityId entityId;
switch (entityType) {
case DEVICE:
entityId = new DeviceId(entityUUID);
break;
case ASSET:
entityId = new AssetId(entityUUID);
break;
case ENTITY_VIEW:
entityId = new EntityViewId(entityUUID);
break;
case DASHBOARD:
entityId = new DashboardId(entityUUID);
break;
case TENANT:
entityId = TenantId.fromUUID(entityUUID);
break;
case CUSTOMER:
entityId = new CustomerId(entityUUID);
break;
case USER:
entityId = new UserId(entityUUID);
break;
case EDGE:
entityId = new EdgeId(entityUUID);
break;
default:
case DEVICE -> entityId = new DeviceId(entityUUID);
case ASSET -> entityId = new AssetId(entityUUID);
case ENTITY_VIEW -> entityId = new EntityViewId(entityUUID);
case DASHBOARD -> entityId = new DashboardId(entityUUID);
case TENANT -> entityId = TenantId.fromUUID(entityUUID);
case CUSTOMER -> entityId = new CustomerId(entityUUID);
case USER -> entityId = new UserId(entityUUID);
case EDGE -> entityId = new EdgeId(entityUUID);
default -> {
log.warn("[{}] Unsupported edge event type [{}]", tenantId, entityType);
return null;
}
}
String bodyJackson = JacksonUtil.toString(body);
return bodyJackson == null ? null :
entityDataMsgConstructor.constructEntityDataMsg(tenantId, entityId, actionType, JsonParser.parseString(bodyJackson));
}
}

89
application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java

@ -69,80 +69,71 @@ public class EntityStateSourcingListener {
@TransactionalEventListener(fallbackExecution = true)
public void handleEvent(SaveEntityEvent<?> event) {
log.trace("[{}] SaveEntityEvent called: {}", event.getTenantId(), event);
TenantId tenantId = event.getTenantId();
EntityId entityId = event.getEntityId();
EntityType entityType = entityId.getEntityType();
log.debug("[{}][{}][{}] Handling entity save event: {}", tenantId, entityType, entityId, event);
boolean isCreated = event.getCreated() != null && event.getCreated();
ComponentLifecycleEvent lifecycleEvent = isCreated ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED;
switch (entityType) {
case ASSET:
case ASSET_PROFILE:
case ENTITY_VIEW:
case NOTIFICATION_RULE:
case ASSET, ASSET_PROFILE, ENTITY_VIEW, NOTIFICATION_RULE -> {
tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, lifecycleEvent);
break;
case RULE_CHAIN:
}
case RULE_CHAIN -> {
RuleChain ruleChain = (RuleChain) event.getEntity();
if (RuleChainType.CORE.equals(ruleChain.getType())) {
tbClusterService.broadcastEntityStateChangeEvent(ruleChain.getTenantId(), ruleChain.getId(), lifecycleEvent);
}
break;
case TENANT:
}
case TENANT -> {
Tenant tenant = (Tenant) event.getEntity();
onTenantUpdate(tenant, lifecycleEvent);
break;
case TENANT_PROFILE:
}
case TENANT_PROFILE -> {
TenantProfile tenantProfile = (TenantProfile) event.getEntity();
onTenantProfileUpdate(tenantProfile, lifecycleEvent);
break;
case DEVICE:
}
case DEVICE -> {
onDeviceUpdate(event.getEntity(), event.getOldEntity());
break;
case DEVICE_PROFILE:
}
case DEVICE_PROFILE -> {
DeviceProfile deviceProfile = (DeviceProfile) event.getEntity();
onDeviceProfileUpdate(deviceProfile, event.getOldEntity(), isCreated);
break;
case EDGE:
}
case EDGE -> {
handleEdgeEvent(tenantId, entityId, event.getEntity(), lifecycleEvent);
break;
case TB_RESOURCE:
}
case TB_RESOURCE -> {
TbResource tbResource = (TbResource) event.getEntity();
tbClusterService.onResourceChange(tbResource, null);
break;
case API_USAGE_STATE:
}
case API_USAGE_STATE -> {
ApiUsageState apiUsageState = (ApiUsageState) event.getEntity();
tbClusterService.onApiStateChange(apiUsageState, null);
break;
default:
break;
}
default -> {}
}
}
@TransactionalEventListener(fallbackExecution = true)
public void handleEvent(DeleteEntityEvent<?> event) {
log.trace("[{}] DeleteEntityEvent called: {}", event.getTenantId(), event);
TenantId tenantId = event.getTenantId();
EntityId entityId = event.getEntityId();
EntityType entityType = entityId.getEntityType();
log.debug("[{}][{}][{}] Handling entity deletion event: {}", tenantId, entityType, entityId, event);
switch (entityType) {
case ASSET:
case ASSET_PROFILE:
case ENTITY_VIEW:
case CUSTOMER:
case EDGE:
case NOTIFICATION_RULE:
case ASSET, ASSET_PROFILE, ENTITY_VIEW, CUSTOMER, EDGE, NOTIFICATION_RULE -> {
tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, ComponentLifecycleEvent.DELETED);
break;
case NOTIFICATION_REQUEST:
}
case NOTIFICATION_REQUEST -> {
NotificationRequest request = (NotificationRequest) event.getEntity();
if (request.isScheduled()) {
tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, ComponentLifecycleEvent.DELETED);
}
break;
case RULE_CHAIN:
}
case RULE_CHAIN -> {
RuleChain ruleChain = (RuleChain) event.getEntity();
if (RuleChainType.CORE.equals(ruleChain.getType())) {
Set<RuleChainId> referencingRuleChainIds = JacksonUtil.fromString(event.getBody(), new TypeReference<>() {});
@ -152,29 +143,28 @@ public class EntityStateSourcingListener {
}
tbClusterService.broadcastEntityStateChangeEvent(tenantId, ruleChain.getId(), ComponentLifecycleEvent.DELETED);
}
break;
case TENANT:
}
case TENANT -> {
Tenant tenant = (Tenant) event.getEntity();
onTenantDeleted(tenant);
break;
case TENANT_PROFILE:
}
case TENANT_PROFILE -> {
TenantProfile tenantProfile = (TenantProfile) event.getEntity();
tbClusterService.onTenantProfileDelete(tenantProfile, null);
break;
case DEVICE:
}
case DEVICE -> {
Device device = (Device) event.getEntity();
tbClusterService.onDeviceDeleted(tenantId, device, null);
break;
case DEVICE_PROFILE:
}
case DEVICE_PROFILE -> {
DeviceProfile deviceProfile = (DeviceProfile) event.getEntity();
onDeviceProfileDelete(event.getTenantId(), event.getEntityId(), deviceProfile);
break;
case TB_RESOURCE:
}
case TB_RESOURCE -> {
TbResourceInfo tbResource = (TbResourceInfo) event.getEntity();
tbClusterService.onResourceDeleted(tbResource, null);
break;
default:
break;
}
default -> {}
}
}
@ -186,8 +176,7 @@ public class EntityStateSourcingListener {
&& event.getEntity() instanceof DeviceCredentials) {
tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(event.getTenantId(),
(DeviceId) event.getEntityId(), (DeviceCredentials) event.getEntity()), null);
} else if (ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType()) && event.getEntity() instanceof Device) {
Device device = (Device) event.getEntity();
} else if (ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType()) && event.getEntity() instanceof Device device) {
Tenant tenant = JacksonUtil.fromString(event.getBody(), Tenant.class);
if (tenant != null) {
tbClusterService.onDeviceAssignedToTenant(tenant.getId(), device);

18
application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java

@ -16,12 +16,10 @@
package org.thingsboard.server.service.entitiy.tenant;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
@ -45,25 +43,19 @@ public class DefaultTbTenantService extends AbstractTbEntityService implements T
private final TbQueueService tbQueueService;
private final TenantProfileService tenantProfileService;
private final EntitiesVersionControlService versionControlService;
private final ApplicationEventPublisher eventPublisher;
@Override
public Tenant save(Tenant tenant) throws Exception {
boolean created = tenant.getId() == null;
Tenant oldTenant = !created ? tenantService.findTenantById(tenant.getId()) : null;
Tenant savedTenant = checkNotNull(tenantService.saveTenant(tenant, !created));
if (created) {
installScripts.createDefaultRuleChains(savedTenant.getId());
installScripts.createDefaultEdgeRuleChains(savedTenant.getId());
installScripts.createDefaultTenantDashboards(savedTenant.getId(), null);
}
Tenant savedTenant = tenantService.saveTenant(tenant, tenantId -> {
installScripts.createDefaultRuleChains(tenantId);
installScripts.createDefaultEdgeRuleChains(tenantId);
installScripts.createDefaultTenantDashboards(tenantId, null);
});
tenantProfileCache.evict(savedTenant.getId());
if (created) {
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(TenantId.SYS_TENANT_ID).entityId(savedTenant.getId()).entity(savedTenant).created(true).build());
}
TenantProfile oldTenantProfile = oldTenant != null ? tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, oldTenant.getTenantProfileId()) : null;
TenantProfile newTenantProfile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, savedTenant.getTenantProfileId());
tbQueueService.updateQueuesByTenants(Collections.singletonList(savedTenant.getTenantId()), newTenantProfile, oldTenantProfile);

16
application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

@ -151,17 +151,18 @@ public class InstallScripts {
}
}
public void createDefaultRuleChains(TenantId tenantId) throws IOException {
public void createDefaultRuleChains(TenantId tenantId) {
Path tenantChainsDir = getTenantRuleChainsDir();
loadRuleChainsFromPath(tenantId, tenantChainsDir);
}
public void createDefaultEdgeRuleChains(TenantId tenantId) throws IOException {
public void createDefaultEdgeRuleChains(TenantId tenantId) {
Path edgeChainsDir = getEdgeRuleChainsDir();
loadRuleChainsFromPath(tenantId, edgeChainsDir);
}
private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) throws IOException {
@SneakyThrows
private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) {
findRuleChainsFromPath(ruleChainsPath).forEach(path -> {
try {
createRuleChainFromFile(tenantId, path, null);
@ -329,17 +330,18 @@ public class InstallScripts {
}
}
public void loadDashboards(TenantId tenantId, CustomerId customerId) throws Exception {
public void loadDashboards(TenantId tenantId, CustomerId customerId) {
Path dashboardsDir = Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR);
loadDashboardsFromDir(tenantId, customerId, dashboardsDir);
}
public void createDefaultTenantDashboards(TenantId tenantId, CustomerId customerId) throws Exception {
public void createDefaultTenantDashboards(TenantId tenantId, CustomerId customerId) {
Path dashboardsDir = Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DASHBOARDS_DIR);
loadDashboardsFromDir(tenantId, customerId, dashboardsDir);
}
private void loadDashboardsFromDir(TenantId tenantId, CustomerId customerId, Path dashboardsDir) throws IOException {
@SneakyThrows
private void loadDashboardsFromDir(TenantId tenantId, CustomerId customerId, Path dashboardsDir) {
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dashboardsDir, path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach(
path -> {
@ -414,7 +416,7 @@ public class InstallScripts {
}
);
} catch (Exception e) {
log.error("Unable to load resources lwm2m object model from file: [{}]", resourceLwm2mPath.toString());
log.error("Unable to load resources lwm2m object model from file: [{}]", resourceLwm2mPath);
throw new RuntimeException("resource lwm2m object model from file", e);
}
}

62
application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java

@ -208,52 +208,22 @@ public class AccessValidator {
public void validate(SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback<ValidationResult> callback) {
switch (entityId.getEntityType()) {
case DEVICE:
validateDevice(currentUser, operation, entityId, callback);
return;
case DEVICE_PROFILE:
validateDeviceProfile(currentUser, operation, entityId, callback);
return;
case ASSET:
validateAsset(currentUser, operation, entityId, callback);
return;
case ASSET_PROFILE:
validateAssetProfile(currentUser, operation, entityId, callback);
return;
case RULE_CHAIN:
validateRuleChain(currentUser, operation, entityId, callback);
return;
case CUSTOMER:
validateCustomer(currentUser, operation, entityId, callback);
return;
case TENANT:
validateTenant(currentUser, operation, entityId, callback);
return;
case TENANT_PROFILE:
validateTenantProfile(currentUser, operation, entityId, callback);
return;
case USER:
validateUser(currentUser, operation, entityId, callback);
return;
case ENTITY_VIEW:
validateEntityView(currentUser, operation, entityId, callback);
return;
case EDGE:
validateEdge(currentUser, operation, entityId, callback);
return;
case API_USAGE_STATE:
validateApiUsageState(currentUser, operation, entityId, callback);
return;
case TB_RESOURCE:
validateResource(currentUser, operation, entityId, callback);
return;
case OTA_PACKAGE:
validateOtaPackage(currentUser, operation, entityId, callback);
return;
case RPC:
validateRpc(currentUser, operation, entityId, callback);
return;
default:
case DEVICE -> validateDevice(currentUser, operation, entityId, callback);
case DEVICE_PROFILE -> validateDeviceProfile(currentUser, operation, entityId, callback);
case ASSET -> validateAsset(currentUser, operation, entityId, callback);
case ASSET_PROFILE -> validateAssetProfile(currentUser, operation, entityId, callback);
case RULE_CHAIN -> validateRuleChain(currentUser, operation, entityId, callback);
case CUSTOMER -> validateCustomer(currentUser, operation, entityId, callback);
case TENANT -> validateTenant(currentUser, operation, entityId, callback);
case TENANT_PROFILE -> validateTenantProfile(currentUser, operation, entityId, callback);
case USER -> validateUser(currentUser, operation, entityId, callback);
case ENTITY_VIEW -> validateEntityView(currentUser, operation, entityId, callback);
case EDGE -> validateEdge(currentUser, operation, entityId, callback);
case API_USAGE_STATE -> validateApiUsageState(currentUser, operation, entityId, callback);
case TB_RESOURCE -> validateResource(currentUser, operation, entityId, callback);
case OTA_PACKAGE -> validateOtaPackage(currentUser, operation, entityId, callback);
case RPC -> validateRpc(currentUser, operation, entityId, callback);
default ->
//TODO: add support of other entities
throw new IllegalStateException("Not Implemented!");
}

23
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java

@ -236,14 +236,14 @@ public abstract class AbstractBulkImportService<E extends HasId<? extends Entity
@Override
public void onSuccess(Void unused) {
entityActionService.logEntityAction(user, (UUIDBased & EntityId) entityId, null,
null, ActionType.ATTRIBUTES_UPDATED, null, scope, attributes);
null, ActionType.ATTRIBUTES_UPDATED, null, AttributeScope.valueOf(scope), attributes);
}
@Override
public void onFailure(Throwable throwable) {
entityActionService.logEntityAction(user, (UUIDBased & EntityId) entityId, null,
null, ActionType.ATTRIBUTES_UPDATED, BaseController.toException(throwable),
scope, attributes);
AttributeScope.valueOf(scope), attributes);
throw new RuntimeException(throwable);
}
@ -301,18 +301,13 @@ public abstract class AbstractBulkImportService<E extends HasId<? extends Entity
private final DataType dataType;
public JsonPrimitive toJsonPrimitive() {
switch (dataType) {
case STRING:
return new JsonPrimitive((String) value);
case LONG:
return new JsonPrimitive((Long) value);
case DOUBLE:
return new JsonPrimitive((Double) value);
case BOOLEAN:
return new JsonPrimitive((Boolean) value);
default:
return null;
}
return switch (dataType) {
case STRING -> new JsonPrimitive((String) value);
case LONG -> new JsonPrimitive((Long) value);
case DOUBLE -> new JsonPrimitive((Double) value);
case BOOLEAN -> new JsonPrimitive((Boolean) value);
default -> null;
};
}
public String stringValue() {

16
application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java

@ -663,7 +663,7 @@ public class TenantControllerTest extends AbstractControllerTest {
savedDifferentTenant.setTenantProfileId(tenantProfile.getId());
savedDifferentTenant = doPost("/api/tenant", savedDifferentTenant, Tenant.class);
TenantId tenantId = differentTenantId;
await().atMost(10, TimeUnit.SECONDS)
await().atMost(30, TimeUnit.SECONDS)
.until(() -> {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId);
return !tpi.getTenantId().get().isSysTenantId();
@ -677,7 +677,7 @@ public class TenantControllerTest extends AbstractControllerTest {
tenantProfile.setIsolatedTbRuleEngine(false);
tenantProfile.getProfileData().setQueueConfiguration(Collections.emptyList());
tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
await().atMost(10, TimeUnit.SECONDS)
await().atMost(30, TimeUnit.SECONDS)
.until(() -> partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId)
.getTenantId().get().isSysTenantId());
@ -689,11 +689,11 @@ public class TenantControllerTest extends AbstractControllerTest {
submittedMsgs.add(tbMsg.getId());
Thread.sleep(timeLeft / msgs);
}
await().atMost(15, TimeUnit.SECONDS).untilAsserted(() -> {
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
verify(queueAdmin, times(1)).deleteTopic(eq(isolatedTopic));
});
await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
for (UUID msgId : submittedMsgs) {
verify(actorContext).tell(argThat(msg -> {
return msg instanceof QueueToRuleEngineMsg && ((QueueToRuleEngineMsg) msg).getMsg().getId().equals(msgId);
@ -718,13 +718,13 @@ public class TenantControllerTest extends AbstractControllerTest {
savedDifferentTenant.setTenantProfileId(tenantProfile.getId());
savedDifferentTenant = doPost("/api/tenant", savedDifferentTenant, Tenant.class);
TenantId tenantId = differentTenantId;
await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(partitionService.getMyPartitions(new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId))).isNotNull();
});
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tenantId);
assertThat(tpi.getTenantId()).hasValue(tenantId);
TbMsg tbMsg = publishTbMsg(tenantId, tpi);
await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
verify(actorContext).tell(argThat(msg -> {
return msg instanceof QueueToRuleEngineMsg && ((QueueToRuleEngineMsg) msg).getMsg().getId().equals(tbMsg.getId());
}));
@ -732,7 +732,7 @@ public class TenantControllerTest extends AbstractControllerTest {
deleteDifferentTenant();
await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(partitionService.getMyPartitions(new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId))).isNull();
assertThatThrownBy(() -> partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tenantId))
.isInstanceOf(TenantNotFoundException.class);
@ -752,7 +752,7 @@ public class TenantControllerTest extends AbstractControllerTest {
}
private void verifyUsedQueueAndMessage(String queue, TenantId tenantId, EntityId entityId, String msgType, Runnable action, Consumer<TopicPartitionInfo> tpiAssert) {
await().atMost(15, TimeUnit.SECONDS)
await().atMost(30, TimeUnit.SECONDS)
.untilAsserted(() -> {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, queue, tenantId, entityId);
tpiAssert.accept(tpi);

5
application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java

@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.server.resources.Resource;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.coapserver.DefaultCoapServerService;
@ -60,15 +59,11 @@ public class CoapAttributesUpdatesIntegrationTest extends AbstractCoapAttributes
processAfterTest();
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception {
processJsonTestSubscribeToAttributesUpdates(false);
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testSubscribeToAttributesUpdatesFromTheServerWithEmptyCurrentStateNotification() throws Exception {
processJsonTestSubscribeToAttributesUpdates(true);

4
application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesJsonIntegrationTest.java

@ -18,7 +18,6 @@ package org.thingsboard.server.transport.coap.attributes.updates;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.thingsboard.server.common.data.CoapDeviceType;
import org.thingsboard.server.common.data.TransportPayloadType;
@ -45,12 +44,11 @@ public class CoapAttributesUpdatesJsonIntegrationTest extends AbstractCoapAttrib
processAfterTest();
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception {
processJsonTestSubscribeToAttributesUpdates(false);
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testSubscribeToAttributesUpdatesFromTheServerWithEmptyCurrentStateNotification() throws Exception {
processJsonTestSubscribeToAttributesUpdates(true);

3
application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesProtoIntegrationTest.java

@ -18,7 +18,6 @@ package org.thingsboard.server.transport.coap.attributes.updates;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.thingsboard.server.common.data.CoapDeviceType;
import org.thingsboard.server.common.data.TransportPayloadType;
@ -44,12 +43,10 @@ public class CoapAttributesUpdatesProtoIntegrationTest extends AbstractCoapAttri
public void afterTest() throws Exception {
processAfterTest();
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception {
processProtoTestSubscribeToAttributesUpdates(false);
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testSubscribeToAttributesUpdatesFromTheServerWithEmptyCurrentStateNotification() throws Exception {
processProtoTestSubscribeToAttributesUpdates(true);

4
application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java

@ -26,7 +26,6 @@ import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.DeviceId;
@ -69,7 +68,6 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest {
private static final List<String> EXPECTED_KEYS = Arrays.asList("key1", "key2", "key3", "key4", "key5");
private static final String DEVICE_RESPONSE = "{\"value1\":\"A\",\"value2\":\"B\"}";
@Before
public void beforeTest() throws Exception {
CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder()
@ -83,7 +81,6 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest {
processAfterTest();
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testConfirmableRequests() throws Exception {
boolean confirmable = true;
@ -92,7 +89,6 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest {
processTestRequestAttributesValuesFromTheServer(confirmable);
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testNonConfirmableRequests() throws Exception {
boolean confirmable = false;

3
application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcDefaultIntegrationTest.java

@ -20,7 +20,6 @@ import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.security.AccessValidator;
@ -83,13 +82,11 @@ public class CoapServerSideRpcDefaultIntegrationTest extends AbstractCoapServerS
Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testServerCoapOneWayRpc() throws Exception {
processOneWayRpcTest(false);
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testServerCoapTwoWayRpc() throws Exception {
processTwoWayRpcTest("{\"value1\":\"A\",\"value2\":\"B\"}", false);

3
application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcJsonIntegrationTest.java

@ -18,7 +18,6 @@ package org.thingsboard.server.transport.coap.rpc;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.thingsboard.server.common.data.CoapDeviceType;
import org.thingsboard.server.common.data.TransportPayloadType;
@ -44,13 +43,11 @@ public class CoapServerSideRpcJsonIntegrationTest extends AbstractCoapServerSide
processAfterTest();
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testServerCoapOneWayRpc() throws Exception {
processOneWayRpcTest(false);
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testServerCoapTwoWayRpc() throws Exception {
processTwoWayRpcTest("{\"value1\":\"A\",\"value2\":\"B\"}", false);

3
application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcProtoIntegrationTest.java

@ -18,7 +18,6 @@ package org.thingsboard.server.transport.coap.rpc;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.thingsboard.server.common.data.CoapDeviceType;
import org.thingsboard.server.common.data.TransportPayloadType;
@ -45,13 +44,11 @@ public class CoapServerSideRpcProtoIntegrationTest extends AbstractCoapServerSid
processAfterTest();
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testServerCoapOneWayRpc() throws Exception {
processOneWayRpcTest(true);
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testServerCoapTwoWayRpc() throws Exception {
processTwoWayRpcTest("{\"payload\":\"{\\\"value1\\\":\\\"A\\\",\\\"value2\\\":\\\"B\\\"}\"}", true);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java

@ -35,6 +35,8 @@ public interface EventService {
List<EventInfo> findLatestEvents(TenantId tenantId, EntityId entityId, EventType eventType, int limit);
EventInfo findLatestDebugRuleNodeInEvent(TenantId tenantId, EntityId entityId);
PageData<EventInfo> findEventsByFilter(TenantId tenantId, EntityId entityId, EventFilter eventFilter, TimePageLink pageLink);
void removeEvents(TenantId tenantId, EntityId entityId);

3
common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.List;
import java.util.function.Consumer;
public interface TenantService extends EntityDaoService {
@ -36,7 +37,7 @@ public interface TenantService extends EntityDaoService {
Tenant saveTenant(Tenant tenant);
Tenant saveTenant(Tenant tenant, boolean publishSaveEvent);
Tenant saveTenant(Tenant tenant, Consumer<TenantId> defaultEntitiesCreator);
boolean tenantExists(TenantId tenantId);

3
common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java

@ -111,7 +111,7 @@ public class EdgeGrpcClient implements EdgeRpcClient {
.setConnectRequestMsg(ConnectRequestMsg.newBuilder()
.setEdgeRoutingKey(edgeKey)
.setEdgeSecret(edgeSecret)
.setEdgeVersion(EdgeVersion.V_3_6_2)
.setEdgeVersion(EdgeVersion.V_3_7_0)
.setMaxInboundMessageSize(maxInboundMessageSize)
.build())
.build());
@ -248,4 +248,5 @@ public class EdgeGrpcClient implements EdgeRpcClient {
uplinkMsgLock.unlock();
}
}
}

20
common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java

@ -258,31 +258,31 @@ public class ProtoUtils {
.setLastUpdateTs(attributeKvEntry.getLastUpdateTs())
.setKey(attributeKvEntry.getKey());
switch (attributeKvEntry.getDataType()) {
case BOOLEAN:
case BOOLEAN -> {
attributeKvEntry.getBooleanValue().ifPresent(attributeValueBuilder::setBoolV);
attributeValueBuilder.setHasV(attributeKvEntry.getBooleanValue().isPresent());
attributeValueBuilder.setType(TransportProtos.KeyValueType.BOOLEAN_V);
break;
case STRING:
}
case STRING -> {
attributeKvEntry.getStrValue().ifPresent(attributeValueBuilder::setStringV);
attributeValueBuilder.setHasV(attributeKvEntry.getStrValue().isPresent());
attributeValueBuilder.setType(TransportProtos.KeyValueType.STRING_V);
break;
case DOUBLE:
}
case DOUBLE -> {
attributeKvEntry.getDoubleValue().ifPresent(attributeValueBuilder::setDoubleV);
attributeValueBuilder.setHasV(attributeKvEntry.getDoubleValue().isPresent());
attributeValueBuilder.setType(TransportProtos.KeyValueType.DOUBLE_V);
break;
case LONG:
}
case LONG -> {
attributeKvEntry.getLongValue().ifPresent(attributeValueBuilder::setLongV);
attributeValueBuilder.setHasV(attributeKvEntry.getLongValue().isPresent());
attributeValueBuilder.setType(TransportProtos.KeyValueType.LONG_V);
break;
case JSON:
}
case JSON -> {
attributeKvEntry.getJsonValue().ifPresent(attributeValueBuilder::setJsonV);
attributeValueBuilder.setHasV(attributeKvEntry.getJsonValue().isPresent());
attributeValueBuilder.setType(TransportProtos.KeyValueType.JSON_V);
break;
}
}
builder.addValues(attributeValueBuilder.build());
}

22
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java

@ -229,7 +229,7 @@ public class LwM2mClient {
this.resources.get(pathRezIdVer).updateLwM2mResource(resource, mode);
return true;
} else {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRezIdVer));
LwM2mPath pathIds = getLwM2mPathFromString(pathRezIdVer);
ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId());
if (resourceModel != null) {
this.resources.put(pathRezIdVer, new ResourceValue(resource, resourceModel));
@ -257,7 +257,7 @@ public class LwM2mClient {
}
public String getRezIdByResourceNameAndObjectInstanceId(String resourceName, String pathObjectInstanceIdVer, LwM2mModelProvider modelProvider) {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathObjectInstanceIdVer));
LwM2mPath pathIds = getLwM2mPathFromString(pathObjectInstanceIdVer);
if (pathIds.isObjectInstance()) {
Set<Integer> rezIds = modelProvider.getObjectModel(registration)
.getObjectModel(pathIds.getObjectId()).resources.entrySet()
@ -271,7 +271,7 @@ public class LwM2mClient {
}
public ResourceModel getResourceModel(String pathIdVer, LwM2mModelProvider modelProvider) {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer));
LwM2mPath pathIds = getLwM2mPathFromString(pathIdVer);
String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId()));
String verRez = getVerFromPathIdVerOrId(pathIdVer);
return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration)
@ -289,7 +289,7 @@ public class LwM2mClient {
public ObjectModel getObjectModel(String pathIdVer, LwM2mModelProvider modelProvider) {
try {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer));
LwM2mPath pathIds = getLwM2mPathFromString(pathIdVer);
String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId()));
String verRez = getVerFromPathIdVerOrId(pathIdVer);
return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration)
@ -309,7 +309,7 @@ public class LwM2mClient {
public Collection<LwM2mResource> getNewResourceForInstance(String pathRezIdVer, Object params, LwM2mModelProvider modelProvider,
LwM2mValueConverter converter) {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRezIdVer));
LwM2mPath pathIds = getLwM2mPathFromString(pathRezIdVer);
Collection<LwM2mResource> resources = ConcurrentHashMap.newKeySet();
Map<Integer, ResourceModel> resourceModels = modelProvider.getObjectModel(registration)
.getObjectModel(pathIds.getObjectId()).resources;
@ -329,7 +329,7 @@ public class LwM2mClient {
*/
public Collection<LwM2mResource> getNewResourcesForInstance(String pathRezIdVer, Object params, LwM2mModelProvider modelProvider,
LwM2mValueConverter converter) {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRezIdVer));
LwM2mPath pathIds = getLwM2mPathFromString(pathRezIdVer);
Collection<LwM2mResource> resources = ConcurrentHashMap.newKeySet();
Map<Integer, ResourceModel> resourceModels = modelProvider.getObjectModel(registration)
.getObjectModel(pathIds.getObjectId()).resources;
@ -370,7 +370,7 @@ public class LwM2mClient {
}
public String isValidObjectVersion(String path) {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(path));
LwM2mPath pathIds = getLwM2mPathFromString(path);
LwM2m.Version verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId());
if (verSupportedObject == null) {
return String.format("Specified object id %s absent in the list supported objects of the client or is security object!", pathIds.getObjectId());
@ -390,7 +390,7 @@ public class LwM2mClient {
public void deleteResources(String pathIdVer, LwM2mModelProvider modelProvider) {
Set<String> key = getKeysEqualsIdVer(pathIdVer);
key.forEach(pathRez -> {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRez));
LwM2mPath pathIds = getLwM2mPathFromString(pathRez);
ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId());
if (resourceModel != null) {
this.resources.get(pathRez).setResourceModel(resourceModel);
@ -410,7 +410,7 @@ public class LwM2mClient {
}
private void saveResourceModel(String pathRez, LwM2mModelProvider modelProvider) {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRez));
LwM2mPath pathIds = getLwM2mPathFromString(pathRez);
ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId());
this.resources.get(pathRez).setResourceModel(resourceModel);
}
@ -456,5 +456,9 @@ public class LwM2mClient {
return result;
}
public LwM2mPath getLwM2mPathFromString(String path) {
return new LwM2mPath(fromVersionedIdToObjectId(path));
}
}

287
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java

@ -15,8 +15,12 @@
*/
package org.thingsboard.server.transport.lwm2m.server.store.util;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.protobuf.util.JsonFormat;
import lombok.SneakyThrows;
import org.eclipse.leshan.core.model.ResourceModel;
@ -25,23 +29,35 @@ import org.eclipse.leshan.core.node.LwM2mNodeException;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.LwM2mSingleResource;
import org.eclipse.leshan.core.node.ObjectLink;
import org.eclipse.leshan.core.util.datatype.ULong;
import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue;
import java.lang.reflect.Field;
import java.time.Instant;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import static org.thingsboard.common.util.JacksonUtil.toJsonNode;
public class LwM2MClientSerDes {
public static final String VALUE = "value";
private static final RegistrationSerDes registrationSerDes = new RegistrationSerDes();
@SneakyThrows
public static byte[] serialize(LwM2mClient client) {
JsonObject o = new JsonObject();
o.addProperty("nodeId", "client.getNodeId()");
o.addProperty("nodeId", client.getNodeId());
o.addProperty("endpoint", client.getEndpoint());
JsonObject resources = new JsonObject();
@ -92,8 +108,15 @@ public class LwM2MClientSerDes {
o.addProperty("pagingTransmissionWindow", client.getPagingTransmissionWindow());
}
if (client.getRegistration() != null) {
RegistrationSerDes regDez = new RegistrationSerDes();
o.addProperty("registration", regDez.jSerialize(client.getRegistration()).toString());
String registrationAddress = client.getRegistration().getAddress().toString();
JsonNode registrationNode = registrationSerDes.jSerialize(client.getRegistration());
if (!registrationAddress.equals(registrationNode.get("transportdata").get("address").asText())){
ObjectNode actualRegAddress = (ObjectNode)registrationNode.get("transportdata");
actualRegAddress.put("address", registrationAddress);
ObjectNode actualIdentity = (ObjectNode) actualRegAddress.get("identity");
actualIdentity.put("address", registrationAddress);
}
o.addProperty("registration", registrationNode.toString());
}
o.addProperty("asleep", client.isAsleep());
o.addProperty("lastUplinkTime", client.getLastUplinkTime());
@ -138,33 +161,35 @@ public class LwM2MClientSerDes {
ResourceModel.Type type = ResourceModel.Type.valueOf(o.get("type").getAsString());
if (multiInstances) {
Map<Integer, Object> instances = new HashMap<>();
o.get("instances").getAsJsonArray().forEach(entry -> {
// instances.put(Integer.valueOf(entry.getAsJsonObject().), parseValue(type, entry.getValue()));
});
for (Entry<String, JsonElement> entry : o.get("instances").getAsJsonObject().entrySet()) {
JsonObject instance = entry.getValue().getAsJsonObject();
instances.put(Integer.valueOf(instance.get("id").getAsString()), parseValue(type, instance.get(VALUE)));
}
return LwM2mMultipleResource.newResource(id, instances, type);
} else {
return LwM2mSingleResource.newResource(id, parseValue(type, (JsonValue) o.get(VALUE)));
return LwM2mSingleResource.newResource(id, parseValue(type, o.get(VALUE)));
}
}
private static Object parseValue(ResourceModel.Type type, JsonValue value) {
private static Object parseValue(ResourceModel.Type type, JsonElement value) {
switch (type) {
case INTEGER:
return value.value();
// case FLOAT:
// return value.value();
// case BOOLEAN:
// return value.asBoolean();
// case OPAQUE:
// return Base64.getDecoder().decode(value.asString());
// case STRING:
// return value.asString();
// case TIME:
// return new Date(value.asLong());
// case OBJLNK:
// return ObjectLink.decodeFromString(value.asString());
// case UNSIGNED_INTEGER:
// return ULong.valueOf(value.asString());
return value.getAsInt();
case FLOAT:
return value.getAsDouble();
case BOOLEAN:
return value.getAsBoolean();
case OPAQUE:
return Base64.getDecoder().decode(value.getAsString());
case STRING:
return value.getAsString();
case TIME:
return Instant.ofEpochMilli(value.getAsLong());
case OBJLNK:
return ObjectLink.decodeFromString(value.getAsString());
case UNSIGNED_INTEGER:
return ULong.valueOf(value.getAsString());
default:
throw new LwM2mNodeException(String.format("Type %s is not supported", type.name()));
}
@ -212,7 +237,7 @@ public class LwM2MClientSerDes {
o.addProperty(VALUE, Base64.getEncoder().encodeToString((byte[]) value));
break;
case STRING:
o.addProperty(VALUE, (String) value);
o.addProperty(VALUE, String.valueOf(value));
break;
case TIME:
o.addProperty(VALUE, ((Date) value).getTime());
@ -221,7 +246,7 @@ public class LwM2MClientSerDes {
o.addProperty(VALUE, ((ObjectLink) value).encodeToString());
break;
case UNSIGNED_INTEGER:
o.addProperty(VALUE, value.toString());
o.addProperty(VALUE, Integer.toUnsignedString((int)value));
break;
default:
throw new LwM2mNodeException(String.format("Type %s is not supported", type.name()));
@ -230,111 +255,111 @@ public class LwM2MClientSerDes {
@SneakyThrows
public static LwM2mClient deserialize(byte[] data) {
// JsonObject o = new JsonObject(new String(data)));
// LwM2mClient lwM2mClient = new LwM2mClient(o.get("nodeId").getAsString(), o.get("endpoint").getAsString());
LwM2mClient lwM2mClient = new LwM2mClient("nodeId", "endpoint");
// o.get("resources").getAsJsonObject().forEach(entry -> {
// JsonObject resource = entry.getValue().asObject();
// LwM2mResource lwM2mResource = parseLwM2mResource(resource.get("lwM2mResource").getAsJsonObject());
// ResourceModel resourceModel = parseResourceModel(resource.get("resourceModel").asObject());
// ResourceValue resourceValue = new ResourceValue(lwM2mResource, resourceModel);
// lwM2mClient.getResources().put(entry.getName(), resourceValue);
// });
//
// for (JsonObject.Member entry : o.get("sharedAttributes").asObject()) {
// TransportProtos.TsKvProto.Builder builder = TransportProtos.TsKvProto.newBuilder();
// JsonFormat.parser().merge(entry.getValue().getAsString(), builder);
// lwM2mClient.getSharedAttributes().put(entry.getName(), builder.build());
// }
//
// o.get("keyTsLatestMap").asObject().forEach(entry -> {
// lwM2mClient.getKeyTsLatestMap().put(entry.getName(), new AtomicLong(entry.getValue().asLong()));
// });
//
// lwM2mClient.setState(LwM2MClientState.valueOf(o.get("state").getAsString()));
//
// Class<LwM2mClient> lwM2mClientClass = LwM2mClient.class;
//
// JsonValue session = o.get("session");
// if (session != null) {
// TransportProtos.SessionInfoProto.Builder builder = TransportProtos.SessionInfoProto.newBuilder();
// JsonFormat.parser().merge(session.asString(), builder);
//
// Field sessionField = lwM2mClientClass.getDeclaredField("session");
// sessionField.setAccessible(true);
// sessionField.set(lwM2mClient, builder.build());
// }
//
// JsonValue tenantId = o.get("tenantId");
// if (tenantId != null) {
// Field tenantIdField = lwM2mClientClass.getDeclaredField("tenantId");
// tenantIdField.setAccessible(true);
// tenantIdField.set(lwM2mClient, new TenantId(UUID.fromString(tenantId.asString())));
// }
//
// JsonValue deviceId = o.get("deviceId");
// if (tenantId != null) {
// Field deviceIdField = lwM2mClientClass.getDeclaredField("deviceId");
// deviceIdField.setAccessible(true);
// deviceIdField.set(lwM2mClient, UUID.fromString(deviceId.asString()));
// }
//
// JsonValue profileId = o.get("profileId");
// if (tenantId != null) {
// Field profileIdField = lwM2mClientClass.getDeclaredField("profileId");
// profileIdField.setAccessible(true);
// profileIdField.set(lwM2mClient, UUID.fromString(profileId.asString()));
// }
//
// JsonValue powerMode = o.get("powerMode");
// if (powerMode != null) {
// Field powerModeField = lwM2mClientClass.getDeclaredField("powerMode");
// powerModeField.setAccessible(true);
// powerModeField.set(lwM2mClient, PowerMode.valueOf(powerMode.asString()));
// }
//
// JsonValue edrxCycle = o.get("edrxCycle");
// if (edrxCycle != null) {
// Field edrxCycleField = lwM2mClientClass.getDeclaredField("edrxCycle");
// edrxCycleField.setAccessible(true);
// edrxCycleField.set(lwM2mClient, edrxCycle.asLong());
// }
//
// JsonValue psmActivityTimer = o.get("psmActivityTimer");
// if (psmActivityTimer != null) {
// Field psmActivityTimerField = lwM2mClientClass.getDeclaredField("psmActivityTimer");
// psmActivityTimerField.setAccessible(true);
// psmActivityTimerField.set(lwM2mClient, psmActivityTimer.asLong());
// }
//
// JsonValue pagingTransmissionWindow = o.get("pagingTransmissionWindow");
// if (pagingTransmissionWindow != null) {
// Field pagingTransmissionWindowField = lwM2mClientClass.getDeclaredField("pagingTransmissionWindow");
// pagingTransmissionWindowField.setAccessible(true);
// pagingTransmissionWindowField.set(lwM2mClient, pagingTransmissionWindow.asLong());
// }
//
// JsonValue registration = o.get("registration");
// if (registration != null) {
// lwM2mClient.setRegistration(RegistrationSerDes.deserialize(registration.asObject()));
// }
//
// lwM2mClient.setAsleep(o.get("asleep").getAsBoolean());
//
// Field lastUplinkTimeField = lwM2mClientClass.getDeclaredField("lastUplinkTime");
// lastUplinkTimeField.setAccessible(true);
// lastUplinkTimeField.setLong(lwM2mClient, o.get("lastUplinkTime").asLong());
//
// Field firstEdrxDownlinkField = lwM2mClientClass.getDeclaredField("firstEdrxDownlink");
// firstEdrxDownlinkField.setAccessible(true);
// firstEdrxDownlinkField.setBoolean(lwM2mClient, o.get("firstEdrxDownlink").getAsBoolean());
//
// lwM2mClient.getRetryAttempts().set(o.get("retryAttempts").asInt());
//
// JsonValue lastSentRpcId = o.get("lastSentRpcId");
// if (lastSentRpcId != null) {
// lwM2mClient.setLastSentRpcId(UUID.fromString(lastSentRpcId.asString()));
// }
JsonObject o = JsonParser.parseString(new String(data)).getAsJsonObject();
LwM2mClient lwM2mClient = new LwM2mClient(o.get("nodeId").getAsString(), o.get("endpoint").getAsString());
o.get("resources").getAsJsonObject().entrySet().forEach(entry -> {
JsonObject resource = entry.getValue().getAsJsonObject();
LwM2mResource lwM2mResource = parseLwM2mResource(resource.get("lwM2mResource").getAsJsonObject());
ResourceModel resourceModel = parseResourceModel(resource.get("resourceModel").getAsJsonObject());
ResourceValue resourceValue = new ResourceValue(lwM2mResource, resourceModel);
lwM2mClient.getResources().put(String.valueOf(lwM2mResource.getId()), resourceValue);
});
for (Entry<String, JsonElement> entry : o.get("sharedAttributes").getAsJsonObject().entrySet()) {
TransportProtos.TsKvProto.Builder builder = TransportProtos.TsKvProto.newBuilder();
JsonFormat.parser().merge(entry.getValue().getAsString(), builder);
lwM2mClient.getSharedAttributes().put(entry.getKey(), builder.build());
}
o.get("keyTsLatestMap").getAsJsonObject().entrySet().forEach(entry -> {
lwM2mClient.getKeyTsLatestMap().put(entry.getKey(), new AtomicLong(entry.getValue().getAsLong()));
});
lwM2mClient.setState(LwM2MClientState.valueOf(o.get("state").getAsString()));
Class<LwM2mClient> lwM2mClientClass = LwM2mClient.class;
JsonElement session = o.get("session");
if (session != null) {
TransportProtos.SessionInfoProto.Builder builder = TransportProtos.SessionInfoProto.newBuilder();
JsonFormat.parser().merge(session.getAsString(), builder);
Field sessionField = lwM2mClientClass.getDeclaredField("session");
sessionField.setAccessible(true);
sessionField.set(lwM2mClient, builder.build());
}
JsonElement tenantId = o.get("tenantId");
if (tenantId != null) {
Field tenantIdField = lwM2mClientClass.getDeclaredField("tenantId");
tenantIdField.setAccessible(true);
tenantIdField.set(lwM2mClient, new TenantId(UUID.fromString(tenantId.getAsString())));
}
JsonElement deviceId = o.get("deviceId");
if (tenantId != null) {
Field deviceIdField = lwM2mClientClass.getDeclaredField("deviceId");
deviceIdField.setAccessible(true);
deviceIdField.set(lwM2mClient, UUID.fromString(deviceId.getAsString()));
}
JsonElement profileId = o.get("profileId");
if (tenantId != null) {
Field profileIdField = lwM2mClientClass.getDeclaredField("profileId");
profileIdField.setAccessible(true);
profileIdField.set(lwM2mClient, UUID.fromString(profileId.getAsString()));
}
JsonElement powerMode = o.get("powerMode");
if (powerMode != null) {
Field powerModeField = lwM2mClientClass.getDeclaredField("powerMode");
powerModeField.setAccessible(true);
powerModeField.set(lwM2mClient, PowerMode.valueOf(powerMode.getAsString()));
}
JsonElement edrxCycle = o.get("edrxCycle");
if (edrxCycle != null) {
Field edrxCycleField = lwM2mClientClass.getDeclaredField("edrxCycle");
edrxCycleField.setAccessible(true);
edrxCycleField.set(lwM2mClient, edrxCycle.getAsLong());
}
JsonElement psmActivityTimer = o.get("psmActivityTimer");
if (psmActivityTimer != null) {
Field psmActivityTimerField = lwM2mClientClass.getDeclaredField("psmActivityTimer");
psmActivityTimerField.setAccessible(true);
psmActivityTimerField.set(lwM2mClient, psmActivityTimer.getAsLong());
}
JsonElement pagingTransmissionWindow = o.get("pagingTransmissionWindow");
if (pagingTransmissionWindow != null) {
Field pagingTransmissionWindowField = lwM2mClientClass.getDeclaredField("pagingTransmissionWindow");
pagingTransmissionWindowField.setAccessible(true);
pagingTransmissionWindowField.set(lwM2mClient, pagingTransmissionWindow.getAsLong());
}
JsonElement registration = o.get("registration");
if (registration != null) {
lwM2mClient.setRegistration(registrationSerDes.deserialize(toJsonNode(registration.getAsString())));
}
lwM2mClient.setAsleep(o.get("asleep").getAsBoolean());
Field lastUplinkTimeField = lwM2mClientClass.getDeclaredField("lastUplinkTime");
lastUplinkTimeField.setAccessible(true);
lastUplinkTimeField.setLong(lwM2mClient, o.get("lastUplinkTime").getAsLong());
Field firstEdrxDownlinkField = lwM2mClientClass.getDeclaredField("firstEdrxDownlink");
firstEdrxDownlinkField.setAccessible(true);
firstEdrxDownlinkField.setBoolean(lwM2mClient, o.get("firstEdrxDownlink").getAsBoolean());
lwM2mClient.getRetryAttempts().set(o.get("retryAttempts").getAsInt());
JsonElement lastSentRpcId = o.get("lastSentRpcId");
if (lastSentRpcId != null) {
lwM2mClient.setLastSentRpcId(UUID.fromString(lastSentRpcId.getAsString()));
}
return lwM2mClient;
}

100
common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServiceTest.java

@ -1,100 +0,0 @@
/**
* Copyright © 2016-2024 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.transport.lwm2m.bootstrap;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.junit.Ignore;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.transport.lwm2m.bootstrap.secure.TbLwM2MDtlsBootstrapCertificateVerifier;
import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MBootstrapSecurityStore;
import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MInMemoryBootstrapConfigStore;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.when;
@ExtendWith(MockitoExtension.class)
public class LwM2MTransportBootstrapServiceTest {
@Mock
private LwM2MTransportServerConfig serverConfig;
@Mock
private LwM2MTransportBootstrapConfig bootstrapConfig;
@Mock
private LwM2MBootstrapSecurityStore lwM2MBootstrapSecurityStore;
@Mock
private LwM2MInMemoryBootstrapConfigStore lwM2MInMemoryBootstrapConfigStore;
@Mock
private TransportService transportService;
@Mock
private TbLwM2MDtlsBootstrapCertificateVerifier certificateVerifier;
@Disabled // fixme: nick
@Test
public void getLHServer_creates_ConnectionIdGenerator_when_connection_id_length_not_null(){
final Integer CONNECTION_ID_LENGTH = 6;
when(serverConfig.getDtlsCidLength()).thenReturn(CONNECTION_ID_LENGTH);
var lwM2MBootstrapService = createLwM2MBootstrapService();
var server = lwM2MBootstrapService.getLhBootstrapServer();
var securedEndpoint = (CoapEndpoint) ReflectionTestUtils.getField(server, "securedEndpoint");
assertThat(securedEndpoint).isNotNull();
var config = (DtlsConnectorConfig) ReflectionTestUtils.getField(securedEndpoint.getConnector(), "config");
assertThat(config).isNotNull();
assertThat(config.getConnectionIdGenerator()).isNotNull();
assertThat((Integer) ReflectionTestUtils.getField(config.getConnectionIdGenerator(), "connectionIdLength"))
.isEqualTo(CONNECTION_ID_LENGTH);
}
@Disabled // fixme: nick
@Test
public void getLHServer_creates_no_ConnectionIdGenerator_when_connection_id_length_is_null(){
when(serverConfig.getDtlsCidLength()).thenReturn(null);
var lwM2MBootstrapService = createLwM2MBootstrapService();
var server = lwM2MBootstrapService.getLhBootstrapServer();
var securedEndpoint = (CoapEndpoint) ReflectionTestUtils.getField(server, "securedEndpoint");
assertThat(securedEndpoint).isNotNull();
var config = (DtlsConnectorConfig) ReflectionTestUtils.getField(securedEndpoint.getConnector(), "config");
assertThat(config).isNotNull();
assertThat(config.getConnectionIdGenerator()).isNull();
}
private LwM2MTransportBootstrapService createLwM2MBootstrapService() {
setDefaultConfigVariables();
return new LwM2MTransportBootstrapService(serverConfig, bootstrapConfig, lwM2MBootstrapSecurityStore,
lwM2MInMemoryBootstrapConfigStore, transportService, certificateVerifier);
}
private void setDefaultConfigVariables(){
when(bootstrapConfig.getPort()).thenReturn(5683);
when(bootstrapConfig.getSecurePort()).thenReturn(5684);
when(serverConfig.isRecommendedCiphers()).thenReturn(false);
when(serverConfig.getDtlsRetransmissionTimeout()).thenReturn(9000);
}
}

11
common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientTest.java

@ -15,9 +15,10 @@
*/
package org.thingsboard.server.transport.lwm2m.server.client;
import org.eclipse.leshan.core.endpoint.EndpointUriUtil;
import org.eclipse.leshan.core.link.Link;
import org.eclipse.leshan.core.peer.IpPeer;
import org.eclipse.leshan.server.registration.Registration;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
@ -25,14 +26,14 @@ import java.net.InetSocketAddress;
public class LwM2mClientTest {
@Ignore
@Test
public void setRegistration() {
LwM2mClient client = new LwM2mClient("nodeId", "testEndpoint");
Registration registration = null; /*new Registration
.Builder("test", "testEndpoint", Identity.unsecure(new InetSocketAddress(1000))) // FIXME: nick
Registration registration = new Registration
.Builder("testId", "testEndpoint", new IpPeer(new InetSocketAddress(1000)),
EndpointUriUtil.createUri("coap://localhost:5685"))
.objectLinks(new Link[0])
.build();*/
.build();
Assertions.assertDoesNotThrow(() -> client.setRegistration(registration));
}

41
common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java

@ -15,13 +15,17 @@
*/
package org.thingsboard.server.transport.lwm2m.server.store.util;
import org.eclipse.leshan.core.LwM2m.LwM2mVersion;
import org.eclipse.leshan.core.endpoint.EndpointUriUtil;
import org.eclipse.leshan.core.link.Link;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.LwM2mSingleResource;
import org.eclipse.leshan.core.peer.IpPeer;
import org.eclipse.leshan.core.request.WriteRequest;
import org.eclipse.leshan.server.registration.Registration;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.device.data.PowerMode;
@ -41,9 +45,12 @@ import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.UUID;
@ -56,10 +63,9 @@ import static org.mockito.Mockito.when;
public class LwM2MClientSerDesTest {
@Ignore
@Test
public void serializeDeserialize() throws Exception {
LwM2mClient client = new LwM2mClient("nodeId", "testEndpoint");
LwM2mClient client = new LwM2mClient("nodeId", "endpoint");
TransportDeviceInfo tdi = new TransportDeviceInfo();
tdi.setPowerMode(PowerMode.PSM);
@ -78,13 +84,13 @@ public class LwM2MClientSerDesTest {
client.init(credentialsResponse, UUID.randomUUID());
Registration registration = null; // FIXME: nick
// new Registration.Builder("test", "testEndpoint", Identity
// .unsecure(new InetSocketAddress(1000)))
// .supportedContentFormats()
// .supportedObjects(Map.of(15, "1.0", 17, "1.0"))
// .objectLinks(new Link[]{new Link("/")})
// .build();
Registration registration = new Registration
.Builder("test", "endpoint", new IpPeer(new InetSocketAddress(Inet4Address.getLoopbackAddress(), 1000)),
EndpointUriUtil.createUri("coap://localhost:5685"))
.supportedContentFormats()
.supportedObjects(Map.of(15, LwM2mVersion.V1_0, 17, LwM2mVersion.V1_0))
.objectLinks(new Link[] { new Link("/15"), new Link("/17") })
.build();
client.setRegistration(registration);
client.setState(LwM2MClientState.REGISTERED);
@ -130,7 +136,12 @@ public class LwM2MClientSerDesTest {
assertEquals(client.getPsmActivityTimer(), desClient.getPsmActivityTimer());
assertEquals(client.getPagingTransmissionWindow(), desClient.getPagingTransmissionWindow());
assertEquals(client.getEdrxCycle(), desClient.getEdrxCycle());
assertEquals(client.getRegistration(), desClient.getRegistration());
if (((IpPeer)desClient.getRegistration().getClientTransportData()).getSocketAddress().isUnresolved()) {
String actualReg = desClient.getRegistration().toString().replaceAll("/<unresolved>", "");
assertEquals(client.getRegistration().toString(), actualReg);
} else {
assertEquals(client.getRegistration(), desClient.getRegistration());
}
assertEquals(client.isAsleep(), desClient.isAsleep());
assertEquals(client.getLastUplinkTime(), desClient.getLastUplinkTime());
assertEquals(client.getSleepTask(), desClient.getSleepTask());
@ -143,7 +154,11 @@ public class LwM2MClientSerDesTest {
Map<String, ResourceValue> actualResources = desClient.getResources();
assertNotNull(actualResources);
assertEquals(expectedResources.size(), actualResources.size());
expectedResources.forEach((key, value) -> assertEquals(value.toString(), actualResources.get(key).toString()));
for (Entry entry : expectedResources.entrySet()) {
LwM2mPath expectedPathId = client.getLwM2mPathFromString(entry.getKey().toString());
String actualOld = actualResources.get(String.valueOf(expectedPathId.getObjectId())).toString();
String actual = actualOld.replaceAll("\"", "");
assertEquals(entry.getValue().toString(), actual);
}
}
}

16
common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java

@ -30,6 +30,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.KvEntry;
@ -50,6 +51,7 @@ import java.util.regex.Pattern;
/**
* Created by Valerii Sosliuk on 5/12/2017.
*/
@Slf4j
public class JacksonUtil {
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@ -158,6 +160,20 @@ public class JacksonUtil {
}
}
public static String toPlainText(String data) {
if (data == null) {
return null;
}
if (data.startsWith("\"") && data.endsWith("\"") && data.length() >= 2) {
final String dataBefore = data;
try {
data = JacksonUtil.fromString(data, String.class);
} catch (Exception ignored) {}
log.trace("Trimming double quotes. Before trim: [{}], after trim: [{}]", dataBefore, data);
}
return data;
}
public static <T> T treeToValue(JsonNode node, Class<T> clazz) {
try {
return OBJECT_MAPPER.treeToValue(node, clazz);

14
common/util/src/test/java/org/thingsboard/common/util/JacksonUtilTest.java

@ -19,6 +19,9 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Assert;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
@ -55,4 +58,15 @@ public class JacksonUtilTest {
Assert.assertEquals(asset.getName(), result.getName());
Assert.assertEquals(asset.getType(), result.getType());
}
@ParameterizedTest
@ValueSource(strings = { "", "false", "\"", "\"\"", "\"This is a string with double quotes\"", "Path: /home/developer/test.txt",
"First line\nSecond line\n\nFourth line", "Before\rAfter", "Tab\tSeparated\tValues", "Test\bbackspace", "[]",
"[1, 2, 3]", "{\"key\": \"value\"}", "{\n\"temperature\": 25.5,\n\"humidity\": 50.2\n\"}", "Expression: (a + b) * c",
"世界", "Україна", "\u1F1FA\u1F1E6", "🇺🇦"})
public void toPlainTextTest(String original) {
String serialized = JacksonUtil.toString(original);
Assertions.assertNotNull(serialized);
Assertions.assertEquals(original, JacksonUtil.toPlainText(serialized));
}
}

9
dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java

@ -25,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.StringUtils;
@ -200,10 +201,10 @@ public class AuditLogServiceImpl implements AuditLogService {
break;
case ATTRIBUTES_UPDATED:
actionData.put("entityId", entityId.toString());
String scope = extractParameter(String.class, 0, additionalInfo);
AttributeScope scope = extractParameter(AttributeScope.class, 0, additionalInfo);
@SuppressWarnings("unchecked")
List<AttributeKvEntry> attributes = extractParameter(List.class, 1, additionalInfo);
actionData.put("scope", scope);
actionData.put("scope", scope.name());
ObjectNode attrsNode = JacksonUtil.newObjectNode();
if (attributes != null) {
for (AttributeKvEntry attr : attributes) {
@ -215,8 +216,8 @@ public class AuditLogServiceImpl implements AuditLogService {
case ATTRIBUTES_DELETED:
case ATTRIBUTES_READ:
actionData.put("entityId", entityId.toString());
scope = extractParameter(String.class, 0, additionalInfo);
actionData.put("scope", scope);
scope = extractParameter(AttributeScope.class, 0, additionalInfo);
actionData.put("scope", scope.name());
@SuppressWarnings("unchecked")
List<String> keys = extractParameter(List.class, 1, additionalInfo);
ArrayNode attrsArrayNode = actionData.putArray("attributes");

17
dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java

@ -21,6 +21,7 @@ import com.google.common.base.Function;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import jakarta.annotation.Nullable;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
@ -31,7 +32,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
@ -71,7 +72,6 @@ import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import jakarta.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@ -354,7 +354,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
return Futures.successfulAsList(futures);
}, MoreExecutors.directExecutor());
edges = Futures.transform(edges, new Function<List<Edge>, List<Edge>>() {
edges = Futures.transform(edges, new Function<>() {
@Nullable
@Override
public List<Edge> apply(@Nullable List<Edge> edgeList) {
@ -414,7 +414,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
}
private PaginatedRemover<TenantId, Edge> tenantEdgesRemover =
new PaginatedRemover<TenantId, Edge>() {
new PaginatedRemover<>() {
@Override
protected PageData<Edge> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
@ -427,7 +427,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
}
};
private PaginatedRemover<CustomerId, Edge> customerEdgeUnassigner = new PaginatedRemover<CustomerId, Edge>() {
private PaginatedRemover<CustomerId, Edge> customerEdgeUnassigner = new PaginatedRemover<>() {
@Override
protected PageData<Edge> findEntities(TenantId tenantId, CustomerId id, PageLink pageLink) {
@ -513,7 +513,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
@Override
public String findMissingToRelatedRuleChains(TenantId tenantId, EdgeId edgeId, String tbRuleChainInputNodeClassName) {
List<RuleChain> edgeRuleChains = findEdgeRuleChains(tenantId, edgeId);
List<RuleChainId> edgeRuleChainIds = edgeRuleChains.stream().map(IdBased::getId).collect(Collectors.toList());
List<RuleChainId> edgeRuleChainIds = edgeRuleChains.stream().map(IdBased::getId).toList();
ObjectNode result = JacksonUtil.newObjectNode();
for (RuleChain edgeRuleChain : edgeRuleChains) {
List<RuleNode> ruleNodes =
@ -522,8 +522,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
List<RuleChainId> connectedRuleChains =
ruleNodes.stream()
.filter(rn -> rn.getType().equals(tbRuleChainInputNodeClassName))
.map(rn -> new RuleChainId(UUID.fromString(rn.getConfiguration().get("ruleChainId").asText())))
.collect(Collectors.toList());
.map(rn -> new RuleChainId(UUID.fromString(rn.getConfiguration().get("ruleChainId").asText()))).toList();
List<String> missingRuleChains = new ArrayList<>();
for (RuleChainId connectedRuleChain : connectedRuleChains) {
if (!edgeRuleChainIds.contains(connectedRuleChain)) {
@ -549,7 +548,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
if (persistToTelemetry) {
futureKvEntry = timeseriesService.findLatest(tenantId, edgeId, key);
} else {
futureKvEntry = attributesService.find(tenantId, edgeId, DataConstants.SERVER_SCOPE, key);
futureKvEntry = attributesService.find(tenantId, edgeId, AttributeScope.SERVER_SCOPE, key);
}
return Futures.transformAsync(futureKvEntry, kvEntryOpt ->
Futures.immediateFuture(kvEntryOpt.flatMap(KvEntry::getBooleanValue).orElse(false)), MoreExecutors.directExecutor());

121
dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java

@ -40,11 +40,16 @@ import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityFilterType;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityListFilter;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.RelationsQueryFilter;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -63,6 +68,10 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId ";
public static final CustomerId NULL_CUSTOMER_ID = new CustomerId(NULL_UUID);
private static final int MAX_ENTITY_IDS_SIZE = 1024;
private static final Set<EntityFilterType> EXCLUDED_TYPES_FROM_OPTIMIZATION = Set.of(
EntityFilterType.ENTITY_LIST, EntityFilterType.SINGLE_ENTITY, EntityFilterType.RELATIONS_QUERY);
@Autowired
private EntityQueryDao entityQueryDao;
@ -86,9 +95,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id);
validateEntityDataQuery(query);
if (EntityFilterType.RELATIONS_QUERY.equals(query.getEntityFilter().getType())
|| EntityFilterType.SINGLE_ENTITY.equals(query.getEntityFilter().getType())
|| StringUtils.isNotEmpty(query.getPageLink().getTextSearch())) {
if (!isValidForOptimization(query)) {
return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, query);
}
@ -97,41 +104,10 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
if (entityDataByQuery == null || entityDataByQuery.getData().isEmpty()) {
return entityDataByQuery;
}
// 2 step - find entity data by entity ids from the 1st step
PageData<EntityData> result = findEntityDataByEntityIds(tenantId, customerId, query, entityDataByQuery.getData());
return new PageData<>(result.getData(), entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext());
}
private PageData<EntityData> findEntityIdsByFilterAndSorterColumns(TenantId tenantId, CustomerId customerId, EntityDataQuery query) {
List<EntityKey> entityFields = null;
List<EntityKey> latestValues = null;
if (query.getPageLink().getSortOrder() != null) {
if (query.getEntityFields() != null) {
entityFields = query.getEntityFields().stream()
.filter(entityKey -> entityKey.getKey().equals(query.getPageLink().getSortOrder().getKey().getKey()))
.collect(Collectors.toList());
}
if (query.getLatestValues() != null) {
latestValues = query.getLatestValues().stream()
.filter(entityKey -> entityKey.getKey().equals(query.getPageLink().getSortOrder().getKey().getKey()))
.collect(Collectors.toList());
}
}
EntityDataQuery entityQuery = new EntityDataQuery(query.getEntityFilter(), query.getPageLink(), entityFields, latestValues, query.getKeyFilters());
return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, entityQuery);
}
private PageData<EntityData> findEntityDataByEntityIds(TenantId tenantId, CustomerId customerId, EntityDataQuery query, List<EntityData> data) {
List<String> entityIds = data.stream().map(d -> d.getEntityId().getId().toString()).toList();
EntityType entityType = data.isEmpty() ? null : data.get(0).getEntityId().getEntityType();
EntityListFilter filter = new EntityListFilter();
filter.setEntityType(entityType);
filter.setEntityList(entityIds);
EntityDataPageLink pageLink = new EntityDataPageLink(query.getPageLink().getPageSize(), 0, null, query.getPageLink().getSortOrder());
EntityDataQuery entityQuery = new EntityDataQuery(filter, pageLink, query.getEntityFields(), query.getLatestValues(), null);
return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, entityQuery);
// 2 step - find entity data by entity ids from the 1st step
List<EntityData> result = fetchEntityDataByIdsFromInitialQuery(tenantId, customerId, query, entityDataByQuery.getData());
return new PageData<>(result, entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext());
}
@Override
@ -228,4 +204,75 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
}
}
private boolean isValidForOptimization(EntityDataQuery query) {
if (StringUtils.isNotEmpty(query.getPageLink().getTextSearch())) {
return false;
}
if (EXCLUDED_TYPES_FROM_OPTIMIZATION.contains(query.getEntityFilter().getType())) {
return false;
}
if ((query.getEntityFields() == null || query.getEntityFields().isEmpty()) &&
(query.getLatestValues() == null || query.getLatestValues().isEmpty())) {
return false;
}
Set<EntityKey> filteringKeys = new HashSet<>(Optional.ofNullable(query.getKeyFilters()).orElse(Collections.emptyList()).stream().map(KeyFilter::getKey).toList());
Set<EntityKey> entityFields = new HashSet<>(Optional.ofNullable(query.getEntityFields()).orElse(Collections.emptyList()));
Set<EntityKey> latestValues = new HashSet<>(Optional.ofNullable(query.getLatestValues()).orElse(Collections.emptyList()));
return !(filteringKeys.containsAll(entityFields) && filteringKeys.containsAll(latestValues));
}
private PageData<EntityData> findEntityIdsByFilterAndSorterColumns(TenantId tenantId, CustomerId customerId, EntityDataQuery query) {
List<EntityKey> entityFields = null;
List<EntityKey> latestValues = null;
if (query.getPageLink().getSortOrder() != null) {
if (query.getEntityFields() != null) {
entityFields = query.getEntityFields().stream()
.filter(entityKey -> entityKey.getKey().equals(query.getPageLink().getSortOrder().getKey().getKey()))
.collect(Collectors.toList());
}
if (query.getLatestValues() != null) {
latestValues = query.getLatestValues().stream()
.filter(entityKey -> entityKey.getKey().equals(query.getPageLink().getSortOrder().getKey().getKey()))
.collect(Collectors.toList());
}
}
EntityDataQuery entityQuery = new EntityDataQuery(query.getEntityFilter(), query.getPageLink(), entityFields, latestValues, query.getKeyFilters());
return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, entityQuery);
}
private List<EntityData> fetchEntityDataByIdsFromInitialQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query, List<EntityData> initialQueryResult) {
List<EntityData> result = new ArrayList<>();
List<String> entityIds = initialQueryResult.stream().map(d -> d.getEntityId().getId().toString()).collect(Collectors.toList());
EntityType entityType = initialQueryResult.get(0).getEntityId().getEntityType();
if (entityIds.size() > MAX_ENTITY_IDS_SIZE) {
List<List<String>> chunks = new ArrayList<>();
for (int i = 0; i < entityIds.size(); i += MAX_ENTITY_IDS_SIZE) {
chunks.add(entityIds.subList(i, Math.min(entityIds.size(), i + MAX_ENTITY_IDS_SIZE)));
}
for (List<String> chunk : chunks) {
result.addAll(findEntityDataByEntityIds(tenantId, customerId, query, chunk, entityType, chunk.size()));
}
} else {
result.addAll(findEntityDataByEntityIds(tenantId, customerId, query, entityIds, entityType, query.getPageLink().getPageSize()));
}
return result;
}
private List<EntityData> findEntityDataByEntityIds(TenantId tenantId, CustomerId customerId, EntityDataQuery query,
List<String> entityIds, EntityType entityType, int pageSize) {
EntityListFilter filter = new EntityListFilter();
filter.setEntityType(entityType);
filter.setEntityList(entityIds);
EntityDataPageLink pageLink = new EntityDataPageLink(pageSize, 0, null, query.getPageLink().getSortOrder());
EntityDataQuery entityQuery = new EntityDataQuery(filter, pageLink, query.getEntityFields(), query.getLatestValues(), null);
return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, entityQuery).getData();
}
}

10
dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java

@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.service.DataValidator;
import java.util.List;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -106,6 +107,11 @@ public class BaseEventService implements EventService {
return convert(entityId.getEntityType(), eventDao.findLatestEvents(tenantId.getId(), entityId.getId(), eventType, limit));
}
@Override
public EventInfo findLatestDebugRuleNodeInEvent(TenantId tenantId, EntityId entityId) {
return convert(entityId.getEntityType(), eventDao.findLatestDebugRuleNodeInEvent(tenantId.getId(), entityId.getId()));
}
@Override
public PageData<EventInfo> findEventsByFilter(TenantId tenantId, EntityId entityId, EventFilter eventFilter, TimePageLink pageLink) {
return convert(entityId.getEntityType(), eventDao.findEventByFilter(tenantId.getId(), entityId.getId(), eventFilter, pageLink));
@ -140,4 +146,8 @@ public class BaseEventService implements EventService {
return list == null ? null : list.stream().map(e -> e.toInfo(entityType)).collect(Collectors.toList());
}
private EventInfo convert(EntityType entityType, Event event) {
return Optional.ofNullable(event).map(e -> e.toInfo(entityType)).orElse(null);
}
}

9
dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java

@ -62,6 +62,15 @@ public interface EventDao {
*/
List<? extends Event> findLatestEvents(UUID tenantId, UUID entityId, EventType eventType, int limit);
/**
* Find latest debug IN event by tenantId, entityId.
*
* @param tenantId the tenantId
* @param entityId the entityId
* @return the latest debug IN event
*/
Event findLatestDebugRuleNodeInEvent(UUID tenantId, UUID entityId);
/**
* Executes stored procedure to cleanup old events. Uses separate ttl for debug and other events.
* @param regularEventExpTs the expiration time of the regular events

3
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java

@ -20,7 +20,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
@ -157,7 +156,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
return new UserNotificationSettings(prefs);
}
@Transactional(propagation = Propagation.NOT_SUPPORTED) // so that parent transaction is not aborted on method failure
@Transactional
@Override
public void createDefaultNotificationConfigs(TenantId tenantId) {
NotificationTarget allUsers = createTarget(tenantId, "All users", new AllUsersFilter(),

2
dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java

@ -119,7 +119,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
public RuleChain saveRuleChain(RuleChain ruleChain, boolean publishSaveEvent) {
ruleChainValidator.validate(ruleChain, RuleChain::getTenantId);
try {
RuleChain savedRuleChain = ruleChainDao.save(ruleChain.getTenantId(), ruleChain);
RuleChain savedRuleChain = ruleChainDao.saveAndFlush(ruleChain.getTenantId(), ruleChain);
if (ruleChain.getId() == null) {
entityCountService.publishCountEntityEvictEvent(ruleChain.getTenantId(), EntityType.RULE_CHAIN);
}

5
dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java

@ -392,6 +392,11 @@ public class JpaBaseEventDao implements EventDao {
return DaoUtil.convertDataList(getEventRepository(eventType).findLatestEvents(tenantId, entityId, limit));
}
@Override
public Event findLatestDebugRuleNodeInEvent(UUID tenantId, UUID entityId) {
return DaoUtil.getData(ruleNodeDebugEventRepository.findLatestDebugRuleNodeInEvent(tenantId, entityId));
}
@Override
public void cleanupEvents(long regularEventExpTs, long debugEventExpTs, boolean cleanupDb) {
if (regularEventExpTs > 0) {

4
dao/src/main/java/org/thingsboard/server/dao/sql/event/RuleNodeDebugEventRepository.java

@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.event.RuleNodeDebugEvent;
import org.thingsboard.server.dao.model.sql.RuleNodeDebugEventEntity;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@ -35,6 +36,9 @@ public interface RuleNodeDebugEventRepository extends EventRepository<RuleNodeDe
@Query(nativeQuery = true, value = "SELECT * FROM rule_node_debug_event e WHERE e.tenant_id = :tenantId AND e.entity_id = :entityId ORDER BY e.ts DESC LIMIT :limit")
List<RuleNodeDebugEventEntity> findLatestEvents(@Param("tenantId") UUID tenantId, @Param("entityId") UUID entityId, @Param("limit") int limit);
@Query(nativeQuery = true, value = "SELECT * FROM rule_node_debug_event e WHERE e.tenant_id = :tenantId AND e.entity_id = :entityId AND e.e_type = 'IN' ORDER BY e.ts DESC LIMIT 1")
Optional<RuleNodeDebugEventEntity> findLatestDebugRuleNodeInEvent(@Param("tenantId") UUID tenantId, @Param("entityId") UUID entityId);
@Override
@Query("SELECT e FROM RuleNodeDebugEventEntity e WHERE " +
"e.tenantId = :tenantId " +

31
dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java

@ -64,6 +64,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -192,12 +193,12 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
@Override
@Transactional
public Tenant saveTenant(Tenant tenant) {
return saveTenant(tenant, true);
return saveTenant(tenant, null);
}
@Override
@Transactional
public Tenant saveTenant(Tenant tenant, boolean publishSaveEvent) {
public Tenant saveTenant(Tenant tenant, Consumer<TenantId> defaultEntitiesCreator) {
log.trace("Executing saveTenant [{}]", tenant);
tenant.setRegion(DEFAULT_TENANT_REGION);
if (tenant.getTenantProfileId() == null) {
@ -206,20 +207,20 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
}
tenantValidator.validate(tenant, Tenant::getId);
boolean create = tenant.getId() == null;
Tenant savedTenant = tenantDao.save(tenant.getId(), tenant);
publishEvictEvent(new TenantEvictEvent(savedTenant.getId(), create));
if (publishSaveEvent) {
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedTenant.getId())
.entityId(savedTenant.getId()).entity(savedTenant).created(create).build());
}
if (tenant.getId() == null) {
deviceProfileService.createDefaultDeviceProfile(savedTenant.getId());
assetProfileService.createDefaultAssetProfile(savedTenant.getId());
apiUsageStateService.createDefaultApiUsageState(savedTenant.getId(), null);
try {
notificationSettingsService.createDefaultNotificationConfigs(savedTenant.getId());
} catch (Throwable e) {
log.error("Failed to create default notification configs for tenant {}", savedTenant.getId(), e);
TenantId tenantId = savedTenant.getId();
publishEvictEvent(new TenantEvictEvent(tenantId, create));
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId)
.entityId(tenantId).entity(savedTenant).created(create).build());
if (create) {
deviceProfileService.createDefaultDeviceProfile(tenantId);
assetProfileService.createDefaultAssetProfile(tenantId);
apiUsageStateService.createDefaultApiUsageState(tenantId, null);
notificationSettingsService.createDefaultNotificationConfigs(tenantId);
if (defaultEntitiesCreator != null) {
defaultEntitiesCreator.accept(tenantId);
}
}
return savedTenant;

5
dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java

@ -98,6 +98,10 @@ public abstract class AbstractServiceTest {
protected RuleNodeDebugEvent generateEvent(TenantId tenantId, EntityId entityId) throws IOException {
return generateEvent(tenantId, entityId, null);
}
protected RuleNodeDebugEvent generateEvent(TenantId tenantId, EntityId entityId, String eventType) throws IOException {
if (tenantId == null) {
tenantId = TenantId.fromUUID(Uuids.timeBased());
}
@ -105,6 +109,7 @@ public abstract class AbstractServiceTest {
.tenantId(tenantId)
.entityId(entityId.getId())
.serviceId("server A")
.eventType(eventType)
.data(JacksonUtil.toString(readFromResource("TestJsonData.json")))
.build();
}

129
dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java

@ -26,7 +26,6 @@ import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
@ -99,6 +98,8 @@ import java.util.stream.Stream;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.thingsboard.server.common.data.query.EntityKeyType.ATTRIBUTE;
import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD;
@Slf4j
@DaoSqlTest
@ -2113,6 +2114,132 @@ public class EntityServiceTest extends AbstractServiceTest {
deviceService.deleteDevicesByTenantId(tenantId);
}
@Test
public void testFindEntityQuery_for_5000_devices_with_3000_pageSize() {
int pageSize = 3000;
int expectedDevicesSize = 4000;
int unexpectedDevicesSize = 1000;
for (int i = 0; i < expectedDevicesSize + unexpectedDevicesSize; i++) {
Device device = new Device();
device.setTenantId(tenantId);
if (i < expectedDevicesSize) {
device.setName("Device_" + i); // match deviceNameFilter 'D%'
} else {
device.setName("Test_" + i); // does not match deviceNameFilter 'D%'
}
device.setType("default");
device.setLabel("testLabel" + (int) (Math.random() * 1000));
Device savedDevice = deviceService.saveDevice(device);
attributesService.save(tenantId, savedDevice.getId(), AttributeScope.CLIENT_SCOPE,
new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("telemetry", (long) i)));
}
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("D%");
EntityDataSortOrder sortOrder = new EntityDataSortOrder(new EntityKey(ATTRIBUTE, "telemetry"), EntityDataSortOrder.Direction.DESC);
List<KeyFilter> deviceTypeFilters = createStringKeyFilters("type", ENTITY_FIELD, StringFilterPredicate.StringOperation.EQUAL, "default");
List<KeyFilter> attributeFilters = Collections.singletonList(createNumericKeyFilter("telemetry", ATTRIBUTE, NumericFilterPredicate.NumericOperation.LESS, expectedDevicesSize));
List<KeyFilter> nameFilters = createStringKeyFilters("name", ENTITY_FIELD, StringFilterPredicate.StringOperation.CONTAINS, "Device");
List<EntityKey> entityFields = Arrays.asList(new EntityKey(ENTITY_FIELD, "name"), new EntityKey(ENTITY_FIELD, "type"));
// 1. Device type filters:
// query with textSearch - optimization is not performing
EntityDataPageLink originalPageLink = new EntityDataPageLink(pageSize, 0, "Device", sortOrder);
EntityDataQuery originalQuery = new EntityDataQuery(filter, originalPageLink, entityFields, null, deviceTypeFilters);
PageData<EntityData> originalData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), originalQuery);
// query without textSearch - optimization is performing
EntityDataPageLink optimizedPageLink = new EntityDataPageLink(pageSize, 0, null, sortOrder);
EntityDataQuery optimizedQuery = new EntityDataQuery(filter, optimizedPageLink, entityFields, null, deviceTypeFilters);
PageData<EntityData> optimizedData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), optimizedQuery);
List<EntityData> loadedEntities = getLoadedEntities(optimizedData, optimizedQuery);
Assert.assertEquals(expectedDevicesSize, loadedEntities.size());
loadedEntities = getLoadedEntities(originalData, originalQuery);
Assert.assertEquals(expectedDevicesSize, loadedEntities.size());
Assert.assertEquals(pageSize, optimizedData.getData().size());
for (int i = 0; i < pageSize; i++) {
EntityData originalElement = originalData.getData().get(i);
EntityData optimizedElement = optimizedData.getData().get(i);
Assert.assertEquals(originalElement.getEntityId(), optimizedElement.getEntityId());
originalElement.getLatest().get(ENTITY_FIELD).forEach((key, value) -> {
Assert.assertEquals(value.getValue(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getValue());
Assert.assertEquals(value.getCount(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getCount());
});
}
Assert.assertEquals(originalData.getTotalPages(), optimizedData.getTotalPages());
Assert.assertEquals(originalData.getTotalElements(), optimizedData.getTotalElements());
// 2. Device attribute filters
// query with textSearch - optimization is not performing
originalPageLink = new EntityDataPageLink(pageSize, 0, "Device", sortOrder);
originalQuery = new EntityDataQuery(filter, originalPageLink, entityFields, null, attributeFilters);
originalData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), originalQuery);
// query without textSearch - optimization is performing
optimizedPageLink = new EntityDataPageLink(pageSize, 0, null, sortOrder);
optimizedQuery = new EntityDataQuery(filter, optimizedPageLink, entityFields, null, attributeFilters);
optimizedData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), optimizedQuery);
loadedEntities = getLoadedEntities(optimizedData, optimizedQuery);
Assert.assertEquals(expectedDevicesSize, loadedEntities.size());
loadedEntities = getLoadedEntities(originalData, originalQuery);
Assert.assertEquals(expectedDevicesSize, loadedEntities.size());
Assert.assertEquals(pageSize, optimizedData.getData().size());
for (int i = 0; i < pageSize; i++) {
EntityData originalElement = originalData.getData().get(i);
EntityData optimizedElement = optimizedData.getData().get(i);
Assert.assertEquals(originalElement.getEntityId(), optimizedElement.getEntityId());
originalElement.getLatest().get(ENTITY_FIELD).forEach((key, value) -> {
Assert.assertEquals(value.getValue(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getValue());
Assert.assertEquals(value.getCount(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getCount());
});
}
Assert.assertEquals(originalData.getTotalPages(), optimizedData.getTotalPages());
Assert.assertEquals(originalData.getTotalElements(), optimizedData.getTotalElements());
// 3. Device name filters
// query with textSearch - optimization is not performing
originalPageLink = new EntityDataPageLink(pageSize, 0, "Device", sortOrder);
originalQuery = new EntityDataQuery(filter, originalPageLink, entityFields, null, nameFilters);
originalData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), originalQuery);
// query without textSearch - optimization is performing
optimizedPageLink = new EntityDataPageLink(pageSize, 0, null, sortOrder);
optimizedQuery = new EntityDataQuery(filter, optimizedPageLink, entityFields, null, nameFilters);
optimizedData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), optimizedQuery);
loadedEntities = getLoadedEntities(optimizedData, optimizedQuery);
Assert.assertEquals(expectedDevicesSize, loadedEntities.size());
loadedEntities = getLoadedEntities(originalData, originalQuery);
Assert.assertEquals(expectedDevicesSize, loadedEntities.size());
Assert.assertEquals(pageSize, optimizedData.getData().size());
for (int i = 0; i < pageSize; i++) {
EntityData originalElement = originalData.getData().get(i);
EntityData optimizedElement = optimizedData.getData().get(i);
Assert.assertEquals(originalElement.getEntityId(), optimizedElement.getEntityId());
originalElement.getLatest().get(ENTITY_FIELD).forEach((key, value) -> {
Assert.assertEquals(value.getValue(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getValue());
Assert.assertEquals(value.getCount(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getCount());
});
}
Assert.assertEquals(originalData.getTotalPages(), optimizedData.getTotalPages());
Assert.assertEquals(originalData.getTotalElements(), optimizedData.getTotalElements());
deviceService.deleteDevicesByTenantId(tenantId);
}
private Boolean listEqualWithoutOrder(List<String> A, List<String> B) {
return A.containsAll(B) && B.containsAll(A);
}

34
dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java

@ -40,8 +40,11 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Created by igor on 3/13/18.
*/
@ -560,4 +563,35 @@ public class RuleChainServiceTest extends AbstractServiceTest {
Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(0, pageData.getData().size());
}
@Test
public void testSaveRuleChainWithExistingExternalId() {
RuleChainId externalRuleChainId = new RuleChainId(UUID.fromString("2675d180-e1e5-11ee-9f06-71b6c7dc2cbf"));
RuleChain ruleChain = getRuleChain();
ruleChain.setTenantId(tenantId);
ruleChain.setExternalId(externalRuleChainId);
ruleChainService.saveRuleChain(ruleChain);
assertThatThrownBy(() -> ruleChainService.saveRuleChain(ruleChain))
.isInstanceOf(DataValidationException.class)
.hasMessage("Rule Chain with such external id already exists!");
ruleChainService.deleteRuleChainsByTenantId(tenantId);
}
private RuleChain getRuleChain() {
String ruleChainStr = "{\n" +
" \"name\": \"Root Rule Chain\",\n" +
" \"type\": \"CORE\",\n" +
" \"firstRuleNodeId\": {\n" +
" \"entityType\": \"RULE_NODE\",\n" +
" \"id\": \"91ad0b00-e779-11ee-9cf0-15d8b6079fdb\"\n" +
" },\n" +
" \"debugMode\": false,\n" +
" \"configuration\": null,\n" +
" \"additionalInfo\": null\n" +
"}";
return JacksonUtil.fromString(ruleChainStr, RuleChain.class);
}
}

22
dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java

@ -132,8 +132,28 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest {
eventService.cleanupEvents(timeBeforeStartTime - 1, timeAfterEndTime + 1, true);
}
@Test
public void findLatestDebugRuleNodeInEvent() throws Exception {
CustomerId customerId = new CustomerId(Uuids.timeBased());
TenantId tenantId = TenantId.fromUUID(Uuids.timeBased());
Event event1 = saveEventWithProvidedTimeAndEventType(eventTime, "IN", customerId, tenantId);
Event event2 = saveEventWithProvidedTimeAndEventType(eventTime + 1, "IN", customerId, tenantId);
EventInfo event = eventService.findLatestDebugRuleNodeInEvent(tenantId, customerId);
Assert.assertNotNull(event);
Assert.assertEquals(event2.getUuidId(), event.getUuidId());
eventService.cleanupEvents(timeBeforeStartTime - 1, timeAfterEndTime + 1, true);
}
private Event saveEventWithProvidedTime(long time, EntityId entityId, TenantId tenantId) throws Exception {
RuleNodeDebugEvent event = generateEvent(tenantId, entityId);
return saveEventWithProvidedTimeAndEventType(time, null, entityId, tenantId);
}
private Event saveEventWithProvidedTimeAndEventType(long time, String eventType, EntityId entityId, TenantId tenantId) throws Exception {
RuleNodeDebugEvent event = generateEvent(tenantId, entityId, eventType);
event.setId(new EventId(Uuids.timeBased()));
event.setCreatedTime(time);
eventService.saveAsync(event).get();

31
dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.AbstractJpaDaoTest;
import org.thingsboard.server.dao.event.EventDao;
import org.thingsboard.server.dao.model.sql.RuleNodeDebugEventEntity;
import java.util.List;
import java.util.UUID;
@ -42,6 +43,10 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest {
@Autowired
private EventDao eventDao;
@Autowired
private RuleNodeDebugEventRepository ruleNodeDebugEventRepository;
UUID tenantId = Uuids.timeBased();
@ -106,6 +111,23 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest {
}
@Test
public void findLatestDebugRuleNodeInEvent() throws Exception {
UUID entityId = Uuids.timeBased();
RuleNodeDebugEventEntity event = getDebugInEventEntity(Uuids.timeBased(), tenantId, entityId);
eventDao.saveAsync(event.toData()).get(1, TimeUnit.MINUTES);
Thread.sleep(2);
RuleNodeDebugEventEntity event2 = getDebugInEventEntity(Uuids.timeBased(), tenantId, entityId);
eventDao.saveAsync(event2.toData()).get(1, TimeUnit.MINUTES);
RuleNodeDebugEventEntity foundEvent = ruleNodeDebugEventRepository.findLatestDebugRuleNodeInEvent(tenantId, entityId).get();
assertNotNull("Events expected to be not null", foundEvent);
assertEquals(event2.getEventType(), foundEvent.getEventType());
assertEquals(event2.getId(), foundEvent.getId());
}
private Event getStatsEvent(UUID eventId, UUID tenantId, UUID entityId) {
StatisticsEvent.StatisticsEventBuilder event = StatisticsEvent.builder();
event.id(eventId);
@ -117,4 +139,13 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest {
event.errorsOccurred(0);
return event.build();
}
private RuleNodeDebugEventEntity getDebugInEventEntity(UUID eventId, UUID tenantId, UUID entityId) {
RuleNodeDebugEventEntity event = new RuleNodeDebugEventEntity();
event.setId(eventId);
event.setEventType("IN");
event.setEntityId(entityId);
event.setTenantId(tenantId);
return event;
}
}

3
pom.xml

@ -72,7 +72,7 @@
<fasterxml-classmate.version>1.3.4</fasterxml-classmate.version>
<auth0-jwt.version>4.2.1</auth0-jwt.version>
<json-schema-validator.version>2.2.6</json-schema-validator.version>
<californium.version>3.10.0</californium.version>
<californium.version>3.11.0</californium.version>
<leshan.version>2.0.0-M14</leshan.version>
<gson.version>2.9.0</gson.version>
<freemarker.version>2.3.30</freemarker.version>
@ -830,7 +830,6 @@
<exclude>src/main/scripts/control/**</exclude>
<exclude>src/main/scripts/windows/**</exclude>
<exclude>src/main/resources/public/static/rulenode/**</exclude>
<exclude>src/main/java/org/eclipse/leshan/server/observation//**</exclude>
<exclude>**/*.proto.js</exclude>
<exclude>docker/haproxy/**</exclude>
<exclude>docker/tb-node/**</exclude>

2
rest-client/src/main/java/org/thingsboard/rest/client/utils/RestJsonConverter.java

@ -62,7 +62,7 @@ public class RestJsonConverter {
KvEntry entry = parseValue(key, ts.get(VALUE));
return new BasicTsKvEntry(ts.get(TS).asLong(), entry);
}
).collect(Collectors.toList()))
).toList())
);
return result;
} else {

14
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java

@ -87,24 +87,23 @@ public abstract class AbstractTbMsgPushNode<T extends BaseTbMsgPushNodeConfigura
Map<String, Object> entityBody = new HashMap<>();
JsonNode dataJson = JacksonUtil.toJsonNode(msg.getData());
switch (actionType) {
case ATTRIBUTES_UPDATED:
case POST_ATTRIBUTES:
case ATTRIBUTES_UPDATED, POST_ATTRIBUTES -> {
entityBody.put("kv", dataJson);
entityBody.put(SCOPE, getScope(metadata));
if (EdgeEventActionType.POST_ATTRIBUTES.equals(actionType)) {
entityBody.put("isPostAttributes", true);
}
break;
case ATTRIBUTES_DELETED:
}
case ATTRIBUTES_DELETED -> {
List<String> keys = JacksonUtil.convertValue(dataJson.get("attributes"), new TypeReference<>() {
});
entityBody.put("keys", keys);
entityBody.put(SCOPE, getScope(metadata));
break;
case TIMESERIES_UPDATED:
}
case TIMESERIES_UPDATED -> {
entityBody.put("data", dataJson);
entityBody.put("ts", msg.getMetaDataTs());
break;
}
}
return buildEvent(ctx.getTenantId(),
actionType,
@ -179,4 +178,5 @@ public abstract class AbstractTbMsgPushNode<T extends BaseTbMsgPushNodeConfigura
return msg.isTypeOneOf(POST_TELEMETRY_REQUEST, POST_ATTRIBUTES_REQUEST, ATTRIBUTES_UPDATED, ATTRIBUTES_DELETED, TIMESERIES_UPDATED,
ALARM, CONNECT_EVENT, DISCONNECT_EVENT, ACTIVITY_EVENT, INACTIVITY_EVENT, TO_SERVER_RPC_REQUEST);
}
}

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/BaseTbMsgPushNodeConfiguration.java

@ -30,4 +30,5 @@ public class BaseTbMsgPushNodeConfiguration implements NodeConfiguration<BaseTbM
configuration.setScope(AttributeScope.SERVER_SCOPE.name());
return configuration;
}
}

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToCloudNodeConfiguration.java

@ -17,7 +17,7 @@ package org.thingsboard.rule.engine.edge;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.AttributeScope;
@EqualsAndHashCode(callSuper = true)
@Data
@ -26,7 +26,8 @@ public class TbMsgPushToCloudNodeConfiguration extends BaseTbMsgPushNodeConfigur
@Override
public TbMsgPushToCloudNodeConfiguration defaultConfiguration() {
TbMsgPushToCloudNodeConfiguration configuration = new TbMsgPushToCloudNodeConfiguration();
configuration.setScope(DataConstants.SERVER_SCOPE);
configuration.setScope(AttributeScope.SERVER_SCOPE.name());
return configuration;
}
}

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNode.java

@ -155,4 +155,5 @@ public class TbMsgPushToEdgeNode extends AbstractTbMsgPushNode<TbMsgPushToEdgeNo
edgeEvent.setEdgeId(edgeId);
return ctx.getEdgeEventService().saveAsync(edgeEvent);
}
}

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNodeConfiguration.java

@ -17,7 +17,7 @@ package org.thingsboard.rule.engine.edge;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.AttributeScope;
@EqualsAndHashCode(callSuper = true)
@Data
@ -26,7 +26,8 @@ public class TbMsgPushToEdgeNodeConfiguration extends BaseTbMsgPushNodeConfigura
@Override
public TbMsgPushToEdgeNodeConfiguration defaultConfiguration() {
TbMsgPushToEdgeNodeConfiguration configuration = new TbMsgPushToEdgeNodeConfiguration();
configuration.setScope(DataConstants.SERVER_SCOPE);
configuration.setScope(AttributeScope.SERVER_SCOPE.name());
return configuration;
}
}

27
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java

@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
@ -46,7 +47,9 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
@RuleNode(type = ComponentType.ENRICHMENT,
name = "calculate delta", relationTypes = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE, TbNodeConnectionType.OTHER},
name = "calculate delta",
version = 1,
relationTypes = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE, TbNodeConnectionType.OTHER},
configClazz = CalculateDeltaNodeConfiguration.class,
nodeDescription = "Calculates delta and amount of time passed between previous timeseries key reading " +
"and current value for this key from the incoming message",
@ -101,6 +104,11 @@ public class CalculateDeltaNode implements TbNode {
return;
}
if (config.isExcludeZeroDeltas() && delta.doubleValue() == 0) {
ctx.tellSuccess(msg);
return;
}
if (config.getRound() != null) {
delta = delta.setScale(config.getRound(), RoundingMode.HALF_UP);
}
@ -128,6 +136,23 @@ public class CalculateDeltaNode implements TbNode {
}
}
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
boolean hasChanges = false;
switch (fromVersion) {
case 0:
String excludeZeroDeltas = "excludeZeroDeltas";
if (!oldConfiguration.has(excludeZeroDeltas)) {
hasChanges = true;
((ObjectNode) oldConfiguration).put(excludeZeroDeltas, false);
}
break;
default:
break;
}
return new TbPair<>(hasChanges, oldConfiguration);
}
private ListenableFuture<ValueWithTs> fetchLatestValueAsync(EntityId entityId) {
return Futures.transform(timeseriesService.findLatest(ctx.getTenantId(), entityId, Collections.singletonList(config.getInputValueKey())),
list -> extractValue(list.get(0))

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java

@ -30,6 +30,7 @@ public class CalculateDeltaNodeConfiguration implements NodeConfiguration<Calcul
private String periodValueKey;
private Integer round;
private boolean tellFailureIfDeltaIsNegative;
private boolean excludeZeroDeltas;
@Override
public CalculateDeltaNodeConfiguration defaultConfiguration() {
@ -40,6 +41,7 @@ public class CalculateDeltaNodeConfiguration implements NodeConfiguration<Calcul
configuration.setAddPeriodBetweenMsgs(false);
configuration.setPeriodValueKey("periodInMs");
configuration.setTellFailureIfDeltaIsNegative(true);
configuration.setExcludeZeroDeltas(false);
return configuration;
}

30
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java

@ -15,11 +15,14 @@
*/
package org.thingsboard.rule.engine.mqtt;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.netty.handler.ssl.SslContext;
import io.netty.util.concurrent.Promise;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.mqtt.MqttClient;
import org.thingsboard.mqtt.MqttClientConfig;
import org.thingsboard.mqtt.MqttConnectResult;
@ -35,6 +38,7 @@ import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.plugin.ComponentClusteringMode;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -81,7 +85,8 @@ public class TbMqttNode extends TbAbstractExternalNode {
public void onMsg(TbContext ctx, TbMsg msg) {
String topic = TbNodeUtils.processPattern(this.mqttNodeConfiguration.getTopicPattern(), msg);
var tbMsg = ackIfNeeded(ctx, msg);
this.mqttClient.publish(topic, Unpooled.wrappedBuffer(tbMsg.getData().getBytes(UTF8)), MqttQoS.AT_LEAST_ONCE, mqttNodeConfiguration.isRetainedMessage())
this.mqttClient.publish(topic, Unpooled.wrappedBuffer(getData(tbMsg, mqttNodeConfiguration.isParseToPlainText()).getBytes(UTF8)),
MqttQoS.AT_LEAST_ONCE, mqttNodeConfiguration.isRetainedMessage())
.addListener(future -> {
if (future.isSuccess()) {
tellSuccess(ctx, tbMsg);
@ -153,4 +158,27 @@ public class TbMqttNode extends TbAbstractExternalNode {
return this.mqttNodeConfiguration.isSsl() ? this.mqttNodeConfiguration.getCredentials().initSslContext() : null;
}
private String getData(TbMsg tbMsg, boolean parseToPlainText) {
if (parseToPlainText) {
return JacksonUtil.toPlainText(tbMsg.getData());
}
return tbMsg.getData();
}
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
boolean hasChanges = false;
switch (fromVersion) {
case 0:
String parseToPlainText = "parseToPlainText";
if (!oldConfiguration.has(parseToPlainText)) {
hasChanges = true;
((ObjectNode) oldConfiguration).put(parseToPlainText, false);
}
break;
default:
break;
}
return new TbPair<>(hasChanges, oldConfiguration);
}
}

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java

@ -33,6 +33,7 @@ public class TbMqttNodeConfiguration implements NodeConfiguration<TbMqttNodeConf
private boolean cleanSession;
private boolean ssl;
private boolean parseToPlainText;
private ClientCredentials credentials;
@Override
@ -44,6 +45,7 @@ public class TbMqttNodeConfiguration implements NodeConfiguration<TbMqttNodeConf
configuration.setCleanSession(true);
configuration.setSsl(false);
configuration.setRetainedMessage(false);
configuration.setParseToPlainText(false);
configuration.setCredentials(new AnonymousCredentials());
return configuration;
}

14
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java

@ -238,23 +238,11 @@ public class TbHttpClient {
private String getData(TbMsg tbMsg, boolean ignoreBody, boolean parseToPlainText) {
if (!ignoreBody && parseToPlainText) {
return parseJsonStringToPlainText(tbMsg.getData());
return JacksonUtil.toPlainText(tbMsg.getData());
}
return tbMsg.getData();
}
protected String parseJsonStringToPlainText(String data) {
if (data.startsWith("\"") && data.endsWith("\"") && data.length() >= 2) {
final String dataBefore = data;
try {
data = JacksonUtil.fromString(data, String.class);
} catch (Exception ignored) {}
log.trace("Trimming double quotes. Before trim: [{}], after trim: [{}]", dataBefore, data);
}
return data;
}
private TbMsg processResponse(TbContext ctx, TbMsg origMsg, ResponseEntity<String> response) {
TbMsgMetaData metaData = origMsg.getMetaData();
HttpStatus httpStatus = (HttpStatus) response.getStatusCode();

120
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java

@ -16,19 +16,26 @@
package org.thingsboard.rule.engine.metadata;
import com.google.common.util.concurrent.Futures;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.id.DeviceId;
@ -48,7 +55,10 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
@ -63,10 +73,11 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class CalculateDeltaNodeTest {
public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
@ -75,13 +86,13 @@ public class CalculateDeltaNodeTest {
private TbContext ctxMock;
@Mock
private TimeseriesService timeseriesServiceMock;
@Spy
private CalculateDeltaNode node;
private CalculateDeltaNodeConfiguration config;
private TbNodeConfiguration nodeConfiguration;
@BeforeEach
public void setUp() throws TbNodeException {
node = new CalculateDeltaNode();
config = new CalculateDeltaNodeConfiguration().defaultConfiguration();
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock);
@ -424,6 +435,91 @@ public class CalculateDeltaNodeTest {
.hasMessage("Calculation failed. JSON values are not supported!");
}
@ParameterizedTest
@MethodSource("CalculateDeltaTestConfig")
public void givenCalculateDeltaConfig_whenOnMsg_thenVerify(CalculateDeltaTestConfig testConfig) throws TbNodeException {
// GIVEN
config.setTellFailureIfDeltaIsNegative(testConfig.isTellFailureIfDeltaIsNegative());
config.setExcludeZeroDeltas(testConfig.isExcludeZeroDeltas());
config.setInputValueKey("temperature");
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
mockFindLatest(new BasicTsKvEntry(1L, new DoubleDataEntry("temperature", testConfig.getPrevValue())));
var msgData = "{\"temperature\":" + testConfig.getCurrentValue() + ",\"airPressure\":123}";
var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
testConfig.getVerificationMethod().accept(ctxMock, msg);
}
private static Stream<CalculateDeltaTestConfig> CalculateDeltaTestConfig() {
return Stream.of(
// delta = 0, tell failure if delta is negative is set to true and exclude zero deltas is set to true so delta should filter out the message.
new CalculateDeltaTestConfig(true, true, 40, 40, (ctx, msg) -> {
verify(ctx).tellSuccess(eq(msg));
verify(ctx).getDbCallbackExecutor();
verifyNoMoreInteractions(ctx);
}),
// delta < 0, tell failure if delta is negative is set to true so it should throw exception.
new CalculateDeltaTestConfig(true, true, 41, 40, (ctx, msg) -> {
var errorCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctx).tellFailure(eq(msg), errorCaptor.capture());
verify(ctx).getDbCallbackExecutor();
verifyNoMoreInteractions(ctx);
assertThat(errorCaptor.getValue()).isInstanceOf(IllegalArgumentException.class).hasMessage("Delta value is negative!");
}),
// delta < 0, exclude zero deltas is set to true so it should return message with delta if delta is negative is set to false.
new CalculateDeltaTestConfig(false, true, 41, 40, (ctx, msg) -> {
var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctx).tellSuccess(actualMsgCaptor.capture());
verify(ctx).getDbCallbackExecutor();
verifyNoMoreInteractions(ctx);
String expectedMsgData = "{\"temperature\":40.0,\"airPressure\":123,\"delta\":-1}";
assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData());
}),
// delta = 0, tell failure if delta is negative is set to false and exclude zero deltas is set to true so delta should filter out the message.
new CalculateDeltaTestConfig(false, true, 40, 40, (ctx, msg) -> {
verify(ctx).tellSuccess(eq(msg));
verify(ctx).getDbCallbackExecutor();
verifyNoMoreInteractions(ctx);
}),
// delta > 0, exclude zero deltas is set to true so it should return message with delta.
new CalculateDeltaTestConfig(false, true, 39, 40, (ctx, msg) -> {
var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctx).tellSuccess(actualMsgCaptor.capture());
verify(ctx).getDbCallbackExecutor();
verifyNoMoreInteractions(ctx);
String expectedMsgData = "{\"temperature\":40.0,\"airPressure\":123,\"delta\":1}";
assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData());
}),
// delta > 0, exclude zero deltas is set to false so it should return message with delta.
new CalculateDeltaTestConfig(false, false, 39, 40, (ctx, msg) -> {
var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctx).tellSuccess(actualMsgCaptor.capture());
verify(ctx).getDbCallbackExecutor();
verifyNoMoreInteractions(ctx);
String expectedMsgData = "{\"temperature\":40.0,\"airPressure\":123,\"delta\":1}";
assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData());
})
);
}
@Data
@RequiredArgsConstructor
private static class CalculateDeltaTestConfig {
private final boolean tellFailureIfDeltaIsNegative;
private final boolean excludeZeroDeltas;
private final double prevValue;
private final double currentValue;
private final BiConsumer<TbContext, TbMsg> verificationMethod;
}
private void mockFindLatest(TsKvEntry tsKvEntry) {
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(timeseriesServiceMock.findLatestSync(
@ -457,4 +553,24 @@ public class CalculateDeltaNodeTest {
}
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// default config for version 0
Arguments.of(0,
"{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true}",
true,
"{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"excludeZeroDeltas\":false}"),
// default config for version 1 with upgrade from version 0
Arguments.of(1,
"{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"excludeZeroDeltas\":false}",
false,
"{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"excludeZeroDeltas\":false}")
);
}
@Override
protected TbNode getTestNode() {
return node;
}
}

60
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java

@ -0,0 +1,60 @@
/**
* Copyright © 2016-2024 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.rule.engine.mqtt;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.provider.Arguments;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.api.TbNode;
import java.util.stream.Stream;
import static org.mockito.Mockito.mock;
@ExtendWith(MockitoExtension.class)
class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest {
@Spy
TbMqttNode node;
@BeforeEach
public void setUp() throws Exception {
node = mock(TbMqttNode.class);
}
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// default config for version 0
Arguments.of(0,
"{\"topicPattern\":\"my-topic\",\"port\":1883,\"connectTimeoutSec\":10,\"cleanSession\":true, \"ssl\":false, \"retainedMessage\":false,\"credentials\":{\"type\":\"anonymous\"}}",
true,
"{\"topicPattern\":\"my-topic\",\"port\":1883,\"connectTimeoutSec\":10,\"cleanSession\":true, \"ssl\":false, \"retainedMessage\":false,\"credentials\":{\"type\":\"anonymous\"},\"parseToPlainText\":false}"),
// default config for version 1 with upgrade from version 0
Arguments.of(1,
"{\"topicPattern\":\"my-topic\",\"port\":1883,\"connectTimeoutSec\":10,\"cleanSession\":true, \"ssl\":false, \"retainedMessage\":false,\"credentials\":{\"type\":\"anonymous\"},\"parseToPlainText\":false}",
false,
"{\"topicPattern\":\"my-topic\",\"port\":1883,\"connectTimeoutSec\":10,\"cleanSession\":true, \"ssl\":false, \"retainedMessage\":false,\"credentials\":{\"type\":\"anonymous\"},\"parseToPlainText\":false}")
);
}
@Override
protected TbNode getTestNode() {
return node;
}
}

16
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java

@ -22,13 +22,10 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.mockserver.integration.ClientAndServer;
import org.springframework.util.LinkedMultiValueMap;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
@ -47,7 +44,6 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.willCallRealMethod;
import static org.mockito.Mockito.mock;
@ -222,16 +218,4 @@ public class TbHttpClientTest {
Assertions.assertEquals(data.get("Set-Cookie"), "[\"sap-context=sap-client=075; path=/\",\"sap-token=sap-client=075; path=/\"]");
}
@ParameterizedTest
@ValueSource(strings = { "false", "\"", "\"\"", "\"This is a string with double quotes\"", "Path: /home/developer/test.txt",
"First line\nSecond line\n\nFourth line", "Before\rAfter", "Tab\tSeparated\tValues", "Test\bbackspace", "[]",
"[1, 2, 3]", "{\"key\": \"value\"}", "{\n\"temperature\": 25.5,\n\"humidity\": 50.2\n\"}", "Expression: (a + b) * c",
"世界", "Україна", "\u1F1FA\u1F1E6", "🇺🇦"})
public void testParseJsonStringToPlainText(String original) {
Mockito.when(client.parseJsonStringToPlainText(anyString())).thenCallRealMethod();
String serialized = JacksonUtil.toString(original);
Assertions.assertNotNull(serialized);
Assertions.assertEquals(original, client.parseJsonStringToPlainText(serialized));
}
}

296
transport/lwm2m/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java

@ -1,296 +0,0 @@
/**
* Copyright © 2016-2024 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.
*
* Copyright (c) 2016 Sierra Wireless and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v20.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.html.
*
* Contributors:
* Sierra Wireless - initial API and implementation
* Michał Wadowski (Orange) - Add Observe-Composite feature.
*/
package org.eclipse.leshan.server.observation;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.observation.CompositeObservation;
import org.eclipse.leshan.core.observation.Observation;
import org.eclipse.leshan.core.observation.SingleObservation;
import org.eclipse.leshan.core.peer.LwM2mPeer;
import org.eclipse.leshan.core.response.ObserveCompositeResponse;
import org.eclipse.leshan.core.response.ObserveResponse;
import org.eclipse.leshan.server.endpoint.LwM2mServerEndpoint;
import org.eclipse.leshan.server.endpoint.LwM2mServerEndpointsProvider;
import org.eclipse.leshan.server.profile.ClientProfile;
import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.registration.RegistrationStore;
import org.eclipse.leshan.server.registration.RegistrationUpdate;
import org.eclipse.leshan.server.registration.UpdatedRegistration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Implementation of the {@link ObservationService} accessing the persisted observation via the provided
* {@link RegistrationStore}.
*
* When a new observation is added or changed or canceled, the registered listeners are notified.
*/
@Slf4j
public class ObservationServiceImpl implements ObservationService, LwM2mNotificationReceiver {
private final Logger LOG = LoggerFactory.getLogger(ObservationServiceImpl.class);
private final RegistrationStore registrationStore;
private final LwM2mServerEndpointsProvider endpointProvider;
private final boolean updateRegistrationOnNotification;
private final List<ObservationListener> listeners = new CopyOnWriteArrayList<>();;
/**
* Creates an instance of {@link ObservationServiceImpl}
*/
public ObservationServiceImpl(RegistrationStore store, LwM2mServerEndpointsProvider endpointProvider) {
this(store, endpointProvider, false);
}
/**
* Creates an instance of {@link ObservationServiceImpl}
*
* @param updateRegistrationOnNotification will activate registration update on observe notification.
*
* @since 1.1
*/
public ObservationServiceImpl(RegistrationStore store, LwM2mServerEndpointsProvider endpointProvider,
boolean updateRegistrationOnNotification) {
this.registrationStore = store;
this.updateRegistrationOnNotification = updateRegistrationOnNotification;
this.endpointProvider = endpointProvider;
}
@Override
public int cancelObservations(Registration registration) {
// check registration id
String registrationId = registration.getId();
if (registrationId == null)
return 0;
Collection<Observation> observations = registrationStore.removeObservations(registrationId);
if (observations == null)
return 0;
for (Observation observation : observations) {
cancel(observation);
}
return observations.size();
}
@Override
public int cancelObservations(Registration registration, String nodePath) {
if (registration == null || registration.getId() == null || nodePath == null || nodePath.isEmpty())
return 0;
Set<Observation> observations = getObservationsForCancel(registration.getId(), nodePath);
for (Observation observation : observations) {
cancelObservation(observation);
}
return observations.size();
}
@Override
public int cancelCompositeObservations(Registration registration, String[] nodePaths) {
if (registration == null || registration.getId() == null || nodePaths == null || nodePaths.length == 0)
return 0;
Set<Observation> observations = getCompositeObservationsForCancel(registration.getId(), nodePaths);
for (Observation observation : observations) {
cancelObservation(observation);
}
return observations.size();
}
@Override
public void cancelObservation(Observation observation) {
if (observation == null)
return;
registrationStore.removeObservation(observation.getRegistrationId(), observation.getId());
cancel(observation);
}
private void cancel(Observation observation) {
List<LwM2mServerEndpoint> endpoints = endpointProvider.getEndpoints();
for (LwM2mServerEndpoint lwM2mEndpoint : endpoints) {
lwM2mEndpoint.cancelObservation(observation);
}
for (ObservationListener listener : listeners) {
listener.cancelled(observation);
}
}
@Override
public Set<Observation> getObservations(Registration registration) {
return getObservations(registration.getId());
}
private Set<Observation> getObservations(String registrationId) {
if (registrationId == null)
return Collections.emptySet();
return new HashSet<>(registrationStore.getObservations(registrationId));
}
private Set<Observation> getCompositeObservationsForCancel(String registrationId, String[] nodePaths) {
if (registrationId == null || nodePaths == null)
return Collections.emptySet();
// array of String to array of LWM2M path
List<LwM2mPath> lwPaths = new ArrayList<>(nodePaths.length);
for (int i = 0; i < nodePaths.length; i++) {
lwPaths.add(new LwM2mPath(nodePaths[i]));
}
// search composite-observation
Set<Observation> result = new HashSet<>();
for (Observation obs : getObservations(registrationId)) {
if (obs instanceof CompositeObservation) {
if (lwPaths.equals(((CompositeObservation) obs).getPaths())) {
result.add(obs);
}
}
}
return result;
}
private Set<Observation> getObservationsForCancel(String registrationId, String nodePath) {
if (registrationId == null || nodePath == null)
return Collections.emptySet();
Set<Observation> result = new HashSet<>();
LwM2mPath lwPath = new LwM2mPath(nodePath);
for (Observation obs : getObservations(registrationId)) {
if (obs instanceof SingleObservation) {
LwM2mPath lwPathObs = ((SingleObservation) obs).getPath();
if (lwPath.equals(lwPathObs) || lwPathObs.startWith(lwPath)) { // nodePath = "3", lwPathObs = "3/0/9": cancel for tne all lwPathObs
result.add(obs);
} else if (!lwPath.equals(lwPathObs) && lwPath.startWith(lwPathObs)) { // nodePath = "3/0/9", lwPathObs = "3": error...
String errorMsg = String.format(
"Unexpected error <cancelObservation>: There is registration with id [%s] existing observation [%s] includes input observation [%s]!",
registrationId, lwPathObs, lwPath);
throw new IllegalStateException(errorMsg);
}
}
}
return result;
}
@Override
public void addListener(ObservationListener listener) {
listeners.add(listener);
}
@Override
public void removeListener(ObservationListener listener) {
listeners.remove(listener);
}
private Registration updateRegistrationOnRegistration(Observation observation, LwM2mPeer sender,
ClientProfile profile) {
if (updateRegistrationOnNotification) {
RegistrationUpdate regUpdate = new RegistrationUpdate(observation.getRegistrationId(), sender, null, null,
null, null, null, null, null, null, null, null);
UpdatedRegistration updatedRegistration = registrationStore.updateRegistration(regUpdate);
if (updatedRegistration == null || updatedRegistration.getUpdatedRegistration() == null) {
String errorMsg = String.format(
"Unexpected error: There is no registration with id %s for this observation %s",
observation.getRegistrationId(), observation);
LOG.error(errorMsg);
throw new IllegalStateException(errorMsg);
}
return updatedRegistration.getUpdatedRegistration();
}
return profile.getRegistration();
}
// ********** NotificationListener interface **********//
@Override
public void onNotification(SingleObservation observation, LwM2mPeer sender, ClientProfile profile,
ObserveResponse response) {
try {
Registration updatedRegistration = updateRegistrationOnRegistration(observation, sender, profile);
for (ObservationListener listener : listeners) {
listener.onResponse(observation, updatedRegistration, response);
}
} catch (Exception e) {
for (ObservationListener listener : listeners) {
listener.onError(observation, profile.getRegistration(), e);
}
}
}
@Override
public void onNotification(CompositeObservation observation, LwM2mPeer sender, ClientProfile profile,
ObserveCompositeResponse response) {
try {
Registration updatedRegistration = updateRegistrationOnRegistration(observation, sender, profile);
for (ObservationListener listener : listeners) {
listener.onResponse(observation, updatedRegistration, response);
}
} catch (Exception e) {
for (ObservationListener listener : listeners) {
listener.onError(observation, profile.getRegistration(), e);
}
}
}
@Override
public void onError(Observation observation, LwM2mPeer sender, ClientProfile profile, Exception error) {
for (ObservationListener listener : listeners) {
listener.onError(observation, profile.getRegistration(), error);
}
}
@Override
public void newObservation(Observation observation, Registration registration) {
for (ObservationListener listener : listeners) {
listener.newObservation(observation, registration);
}
}
@Override
public void cancelled(Observation observation) {
for (ObservationListener listener : listeners) {
listener.cancelled(observation);
}
}
}
Loading…
Cancel
Save