thingsboard-developers
Changes
application/pom.xml 57(+9 -48)
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java 449(+310 -139)
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorToRuleEngineMsg.java 30(+13 -17)
application/src/main/java/org/thingsboard/server/actors/device/PendingSessionMsgData.java 40(+40 -0)
application/src/main/java/org/thingsboard/server/actors/device/RuleEngineQueuePutAckMsg.java 28(+14 -14)
application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java 4(+2 -2)
application/src/main/java/org/thingsboard/server/actors/device/ToServerRpcRequestMetadata.java 21(+10 -11)
application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java 251(+0 -251)
application/src/main/java/org/thingsboard/server/actors/plugin/PluginCallbackMessage.java 53(+0 -53)
application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java 546(+0 -546)
application/src/main/java/org/thingsboard/server/actors/plugin/RuleToPluginMsgWrapper.java 66(+0 -66)
application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java 141(+0 -141)
application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java 110(+7 -103)
application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionCreateRequestMsg.java 2(+1 -1)
application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingContext.java 117(+0 -117)
application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java 354(+0 -354)
application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingContext.java 115(+0 -115)
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java 241(+241 -0)
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java 297(+297 -0)
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java 57(+57 -0)
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleChainMsg.java 40(+40 -0)
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java 38(+38 -0)
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java 121(+121 -0)
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java 41(+41 -0)
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfErrorMsg.java 37(+37 -0)
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfMsg.java 30(+13 -17)
application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java 184(+91 -93)
application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java 59(+31 -28)
application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java 29(+19 -10)
application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java 52(+0 -52)
application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java 66(+57 -9)
application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java 59(+59 -0)
application/src/main/java/org/thingsboard/server/actors/shared/rulechain/SystemRuleChainManager.java 23(+13 -10)
application/src/main/java/org/thingsboard/server/actors/shared/rulechain/TenantRuleChainManager.java 21(+10 -11)
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java 10(+6 -4)
application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java 23(+17 -6)
application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java 14(+10 -4)
application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java 106(+0 -106)
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java 103(+35 -68)
application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java 4(+2 -2)
application/src/main/java/org/thingsboard/server/service/cluster/discovery/DummyDiscoveryService.java 1(+0 -1)
application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstance.java 14(+8 -6)
application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java 26(+18 -8)
application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java 2(+0 -2)
application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java 5(+2 -3)
application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterGrpcService.java 196(+41 -155)
application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterRpcService.java 25(+7 -18)
application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSessionListener.java 15(+1 -14)
application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcSessionCreationFuture.java 63(+0 -63)
application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java 158(+89 -69)
application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java 5(+3 -2)
application/src/main/java/org/thingsboard/server/service/encoding/DataDecodingEncodingService.java 21(+12 -9)
application/src/main/java/org/thingsboard/server/service/encoding/ProtoWithJavaSerializationDecodingEncodingService.java 67(+67 -0)
application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java 63(+63 -0)
application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java 17(+11 -6)
application/src/main/java/org/thingsboard/server/service/executors/ExternalCallExecutorService.java 18(+12 -6)
application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseSchemaService.java 10(+4 -6)
application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java 31(+24 -7)
application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java 17(+15 -2)
application/src/main/java/org/thingsboard/server/service/install/DefaultDataUpdateService.java 106(+106 -0)
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java 169(+14 -155)
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseSchemaService.java 10(+5 -5)
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java 29(+21 -8)
application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java 4(+0 -4)
application/src/main/java/org/thingsboard/server/service/queue/DefaultMsgQueueService.java 111(+111 -0)
application/src/main/java/org/thingsboard/server/service/rpc/DefaultDeviceRpcService.java 152(+152 -0)
application/src/main/java/org/thingsboard/server/service/rpc/ToDeviceRpcRequestActorMsg.java 30(+12 -18)
application/src/main/java/org/thingsboard/server/service/rpc/ToServerRpcResponseActorMsg.java 27(+21 -6)
application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java 159(+159 -0)
application/src/main/java/org/thingsboard/server/service/script/NashornJsSandboxService.java 58(+58 -0)
application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java 181(+181 -0)
application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptFactory.java 53(+53 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java 8(+0 -8)
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java 1(+0 -1)
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java 6(+4 -2)
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java 2(+0 -2)
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java 7(+5 -2)
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java 2(+0 -2)
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java 2(+0 -2)
application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java 8(+7 -1)
application/src/main/java/org/thingsboard/server/service/security/ValidationCallback.java 36(+17 -19)
application/src/main/java/org/thingsboard/server/service/security/ValidationResultCode.java 5(+4 -1)
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java 386(+386 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/AttributesSubscriptionCmd.java 14(+8 -6)
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/GetHistoryCmd.java 40(+40 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/SubscriptionCmd.java 42(+42 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmd.java 10(+7 -3)
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java 58(+58 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TimeseriesSubscriptionCmd.java 43(+17 -26)
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java 710(+710 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java 560(+560 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/exception/AccessDeniedException.java 2(+1 -1)
application/src/main/java/org/thingsboard/server/service/telemetry/exception/EntityNotFoundException.java 2(+1 -1)
application/src/main/java/org/thingsboard/server/service/telemetry/exception/InternalErrorException.java 2(+1 -1)
application/src/main/java/org/thingsboard/server/service/telemetry/exception/InvalidParametersException.java 2(+1 -1)
application/src/main/java/org/thingsboard/server/service/telemetry/exception/ToErrorResponseEntity.java 2(+1 -1)
application/src/main/java/org/thingsboard/server/service/telemetry/exception/UnauthorizedException.java 2(+1 -1)
application/src/main/java/org/thingsboard/server/service/telemetry/exception/UncheckedApiException.java 2(+1 -1)
application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionErrorCode.java 50(+50 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionState.java 70(+70 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionUpdate.java 99(+99 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java 47(+47 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java 13(+7 -6)
application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java 14(+8 -6)
application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java 68(+68 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketTextMsg.java 12(+5 -7)
application/src/main/proto/cluster.proto 129(+81 -48)
application/src/main/resources/thingsboard.yml 107(+85 -22)
application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java 84(+84 -0)
application/src/test/java/org/thingsboard/server/controller/BaseComponentDescriptorControllerTest.java 23(+6 -17)
application/src/test/java/org/thingsboard/server/controller/BasePluginControllerTest.java 232(+0 -232)
application/src/test/java/org/thingsboard/server/controller/BaseRuleControllerTest.java 247(+0 -247)
application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java 33(+15 -18)
application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java 1(+0 -1)
application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java 322(+322 -0)
application/src/test/java/org/thingsboard/server/rules/flow/nosql/RuleEngineFlowNoSqlIntegrationTest.java 8(+4 -4)
application/src/test/java/org/thingsboard/server/rules/flow/sql/RuleEngineFlowSqlIntegrationTest.java 9(+5 -4)
application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java 234(+234 -0)
application/src/test/java/org/thingsboard/server/rules/lifecycle/nosql/RuleEngineLifecycleNoSqlIntegrationTest.java 8(+4 -4)
application/src/test/java/org/thingsboard/server/rules/lifecycle/sql/RuleEngineLifecycleSqlIntegrationTest.java 9(+5 -4)
application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java 171(+171 -0)
application/src/test/java/org/thingsboard/server/service/script/TestNashornJsSandboxService.java 62(+62 -0)
common/data/pom.xml 2(+1 -1)
common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java 2(+1 -1)
common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardException.java 2(+1 -1)
common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java 4(+3 -1)
common/data/src/main/java/org/thingsboard/server/common/data/rpc/ToDeviceRpcRequestBody.java 2(+1 -1)
common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java 19(+9 -10)
common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java 29(+11 -18)
common/message/pom.xml 11(+10 -1)
common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ClusterEventMsg.java 8(+7 -1)
common/message/src/main/java/org/thingsboard/server/common/msg/cluster/SendToClusterMsg.java 28(+16 -12)
common/message/src/main/java/org/thingsboard/server/common/msg/core/ActorSystemToDeviceSessionActorMsg.java 3(+2 -1)
common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesSubscribeMsg.java 6(+3 -3)
common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUnsubscribeMsg.java 7(+4 -3)
common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUpdateNotification.java 8(+4 -4)
common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUpdateRequest.java 2(+1 -1)
common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicActorSystemToDeviceSessionActorMsg.java 11(+8 -3)
common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicAttributesUpdateRequest.java 14(+7 -7)
common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicCommandAckResponse.java 13(+7 -6)
common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java 7(+4 -3)
common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesResponse.java 11(+6 -5)
common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicStatusCodeResponse.java 13(+7 -6)
common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicTelemetryUploadRequest.java 7(+4 -3)
common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineErrorMsg.java 30(+11 -19)
common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java 8(+4 -4)
common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcRequestMsg.java 8(+4 -4)
common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcResponseMsg.java 7(+4 -3)
common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java 6(+3 -3)
common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcResponseMsg.java 8(+4 -4)
common/message/src/main/java/org/thingsboard/server/common/msg/device/BasicDeviceToDeviceActorMsg.java 22(+14 -8)
common/message/src/main/java/org/thingsboard/server/common/msg/device/DeviceToDeviceActorMsg.java 5(+3 -2)
common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java 37(+15 -22)
common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicTransportToDeviceSessionActorMsg.java 18(+8 -10)
common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java 5(+5 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/TransportToDeviceSessionActorMsg.java 3(+2 -1)
common/message/src/main/java/org/thingsboard/server/common/msg/system/ServiceToRuleEngineMsg.java 24(+15 -9)
common/message/src/main/java/org/thingsboard/server/common/msg/timeout/DeviceActorClientSideRpcTimeoutMsg.java 17(+10 -7)
common/message/src/main/java/org/thingsboard/server/common/msg/timeout/DeviceActorQueueTimeoutMsg.java 13(+10 -3)
common/message/src/main/java/org/thingsboard/server/common/msg/timeout/DeviceActorServerSideRpcTimeoutMsg.java 17(+10 -7)
common/message/src/main/proto/tbmsg.proto 45(+45 -0)
common/pom.xml 2(+1 -1)
common/transport/pom.xml 2(+1 -1)
common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java 15(+11 -4)
common/transport/src/main/java/org/thingsboard/server/common/transport/quota/AbstractQuotaService.java 21(+6 -15)
common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostIntervalRegistryCleaner.java 20(+9 -11)
common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostIntervalRegistryLogger.java 52(+52 -0)
common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostRequestIntervalRegistry.java 37(+37 -0)
common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostRequestLimitPolicy.java 13(+4 -9)
common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostRequestsQuotaService.java 37(+37 -0)
common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryCleaner.java 10(+3 -7)
common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java 24(+4 -20)
common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/KeyBasedIntervalRegistry.java 25(+7 -18)
common/transport/src/main/java/org/thingsboard/server/common/transport/quota/RequestLimitPolicy.java 16(+10 -6)
common/transport/src/main/java/org/thingsboard/server/common/transport/quota/tenant/TenantIntervalRegistryCleaner.java 17(+9 -8)
common/transport/src/main/java/org/thingsboard/server/common/transport/quota/tenant/TenantIntervalRegistryLogger.java 52(+52 -0)
common/transport/src/main/java/org/thingsboard/server/common/transport/quota/tenant/TenantMsgsIntervalRegistry.java 22(+11 -11)
common/transport/src/main/java/org/thingsboard/server/common/transport/quota/tenant/TenantQuotaService.java 23(+10 -13)
common/transport/src/main/java/org/thingsboard/server/common/transport/quota/tenant/TenantRequestLimitPolicy.java 16(+9 -7)
common/transport/src/main/java/org/thingsboard/server/common/transport/TransportAdaptor.java 4(+2 -2)
common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestLimitPolicyTest.java 1(+1 -0)
common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaServiceTest.java 8(+3 -5)
common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistryTest.java 4(+1 -3)
common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLoggerTest.java 4(+3 -1)
dao/pom.xml 25(+10 -15)
dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java 1(+0 -1)
dao/src/main/resources/cassandra/schema.cql 144(+83 -61)
dao/src/main/resources/sql/schema.sql 52(+24 -28)
dao/src/main/resources/sql/system-data.sql 20(+0 -20)
dao/src/test/java/org/thingsboard/server/dao/service/nosql/AdminSettingsServiceNoSqlTest.java 2(+1 -1)
dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialCacheNoSqlTest.java 2(+1 -1)
dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialServiceNoSqlTest.java 2(+1 -1)
dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetsBundleServiceNoSqlTest.java 2(+1 -1)
dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsCacheSqlTest.java 2(+1 -1)
dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsServiceSqlTest.java 2(+1 -1)
dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java 10(+9 -1)
dao/src/test/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDaoTest.java 8(+4 -4)
docker/cassandra/Makefile 2(+1 -1)
docker/cassandra-setup/Makefile 2(+1 -1)
docker/cluster-mode-thirdparty.yml 45(+45 -0)
docker/docker-compose.yml 2(+1 -1)
docker/k8s/cassandra.yaml 2(+1 -1)
docker/k8s/cassandra-setup.yaml 2(+1 -1)
docker/k8s/tb.yaml 2(+1 -1)
docker/k8s/zookeeper.yaml 2(+1 -1)
docker/tb/Makefile 2(+1 -1)
docker/zookeeper/Makefile 2(+1 -1)
extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/action/KafkaActionMsg.java 28(+0 -28)
extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/action/KafkaActionPayload.java 34(+0 -34)
extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/action/KafkaPluginAction.java 45(+0 -45)
extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/action/KafkaPluginActionConfiguration.java 26(+0 -26)
extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/plugin/KafkaMsgHandler.java 63(+0 -63)
extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/plugin/KafkaPlugin.java 94(+0 -94)
extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/plugin/KafkaPluginConfiguration.java 34(+0 -34)
extensions/extension-kafka/src/test/java/org/thingsboard/server/extensions/kafka/KafkaDemoClient.java 131(+0 -131)
extensions/extension-mqtt/src/main/java/org/thingsboard/server/extensions/mqtt/action/MqttActionMsg.java 28(+0 -28)
extensions/extension-mqtt/src/main/java/org/thingsboard/server/extensions/mqtt/action/MqttActionPayload.java 34(+0 -34)
extensions/extension-mqtt/src/main/java/org/thingsboard/server/extensions/mqtt/action/MqttPluginAction.java 43(+0 -43)
extensions/extension-mqtt/src/main/java/org/thingsboard/server/extensions/mqtt/action/MqttPluginActionConfiguration.java 26(+0 -26)
extensions/extension-mqtt/src/main/java/org/thingsboard/server/extensions/mqtt/plugin/MqttMsgHandler.java 70(+0 -70)
extensions/extension-mqtt/src/main/java/org/thingsboard/server/extensions/mqtt/plugin/MqttPlugin.java 130(+0 -130)
extensions/extension-mqtt/src/main/java/org/thingsboard/server/extensions/mqtt/plugin/MqttPluginConfiguration.java 28(+0 -28)
extensions/extension-rabbitmq/src/main/java/org/thingsboard/server/extensions/rabbitmq/action/RabbitMqActionMsg.java 31(+0 -31)
extensions/extension-rabbitmq/src/main/java/org/thingsboard/server/extensions/rabbitmq/action/RabbitMqActionPayload.java 39(+0 -39)
extensions/extension-rabbitmq/src/main/java/org/thingsboard/server/extensions/rabbitmq/action/RabbitMqPluginAction.java 49(+0 -49)
extensions/extension-rabbitmq/src/main/java/org/thingsboard/server/extensions/rabbitmq/action/RabbitMqPluginActionConfiguration.java 32(+0 -32)
extensions/extension-rabbitmq/src/main/java/org/thingsboard/server/extensions/rabbitmq/plugin/RabbitMqMsgHandler.java 86(+0 -86)
extensions/extension-rabbitmq/src/main/java/org/thingsboard/server/extensions/rabbitmq/plugin/RabbitMqPlugin.java 109(+0 -109)
extensions/extension-rabbitmq/src/main/java/org/thingsboard/server/extensions/rabbitmq/plugin/RabbitMqPluginConfiguration.java 47(+0 -47)
extensions/extension-rabbitmq/src/test/java/org/thingsboard/server/extensions/rabbitmq/RabbitMqDemoClient.java 56(+0 -56)
extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/action/RestApiCallActionMsg.java 28(+0 -28)
extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/action/RestApiCallActionPayload.java 37(+0 -37)
extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/action/RestApiCallPluginAction.java 51(+0 -51)
extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/action/RestApiCallPluginActionConfiguration.java 28(+0 -28)
extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallMsgHandler.java 67(+0 -67)
extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPlugin.java 98(+0 -98)
extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPluginConfiguration.java 37(+0 -37)
extensions/extension-rest-api-call/src/test/java/org/thingsboard/server/extensions/rest/RestApiCallDemoClient.java 70(+0 -70)
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionMsg.java 31(+0 -31)
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionPayload.java 37(+0 -37)
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginAction.java 45(+0 -45)
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginActionConfiguration.java 30(+0 -30)
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsMessageHandler.java 63(+0 -63)
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPlugin.java 79(+0 -79)
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPluginConfiguration.java 30(+0 -30)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionMsg.java 31(+0 -31)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionPayload.java 39(+0 -39)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginAction.java 49(+0 -49)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginActionConfiguration.java 30(+0 -30)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionMsg.java 31(+0 -31)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionPayload.java 39(+0 -39)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginAction.java 46(+0 -46)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginActionConfiguration.java 32(+0 -32)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsMessageHandler.java 84(+0 -84)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPlugin.java 78(+0 -78)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPluginConfiguration.java 30(+0 -30)
extensions/extension-sqs/src/test/java/org/thingsboard/server/extensions/sqs/SqsDemoClient.java 69(+0 -69)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/AbstractPlugin.java 88(+0 -88)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRestMsgHandler.java 72(+0 -72)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRuleMsgHandler.java 63(+0 -63)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java 104(+0 -104)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractPluginToRuleMsg.java 63(+0 -63)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractRuleToPluginMsg.java 72(+0 -72)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/PluginToRuleMsg.java 64(+0 -64)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RuleToPluginMsg.java 61(+0 -61)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java 80(+0 -80)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java 128(+0 -128)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/BasicPluginRestMsg.java 63(+0 -63)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/RestRequest.java 87(+0 -87)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/BasicPluginWebsocketSessionRef.java 111(+0 -111)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/AbstractPluginWebSocketMsg.java 66(+0 -66)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailAction.java 109(+0 -109)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/rpc/RpcPluginAction.java 68(+0 -68)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/rpc/ServerSideRpcCallAction.java 107(+0 -107)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/telemetry/TelemetryPluginAction.java 84(+0 -84)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/template/AbstractTemplatePluginAction.java 87(+0 -87)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/BasicJsFilter.java 78(+0 -78)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilter.java 57(+0 -57)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceTypeFilter.java 52(+0 -52)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceTypeFilterConfiguration.java 33(+0 -33)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java 56(+0 -56)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilterConfiguration.java 33(+0 -33)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java 67(+0 -67)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilterConfiguration.java 28(+0 -28)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/NashornJsEvaluator.java 131(+0 -131)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/KeyValuePluginProperties.java 27(+0 -27)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java 129(+0 -129)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPluginConfiguration.java 33(+0 -33)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPlugin.java 69(+0 -69)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPluginConfiguration.java 30(+0 -30)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java 228(+0 -228)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/PendingRpcRequestMetadata.java 37(+0 -37)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/cmd/RpcRequest.java 28(+0 -28)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java 161(+0 -161)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRuleMsgHandler.java 102(+0 -102)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/LocalRequestMetaData.java 30(+0 -30)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java 69(+0 -69)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPlugin.java 86(+0 -86)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPluginConfiguration.java 26(+0 -26)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/AttributeData.java 48(+0 -48)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java 32(+0 -32)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java 40(+0 -40)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java 42(+0 -42)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmd.java 29(+0 -29)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmdsWrapper.java 58(+0 -58)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java 41(+0 -41)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/BiPluginCallBack.java 74(+0 -74)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryFeature.java 29(+0 -29)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java 441(+0 -441)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java 300(+0 -300)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java 152(+0 -152)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java 395(+0 -395)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java 78(+0 -78)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionErrorCode.java 50(+0 -50)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java 69(+0 -69)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionType.java 23(+0 -23)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java 93(+0 -93)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java 365(+0 -365)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TelemetryStoragePlugin.java 106(+0 -106)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TsData.java 42(+0 -42)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java 92(+0 -92)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePluginConfiguration.java 26(+0 -26)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessor.java 84(+0 -84)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessorConfiguration.java 29(+0 -29)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmProcessor.java 250(+0 -250)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmProcessorConfiguration.java 39(+0 -39)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/utils/VelocityUtils.java 105(+0 -105)
extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java 129(+0 -129)
netty-mqtt/.gitignore 7(+7 -0)
netty-mqtt/pom.xml 99(+99 -0)
pom.xml 97(+31 -66)
rule-engine/pom.xml 41(+41 -0)
rule-engine/rule-engine-api/pom.xml 52(+12 -40)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/EmptyNodeConfiguration.java 35(+35 -0)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ListeningExecutor.java 11(+7 -4)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java 10(+7 -3)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceAttributes.java 2(+1 -1)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceAttributesEventNotificationMsg.java 7(+6 -1)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceCredentialsUpdateNotificationMsg.java 7(+6 -1)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceMetaData.java 2(+1 -1)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceNameOrTypeUpdateMsg.java 8(+7 -1)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/ToDeviceActorNotificationMsg.java 5(+3 -2)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeConfiguration.java 8(+3 -5)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java 38(+38 -0)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceRpcRequest.java 36(+36 -0)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceRpcResponse.java 34(+14 -20)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java 17(+10 -7)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java 44(+44 -0)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java 31(+13 -18)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeConfiguration.java 16(+7 -9)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java 19(+11 -8)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbRelationTypes.java 11(+6 -5)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/DonAsynchron.java 56(+56 -0)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java 57(+57 -0)
rule-engine/rule-engine-components/pom.xml 119(+71 -48)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java 117(+117 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java 12(+7 -5)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java 79(+79 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java 36(+36 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java 105(+105 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java 41(+41 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java 69(+69 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNodeConfiguration.java 20(+11 -9)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java 120(+120 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeConfiguration.java 37(+37 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java 151(+151 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeConfiguration.java 51(+51 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/DeviceRelationsQuery.java 16(+10 -6)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java 17(+10 -7)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java 105(+105 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java 44(+44 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java 65(+65 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java 19(+11 -8)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java 71(+71 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java 41(+41 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java 55(+55 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java 43(+43 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java 88(+88 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java 83(+83 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java 120(+120 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java 55(+55 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/EmailPojo.java 10(+3 -7)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java 96(+96 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java 40(+40 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java 146(+146 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNodeConfiguration.java 44(+44 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java 116(+116 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java 105(+105 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java 54(+54 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java 45(+45 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java 44(+44 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java 52(+52 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java 48(+48 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java 35(+16 -19)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java 54(+54 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java 50(+50 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java 46(+46 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/AnonymousCredentials.java 22(+14 -8)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/BasicCredentials.java 33(+18 -15)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java 154(+154 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/MqttClientCredentials.java 41(+18 -23)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java 144(+144 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java 48(+48 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java 148(+148 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNodeConfiguration.java 59(+59 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java 158(+158 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java 39(+39 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java 69(+69 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRpcReplyNodeConfiguration.java 33(+33 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java 103(+103 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRpcRequestNodeConfiguration.java 30(+11 -19)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java 71(+71 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java 33(+33 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java 97(+97 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java 21(+11 -10)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TelemetryNodeCallback.java 40(+19 -21)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java 62(+62 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java 109(+109 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java 48(+48 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java 61(+61 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java 32(+32 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformNodeConfiguration.java 8(+2 -6)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java 49(+49 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java 55(+55 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java 58(+58 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java 56(+56 -0)
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css 2(+2 -0)
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js 4(+4 -0)
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java 378(+378 -0)
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java 126(+126 -0)
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java 108(+108 -0)
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java 110(+110 -0)
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java 262(+262 -0)
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java 167(+167 -0)
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java 126(+126 -0)
tools/pom.xml 2(+1 -1)
transport/coap/pom.xml 2(+1 -1)
transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java 16(+8 -8)
transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java 24(+11 -13)
transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java 3(+2 -1)
transport/http/pom.xml 2(+1 -1)
transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java 7(+4 -3)
transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionCtx.java 6(+3 -3)
transport/mqtt/pom.xml 2(+1 -1)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java 12(+6 -6)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java 22(+15 -7)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java 4(+2 -2)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java 6(+3 -3)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java 16(+8 -8)
transport/pom.xml 2(+1 -1)
ui/package.json 3(+2 -1)
ui/pom.xml 2(+1 -1)
ui/server.js 20(+20 -0)
ui/src/app/api/entity.service.js 31(+6 -25)
ui/src/app/api/rule-chain.service.js 298(+298 -0)
ui/src/app/app.js 7(+7 -0)
ui/src/app/common/types.constant.js 130(+113 -17)
ui/src/app/components/ace-editor-fix.js 45(+45 -0)
ui/src/app/components/details-sidenav.scss 10(+10 -0)
ui/src/app/components/js-func.directive.js 48(+42 -6)
ui/src/app/components/js-func.scss 25(+23 -2)
ui/src/app/components/js-func.tpl.html 22(+13 -9)
ui/src/app/components/json-content.directive.js 184(+184 -0)
ui/src/app/components/json-content.scss 48(+48 -0)
ui/src/app/components/json-content.tpl.html 34(+34 -0)
ui/src/app/components/json-object-edit.directive.js 183(+183 -0)
ui/src/app/components/json-object-edit.scss 38(+17 -21)
ui/src/app/components/kv-map.directive.js 119(+119 -0)
ui/src/app/components/kv-map.scss 20(+10 -10)
ui/src/app/components/kv-map.tpl.html 58(+58 -0)
ui/src/app/event/event.scss 25(+21 -4)
ui/src/app/event/event-row.directive.js 24(+22 -2)
ui/src/app/event/event-table.directive.js 18(+15 -3)
ui/src/app/help/help.directive.js 4(+4 -0)
ui/src/app/help/help-links.constant.js 158(+72 -86)
ui/src/app/import-export/import-export.service.js 148(+57 -91)
ui/src/app/layout/index.js 14(+9 -5)
ui/src/app/locale/locale.constant.js 208(+113 -95)
ui/src/app/rulechain/add-link.tpl.html 48(+48 -0)
ui/src/app/rulechain/add-rulechain.tpl.html 48(+48 -0)
ui/src/app/rulechain/add-rulenode.tpl.html 48(+48 -0)
ui/src/app/rulechain/index.js 41(+41 -0)
ui/src/app/rulechain/link.directive.js 71(+71 -0)
ui/src/app/rulechain/link-fieldset.tpl.html 39(+39 -0)
ui/src/app/rulechain/rulechain.controller.js 1357(+1357 -0)
ui/src/app/rulechain/rulechain.directive.js 48(+48 -0)
ui/src/app/rulechain/rulechain.routes.js 127(+127 -0)
ui/src/app/rulechain/rulechain.scss 512(+512 -0)
ui/src/app/rulechain/rulechain.tpl.html 246(+246 -0)
ui/src/app/rulechain/rulechain-card.tpl.html 18(+18 -0)
ui/src/app/rulechain/rulechains.controller.js 215(+215 -0)
ui/src/app/rulechain/rulechains.tpl.html 78(+78 -0)
ui/src/app/rulechain/rulenode.directive.js 81(+81 -0)
ui/src/app/rulechain/rulenode.scss 23(+11 -12)
ui/src/app/rulechain/rulenode.tpl.html 54(+54 -0)
ui/src/app/rulechain/script/node-script-test.scss 115(+115 -0)
ui/src/app/services/item-buffer.service.js 83(+81 -2)
ui/src/app/services/menu.service.js 48(+5 -43)
ui/src/scss/main.scss 34(+34 -0)
Details
application/pom.xml 57(+9 -48)
diff --git a/application/pom.xml b/application/pom.xml
index 8449246..e0b6b98 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -20,10 +20,9 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.4.1-SNAPSHOT</version>
+ <version>2.0.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
- <groupId>org.thingsboard</groupId>
<artifactId>application</artifactId>
<packaging>jar</packaging>
@@ -50,12 +49,12 @@
<classifier>linux-x86_64</classifier>
</dependency>
<dependency>
- <groupId>org.thingsboard</groupId>
- <artifactId>extensions-api</artifactId>
+ <groupId>org.thingsboard.rule-engine</groupId>
+ <artifactId>rule-engine-api</artifactId>
</dependency>
<dependency>
- <groupId>org.thingsboard</groupId>
- <artifactId>extensions-core</artifactId>
+ <groupId>org.thingsboard.rule-engine</groupId>
+ <artifactId>rule-engine-components</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
@@ -257,6 +256,10 @@
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.javadelight</groupId>
+ <artifactId>delight-nashorn-sandbox</artifactId>
+ </dependency>
</dependencies>
<build>
@@ -464,48 +467,6 @@
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
- <id>copy-extensions</id>
- <phase>package</phase>
- <goals>
- <goal>copy</goal>
- </goals>
- <configuration>
- <outputDirectory>${project.build.directory}/extensions</outputDirectory>
- <artifactItems>
- <artifactItem>
- <groupId>org.thingsboard.extensions</groupId>
- <artifactId>extension-rabbitmq</artifactId>
- <classifier>extension</classifier>
- </artifactItem>
- <artifactItem>
- <groupId>org.thingsboard.extensions</groupId>
- <artifactId>extension-rest-api-call</artifactId>
- <classifier>extension</classifier>
- </artifactItem>
- <artifactItem>
- <groupId>org.thingsboard.extensions</groupId>
- <artifactId>extension-kafka</artifactId>
- <classifier>extension</classifier>
- </artifactItem>
- <artifactItem>
- <groupId>org.thingsboard.extensions</groupId>
- <artifactId>extension-mqtt</artifactId>
- <classifier>extension</classifier>
- </artifactItem>
- <artifactItem>
- <groupId>org.thingsboard.extensions</groupId>
- <artifactId>extension-sqs</artifactId>
- <classifier>extension</classifier>
- </artifactItem>
- <artifactItem>
- <groupId>org.thingsboard.extensions</groupId>
- <artifactId>extension-sns</artifactId>
- <classifier>extension</classifier>
- </artifactItem>
- </artifactItems>
- </configuration>
- </execution>
- <execution>
<id>copy-winsw-service</id>
<phase>package</phase>
<goals>
diff --git a/application/src/main/conf/thingsboard.conf b/application/src/main/conf/thingsboard.conf
index a6e404d..2baee7d 100644
--- a/application/src/main/conf/thingsboard.conf
+++ b/application/src/main/conf/thingsboard.conf
@@ -14,7 +14,7 @@
# limitations under the License.
#
-export JAVA_OPTS="$JAVA_OPTS -Dplatform=@pkg.platform@"
+export JAVA_OPTS="$JAVA_OPTS -Dplatform=@pkg.platform@ -Dinstall.data_dir=@pkg.installFolder@"
export LOG_FILENAME=${pkg.name}.out
export LOADER_PATH=${pkg.installFolder}/conf,${pkg.installFolder}/extensions
export SQL_DATA_FOLDER=${pkg.installFolder}/data/sql
diff --git a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
new file mode 100644
index 0000000..225ccdc
--- /dev/null
+++ b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
@@ -0,0 +1,98 @@
+{
+ "ruleChain": {
+ "additionalInfo": null,
+ "name": "Root Rule Chain",
+ "firstRuleNodeId": null,
+ "root": true,
+ "debugMode": false,
+ "configuration": null
+ },
+ "metadata": {
+ "firstNodeIndex": 2,
+ "nodes": [
+ {
+ "additionalInfo": {
+ "layoutX": 824,
+ "layoutY": 156
+ },
+ "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
+ "name": "SaveTS",
+ "debugMode": false,
+ "configuration": {
+ "defaultTTL": 0
+ }
+ },
+ {
+ "additionalInfo": {
+ "layoutX": 825,
+ "layoutY": 52
+ },
+ "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
+ "name": "save client attributes",
+ "debugMode": false,
+ "configuration": {
+ "scope": "CLIENT_SCOPE"
+ }
+ },
+ {
+ "additionalInfo": {
+ "layoutX": 347,
+ "layoutY": 149
+ },
+ "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode",
+ "name": "Message Type Switch",
+ "debugMode": false,
+ "configuration": {
+ "version": 0
+ }
+ },
+ {
+ "additionalInfo": {
+ "layoutX": 825,
+ "layoutY": 266
+ },
+ "type": "org.thingsboard.rule.engine.action.TbLogNode",
+ "name": "Log RPC",
+ "debugMode": false,
+ "configuration": {
+ "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
+ }
+ },
+ {
+ "additionalInfo": {
+ "layoutX": 825,
+ "layoutY": 379
+ },
+ "type": "org.thingsboard.rule.engine.action.TbLogNode",
+ "name": "Log Other",
+ "debugMode": false,
+ "configuration": {
+ "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
+ }
+ }
+ ],
+ "connections": [
+ {
+ "fromIndex": 2,
+ "toIndex": 4,
+ "type": "Other"
+ },
+ {
+ "fromIndex": 2,
+ "toIndex": 1,
+ "type": "Post attributes"
+ },
+ {
+ "fromIndex": 2,
+ "toIndex": 0,
+ "type": "Post telemetry"
+ },
+ {
+ "fromIndex": 2,
+ "toIndex": 3,
+ "type": "RPC Request"
+ }
+ ],
+ "ruleChainConnections": null
+ }
+}
\ No newline at end of file
diff --git a/application/src/main/data/upgrade/2.0.0/schema_update.cql b/application/src/main/data/upgrade/2.0.0/schema_update.cql
new file mode 100644
index 0000000..5f46878
--- /dev/null
+++ b/application/src/main/data/upgrade/2.0.0/schema_update.cql
@@ -0,0 +1,103 @@
+--
+-- Copyright © 2016-2018 The Thingsboard Authors
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+CREATE TABLE IF NOT EXISTS thingsboard.msg_queue (
+ node_id timeuuid,
+ cluster_partition bigint,
+ ts_partition bigint,
+ ts bigint,
+ msg blob,
+ PRIMARY KEY ((node_id, cluster_partition, ts_partition), ts))
+WITH CLUSTERING ORDER BY (ts DESC)
+AND compaction = {
+ 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy',
+ 'min_threshold': '5',
+ 'base_time_seconds': '43200',
+ 'max_window_size_seconds': '43200',
+ 'tombstone_threshold': '0.9',
+ 'unchecked_tombstone_compaction': 'true'
+};
+
+CREATE TABLE IF NOT EXISTS thingsboard.msg_ack_queue (
+ node_id timeuuid,
+ cluster_partition bigint,
+ ts_partition bigint,
+ msg_id timeuuid,
+ PRIMARY KEY ((node_id, cluster_partition, ts_partition), msg_id))
+WITH CLUSTERING ORDER BY (msg_id DESC)
+AND compaction = {
+ 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy',
+ 'min_threshold': '5',
+ 'base_time_seconds': '43200',
+ 'max_window_size_seconds': '43200',
+ 'tombstone_threshold': '0.9',
+ 'unchecked_tombstone_compaction': 'true'
+};
+
+CREATE TABLE IF NOT EXISTS thingsboard.processed_msg_partitions (
+ node_id timeuuid,
+ cluster_partition bigint,
+ ts_partition bigint,
+ PRIMARY KEY ((node_id, cluster_partition), ts_partition))
+WITH CLUSTERING ORDER BY (ts_partition DESC)
+AND compaction = {
+ 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy',
+ 'min_threshold': '5',
+ 'base_time_seconds': '43200',
+ 'max_window_size_seconds': '43200',
+ 'tombstone_threshold': '0.9',
+ 'unchecked_tombstone_compaction': 'true'
+};
+
+CREATE TABLE IF NOT EXISTS thingsboard.rule_chain (
+ id uuid,
+ tenant_id uuid,
+ name text,
+ search_text text,
+ first_rule_node_id uuid,
+ root boolean,
+ debug_mode boolean,
+ configuration text,
+ additional_info text,
+ PRIMARY KEY (id, tenant_id)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.rule_chain_by_tenant_and_search_text AS
+ SELECT *
+ from thingsboard.rule_chain
+ WHERE tenant_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( tenant_id, search_text, id )
+ WITH CLUSTERING ORDER BY ( search_text ASC, id DESC );
+
+CREATE TABLE IF NOT EXISTS thingsboard.rule_node (
+ id uuid,
+ rule_chain_id uuid,
+ type text,
+ name text,
+ debug_mode boolean,
+ search_text text,
+ configuration text,
+ additional_info text,
+ PRIMARY KEY (id)
+);
+
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.rule_by_plugin_token;
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.rule_by_tenant_and_search_text;
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.plugin_by_api_token;
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.plugin_by_tenant_and_search_text;
+
+DROP TABLE IF EXISTS thingsboard.rule;
+DROP TABLE IF EXISTS thingsboard.plugin;
diff --git a/application/src/main/data/upgrade/2.0.0/schema_update.sql b/application/src/main/data/upgrade/2.0.0/schema_update.sql
new file mode 100644
index 0000000..f16e334
--- /dev/null
+++ b/application/src/main/data/upgrade/2.0.0/schema_update.sql
@@ -0,0 +1,44 @@
+--
+-- Copyright © 2016-2018 The Thingsboard Authors
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+CREATE TABLE IF NOT EXISTS rule_chain (
+ id varchar(31) NOT NULL CONSTRAINT rule_chain_pkey PRIMARY KEY,
+ additional_info varchar,
+ configuration varchar(10000000),
+ name varchar(255),
+ first_rule_node_id varchar(31),
+ root boolean,
+ debug_mode boolean,
+ search_text varchar(255),
+ tenant_id varchar(31)
+);
+
+CREATE TABLE IF NOT EXISTS rule_node (
+ id varchar(31) NOT NULL CONSTRAINT rule_node_pkey PRIMARY KEY,
+ rule_chain_id varchar(31),
+ additional_info varchar,
+ configuration varchar(10000000),
+ type varchar(255),
+ name varchar(255),
+ debug_mode boolean,
+ search_text varchar(255)
+);
+
+DROP TABLE rule;
+DROP TABLE plugin;
+
+DELETE FROM alarm WHERE originator_type = 3 OR originator_type = 4;
+UPDATE alarm SET originator_type = (originator_type - 2) where originator_type > 2;
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index 77953c9..d33734e 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -21,22 +21,27 @@ import akka.actor.Scheduler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import lombok.Getter;
import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
+import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.transport.auth.DeviceAuthService;
-import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
@@ -44,118 +49,222 @@ import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.event.EventService;
-import org.thingsboard.server.dao.plugin.PluginService;
import org.thingsboard.server.dao.relation.RelationService;
-import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
-
+import org.thingsboard.server.service.encoding.DataDecodingEncodingService;
+import org.thingsboard.server.service.executors.DbCallbackExecutorService;
+import org.thingsboard.server.service.executors.ExternalCallExecutorService;
+import org.thingsboard.server.service.mail.MailExecutorService;
+import org.thingsboard.server.service.queue.MsgQueueService;
+import org.thingsboard.server.service.rpc.DeviceRpcService;
+import org.thingsboard.server.service.script.JsExecutorService;
+import org.thingsboard.server.service.script.JsSandboxService;
+import org.thingsboard.server.service.state.DeviceStateService;
+import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
+
+import javax.annotation.Nullable;
+import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Optional;
+@Slf4j
@Component
public class ActorSystemContext {
private static final String AKKA_CONF_FILE_NAME = "actor-system.conf";
protected final ObjectMapper mapper = new ObjectMapper();
- @Getter @Setter private ActorService actorService;
+ @Getter
+ @Setter
+ private ActorService actorService;
+
+ @Autowired
+ @Getter
+ private DiscoveryService discoveryService;
+
+ @Autowired
+ @Getter
+ @Setter
+ private ComponentDiscoveryService componentService;
+
+ @Autowired
+ @Getter
+ private ClusterRoutingService routingService;
+
+ @Autowired
+ @Getter
+ private ClusterRpcService rpcService;
+
+ @Autowired
+ @Getter
+ private DataDecodingEncodingService encodingService;
+
+ @Autowired
+ @Getter
+ private DeviceAuthService deviceAuthService;
+
+ @Autowired
+ @Getter
+ private DeviceService deviceService;
+
+ @Autowired
+ @Getter
+ private AssetService assetService;
+
+ @Autowired
+ @Getter
+ private TenantService tenantService;
+
+ @Autowired
+ @Getter
+ private CustomerService customerService;
@Autowired
- @Getter private DiscoveryService discoveryService;
+ @Getter
+ private UserService userService;
@Autowired
- @Getter @Setter private ComponentDiscoveryService componentService;
+ @Getter
+ private RuleChainService ruleChainService;
@Autowired
- @Getter private ClusterRoutingService routingService;
+ @Getter
+ private TimeseriesService tsService;
@Autowired
- @Getter private ClusterRpcService rpcService;
+ @Getter
+ private AttributesService attributesService;
@Autowired
- @Getter private DeviceAuthService deviceAuthService;
+ @Getter
+ private EventService eventService;
@Autowired
- @Getter private DeviceService deviceService;
+ @Getter
+ private AlarmService alarmService;
@Autowired
- @Getter private AssetService assetService;
+ @Getter
+ private RelationService relationService;
@Autowired
- @Getter private TenantService tenantService;
+ @Getter
+ private AuditLogService auditLogService;
@Autowired
- @Getter private CustomerService customerService;
+ @Getter
+ private TelemetrySubscriptionService tsSubService;
@Autowired
- @Getter private RuleService ruleService;
+ @Getter
+ private DeviceRpcService deviceRpcService;
@Autowired
- @Getter private PluginService pluginService;
+ @Getter
+ private JsSandboxService jsSandbox;
@Autowired
- @Getter private TimeseriesService tsService;
+ @Getter
+ private JsExecutorService jsExecutor;
@Autowired
- @Getter private AttributesService attributesService;
+ @Getter
+ private MailExecutorService mailExecutor;
@Autowired
- @Getter private EventService eventService;
+ @Getter
+ private DbCallbackExecutorService dbCallbackExecutor;
@Autowired
- @Getter private AlarmService alarmService;
+ @Getter
+ private ExternalCallExecutorService externalCallExecutorService;
@Autowired
- @Getter private RelationService relationService;
+ @Getter
+ private MailService mailService;
@Autowired
- @Getter private AuditLogService auditLogService;
+ @Getter
+ private MsgQueueService msgQueueService;
@Autowired
- @Getter @Setter private PluginWebSocketMsgEndpoint wsMsgEndpoint;
+ @Getter
+ private DeviceStateService deviceStateService;
+
+ @Value("${cluster.partition_id}")
+ @Getter
+ private long queuePartitionId;
+
+ @Value("${actors.session.max_concurrent_sessions_per_device:1}")
+ @Getter
+ private long maxConcurrentSessionsPerDevice;
@Value("${actors.session.sync.timeout}")
- @Getter private long syncSessionTimeout;
+ @Getter
+ private long syncSessionTimeout;
- @Value("${actors.plugin.termination.delay}")
- @Getter private long pluginActorTerminationDelay;
+ @Value("${actors.queue.enabled}")
+ @Getter
+ private boolean queuePersistenceEnabled;
- @Value("${actors.plugin.processing.timeout}")
- @Getter private long pluginProcessingTimeout;
+ @Value("${actors.queue.timeout}")
+ @Getter
+ private long queuePersistenceTimeout;
- @Value("${actors.plugin.error_persist_frequency}")
- @Getter private long pluginErrorPersistFrequency;
+ @Value("${actors.client_side_rpc.timeout}")
+ @Getter
+ private long clientSideRpcTimeout;
- @Value("${actors.rule.termination.delay}")
- @Getter private long ruleActorTerminationDelay;
+ @Value("${actors.rule.chain.error_persist_frequency}")
+ @Getter
+ private long ruleChainErrorPersistFrequency;
- @Value("${actors.rule.error_persist_frequency}")
- @Getter private long ruleErrorPersistFrequency;
+ @Value("${actors.rule.node.error_persist_frequency}")
+ @Getter
+ private long ruleNodeErrorPersistFrequency;
@Value("${actors.statistics.enabled}")
- @Getter private boolean statisticsEnabled;
+ @Getter
+ private boolean statisticsEnabled;
@Value("${actors.statistics.persist_frequency}")
- @Getter private long statisticsPersistFrequency;
+ @Getter
+ private long statisticsPersistFrequency;
@Value("${actors.tenant.create_components_on_init}")
- @Getter private boolean tenantComponentsInitEnabled;
+ @Getter
+ private boolean tenantComponentsInitEnabled;
- @Getter @Setter private ActorSystem actorSystem;
+ @Value("${actors.rule.allow_system_mail_service}")
+ @Getter
+ private boolean allowSystemMailService;
- @Getter @Setter private ActorRef appActor;
+ @Getter
+ @Setter
+ private ActorSystem actorSystem;
- @Getter @Setter private ActorRef sessionManagerActor;
+ @Getter
+ @Setter
+ private ActorRef appActor;
- @Getter @Setter private ActorRef statsActor;
+ @Getter
+ @Setter
+ private ActorRef sessionManagerActor;
- @Getter private final Config config;
+ @Getter
+ @Setter
+ private ActorRef statsActor;
+
+ @Getter
+ private final Config config;
public ActorSystemContext() {
config = ConfigFactory.parseResources(AKKA_CONF_FILE_NAME).withFallback(ConfigFactory.load());
@@ -187,7 +296,7 @@ public class ActorSystemContext {
eventService.save(event);
}
- private String toString(Exception e) {
+ private String toString(Throwable e) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
return sw.toString();
@@ -207,4 +316,72 @@ public class ActorSystemContext {
private JsonNode toBodyJson(ServerAddress server, String method, String body) {
return mapper.createObjectNode().put("server", server.toString()).put("method", method).put("error", body);
}
+
+ public String getServerAddress() {
+ return discoveryService.getCurrentServer().getServerAddress().toString();
+ }
+
+ public void persistDebugInput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, String relationType) {
+ persistDebugAsync(tenantId, entityId, "IN", tbMsg, relationType, null);
+ }
+
+ public void persistDebugInput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, String relationType, Throwable error) {
+ persistDebugAsync(tenantId, entityId, "IN", tbMsg, relationType, error);
+ }
+
+ public void persistDebugOutput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, String relationType, Throwable error) {
+ persistDebugAsync(tenantId, entityId, "OUT", tbMsg, relationType, error);
+ }
+
+ public void persistDebugOutput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, String relationType) {
+ persistDebugAsync(tenantId, entityId, "OUT", tbMsg, relationType, null);
+ }
+
+ private void persistDebugAsync(TenantId tenantId, EntityId entityId, String type, TbMsg tbMsg, String relationType, Throwable error) {
+ try {
+ Event event = new Event();
+ event.setTenantId(tenantId);
+ event.setEntityId(entityId);
+ event.setType(DataConstants.DEBUG_RULE_NODE);
+
+ String metadata = mapper.writeValueAsString(tbMsg.getMetaData().getData());
+
+ ObjectNode node = mapper.createObjectNode()
+ .put("type", type)
+ .put("server", getServerAddress())
+ .put("entityId", tbMsg.getOriginator().getId().toString())
+ .put("entityName", tbMsg.getOriginator().getEntityType().name())
+ .put("msgId", tbMsg.getId().toString())
+ .put("msgType", tbMsg.getType())
+ .put("dataType", tbMsg.getDataType().name())
+ .put("relationType", relationType)
+ .put("data", tbMsg.getData())
+ .put("metadata", metadata);
+
+ if (error != null) {
+ node = node.put("error", toString(error));
+ }
+
+ event.setBody(node);
+ ListenableFuture<Event> future = eventService.saveAsync(event);
+ Futures.addCallback(future, new FutureCallback<Event>() {
+ @Override
+ public void onSuccess(@Nullable Event event) {
+
+ }
+
+ @Override
+ public void onFailure(Throwable th) {
+ log.error("Could not save debug Event for Node", th);
+ }
+ });
+ } catch (IOException ex) {
+ log.warn("Failed to persist rule node debug message", ex);
+ }
+ }
+
+ public static Exception toException(Throwable error) {
+ return Exception.class.isInstance(error) ? (Exception) error : new Exception(error);
+ }
+
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
index b475277..d453e59 100644
--- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
@@ -15,55 +15,51 @@
*/
package org.thingsboard.server.actors.app;
-import akka.actor.*;
+import akka.actor.ActorRef;
+import akka.actor.LocalActorRef;
+import akka.actor.OneForOneStrategy;
+import akka.actor.Props;
+import akka.actor.SupervisorStrategy;
import akka.actor.SupervisorStrategy.Directive;
+import akka.actor.Terminated;
import akka.event.Logging;
import akka.event.LoggingAdapter;
import akka.japi.Function;
import org.thingsboard.server.actors.ActorSystemContext;
-import org.thingsboard.server.actors.plugin.PluginTerminationMsg;
-import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.ruleChain.RuleChainManagerActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.actors.service.DefaultActorService;
-import org.thingsboard.server.actors.shared.plugin.PluginManager;
-import org.thingsboard.server.actors.shared.plugin.SystemPluginManager;
-import org.thingsboard.server.actors.shared.rule.RuleManager;
-import org.thingsboard.server.actors.shared.rule.SystemRuleManager;
-import org.thingsboard.server.actors.tenant.RuleChainDeviceMsg;
+import org.thingsboard.server.actors.shared.rulechain.SystemRuleChainManager;
import org.thingsboard.server.actors.tenant.TenantActor;
import org.thingsboard.server.common.data.Tenant;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable;
-import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
+import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.core.BasicActorSystemToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.device.DeviceToDeviceActorMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.tenant.TenantService;
-import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
-import org.thingsboard.server.extensions.api.rules.ToRuleActorMsg;
import scala.concurrent.duration.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
-public class AppActor extends ContextAwareActor {
+public class AppActor extends RuleChainManagerActor {
private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
public static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);
- private final RuleManager ruleManager;
- private final PluginManager pluginManager;
private final TenantService tenantService;
private final Map<TenantId, ActorRef> tenantActors;
private AppActor(ActorSystemContext systemContext) {
- super(systemContext);
- this.ruleManager = new SystemRuleManager(systemContext);
- this.pluginManager = new SystemPluginManager(systemContext);
+ super(systemContext, new SystemRuleChainManager(systemContext));
this.tenantService = systemContext.getTenantService();
this.tenantActors = new HashMap<>();
}
@@ -77,8 +73,7 @@ public class AppActor extends ContextAwareActor {
public void preStart() {
logger.info("Starting main system actor.");
try {
- ruleManager.init(this.context());
- pluginManager.init(this.context());
+ initRuleChains();
if (systemContext.isTenantComponentsInitEnabled()) {
PageDataIterable<Tenant> tenantIterator = new PageDataIterable<>(tenantService::findTenants, ENTITY_PACK_LIMIT);
@@ -96,93 +91,89 @@ public class AppActor extends ContextAwareActor {
}
@Override
- public void onReceive(Object msg) throws Exception {
- logger.debug("Received message: {}", msg);
- if (msg instanceof ToDeviceActorMsg) {
- processDeviceMsg((ToDeviceActorMsg) msg);
- } else if (msg instanceof ToPluginActorMsg) {
- onToPluginMsg((ToPluginActorMsg) msg);
- } else if (msg instanceof ToRuleActorMsg) {
- onToRuleMsg((ToRuleActorMsg) msg);
- } else if (msg instanceof ToDeviceActorNotificationMsg) {
- onToDeviceActorMsg((ToDeviceActorNotificationMsg) msg);
- } else if (msg instanceof Terminated) {
- processTermination((Terminated) msg);
- } else if (msg instanceof ClusterEventMsg) {
- broadcast(msg);
- } else if (msg instanceof ComponentLifecycleMsg) {
- onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
- } else if (msg instanceof PluginTerminationMsg) {
- onPluginTerminated((PluginTerminationMsg) msg);
- } else {
- logger.warning("Unknown message: {}!", msg);
+ protected boolean process(TbActorMsg msg) {
+ switch (msg.getMsgType()) {
+ case SEND_TO_CLUSTER_MSG:
+ onPossibleClusterMsg((SendToClusterMsg) msg);
+ break;
+ case CLUSTER_EVENT_MSG:
+ broadcast(msg);
+ break;
+ case COMPONENT_LIFE_CYCLE_MSG:
+ onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+ break;
+ case SERVICE_TO_RULE_ENGINE_MSG:
+ onServiceToRuleEngineMsg((ServiceToRuleEngineMsg) msg);
+ break;
+ case DEVICE_SESSION_TO_DEVICE_ACTOR_MSG:
+ case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG:
+ case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG:
+ case DEVICE_NAME_OR_TYPE_UPDATE_TO_DEVICE_ACTOR_MSG:
+ case DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG:
+ case SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG:
+ onToDeviceActorMsg((TenantAwareMsg) msg);
+ break;
+ case ACTOR_SYSTEM_TO_DEVICE_SESSION_ACTOR_MSG:
+ onToDeviceSessionMsg((BasicActorSystemToDeviceSessionActorMsg) msg);
+ default:
+ return false;
}
+ return true;
}
- private void onPluginTerminated(PluginTerminationMsg msg) {
- pluginManager.remove(msg.getId());
+ private void onToDeviceSessionMsg(BasicActorSystemToDeviceSessionActorMsg msg) {
+ systemContext.getSessionManagerActor().tell(msg, self());
}
- private void broadcast(Object msg) {
- pluginManager.broadcast(msg);
- tenantActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+ private void onPossibleClusterMsg(SendToClusterMsg msg) {
+ Optional<ServerAddress> address = systemContext.getRoutingService().resolveById(msg.getEntityId());
+ if (address.isPresent()) {
+ systemContext.getRpcService().tell(
+ systemContext.getEncodingService().convertToProtoDataMessage(address.get(), msg.getMsg()));
+ } else {
+ self().tell(msg.getMsg(), ActorRef.noSender());
+ }
}
- private void onToRuleMsg(ToRuleActorMsg msg) {
- ActorRef target;
+ private void onServiceToRuleEngineMsg(ServiceToRuleEngineMsg msg) {
if (SYSTEM_TENANT.equals(msg.getTenantId())) {
- target = ruleManager.getOrCreateRuleActor(this.context(), msg.getRuleId());
+ //TODO: ashvayka handle this.
} else {
- target = getOrCreateTenantActor(msg.getTenantId());
+ getOrCreateTenantActor(msg.getTenantId()).tell(msg, self());
}
- target.tell(msg, ActorRef.noSender());
}
- private void onToPluginMsg(ToPluginActorMsg msg) {
- ActorRef target;
- if (SYSTEM_TENANT.equals(msg.getPluginTenantId())) {
- target = pluginManager.getOrCreatePluginActor(this.context(), msg.getPluginId());
- } else {
- target = getOrCreateTenantActor(msg.getPluginTenantId());
- }
- target.tell(msg, ActorRef.noSender());
+ @Override
+ protected void broadcast(Object msg) {
+ super.broadcast(msg);
+ tenantActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
}
private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
- ActorRef target = null;
+ ActorRef target;
if (SYSTEM_TENANT.equals(msg.getTenantId())) {
- Optional<PluginId> pluginId = msg.getPluginId();
- Optional<RuleId> ruleId = msg.getRuleId();
- if (pluginId.isPresent()) {
- target = pluginManager.getOrCreatePluginActor(this.context(), pluginId.get());
- } else if (ruleId.isPresent()) {
- Optional<ActorRef> ref = ruleManager.update(this.context(), ruleId.get(), msg.getEvent());
- if (ref.isPresent()) {
- target = ref.get();
- } else {
- logger.debug("Failed to find actor for rule: [{}]", ruleId);
- return;
- }
- }
+ target = getEntityActorRef(msg.getEntityId());
} else {
target = getOrCreateTenantActor(msg.getTenantId());
}
if (target != null) {
target.tell(msg, ActorRef.noSender());
+ } else {
+ logger.debug("Invalid component lifecycle msg: {}", msg);
}
}
- private void onToDeviceActorMsg(ToDeviceActorNotificationMsg msg) {
+ private void onToDeviceActorMsg(TenantAwareMsg msg) {
getOrCreateTenantActor(msg.getTenantId()).tell(msg, ActorRef.noSender());
}
- private void processDeviceMsg(ToDeviceActorMsg toDeviceActorMsg) {
- TenantId tenantId = toDeviceActorMsg.getTenantId();
+ private void processDeviceMsg(DeviceToDeviceActorMsg deviceToDeviceActorMsg) {
+ TenantId tenantId = deviceToDeviceActorMsg.getTenantId();
ActorRef tenantActor = getOrCreateTenantActor(tenantId);
- if (toDeviceActorMsg.getPayload().getMsgType().requiresRulesProcessing()) {
- tenantActor.tell(new RuleChainDeviceMsg(toDeviceActorMsg, ruleManager.getRuleChain(this.context())), context().self());
+ if (deviceToDeviceActorMsg.getPayload().getMsgType().requiresRulesProcessing()) {
+// tenantActor.tell(new RuleChainDeviceMsg(deviceToDeviceActorMsg, ruleManager.getRuleChain(this.context())), context().self());
} else {
- tenantActor.tell(toDeviceActorMsg, context().self());
+ tenantActor.tell(deviceToDeviceActorMsg, context().self());
}
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
index 861c405..99d0045 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
@@ -17,61 +17,73 @@ package org.thingsboard.server.actors.device;
import akka.event.Logging;
import akka.event.LoggingAdapter;
+import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg;
import org.thingsboard.server.actors.ActorSystemContext;
-import org.thingsboard.server.actors.rule.RulesProcessedMsg;
import org.thingsboard.server.actors.service.ContextAwareActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
-import org.thingsboard.server.actors.tenant.RuleChainDeviceMsg;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
-import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
-import org.thingsboard.server.extensions.api.device.DeviceNameOrTypeUpdateMsg;
-import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.*;
+import org.thingsboard.server.common.msg.device.DeviceToDeviceActorMsg;
+import org.thingsboard.server.common.msg.timeout.DeviceActorClientSideRpcTimeoutMsg;
+import org.thingsboard.server.common.msg.timeout.DeviceActorQueueTimeoutMsg;
+import org.thingsboard.server.common.msg.timeout.DeviceActorServerSideRpcTimeoutMsg;
+import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg;
+import org.thingsboard.server.service.rpc.ToServerRpcResponseActorMsg;
public class DeviceActor extends ContextAwareActor {
private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
- private final TenantId tenantId;
- private final DeviceId deviceId;
private final DeviceActorMessageProcessor processor;
private DeviceActor(ActorSystemContext systemContext, TenantId tenantId, DeviceId deviceId) {
super(systemContext);
- this.tenantId = tenantId;
- this.deviceId = deviceId;
- this.processor = new DeviceActorMessageProcessor(systemContext, logger, deviceId);
+ this.processor = new DeviceActorMessageProcessor(systemContext, logger, tenantId, deviceId);
}
@Override
- public void onReceive(Object msg) throws Exception {
- if (msg instanceof RuleChainDeviceMsg) {
- processor.process(context(), (RuleChainDeviceMsg) msg);
- } else if (msg instanceof RulesProcessedMsg) {
- processor.onRulesProcessedMsg(context(), (RulesProcessedMsg) msg);
- } else if (msg instanceof ToDeviceActorMsg) {
- processor.process(context(), (ToDeviceActorMsg) msg);
- } else if (msg instanceof ToDeviceActorNotificationMsg) {
- if (msg instanceof DeviceAttributesEventNotificationMsg) {
+ protected boolean process(TbActorMsg msg) {
+ switch (msg.getMsgType()) {
+ case CLUSTER_EVENT_MSG:
+ processor.processClusterEventMsg((ClusterEventMsg) msg);
+ break;
+ case DEVICE_SESSION_TO_DEVICE_ACTOR_MSG:
+ processor.process(context(), (DeviceToDeviceActorMsg) msg);
+ break;
+ case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG:
processor.processAttributesUpdate(context(), (DeviceAttributesEventNotificationMsg) msg);
- } else if (msg instanceof ToDeviceRpcRequestPluginMsg) {
- processor.processRpcRequest(context(), (ToDeviceRpcRequestPluginMsg) msg);
- } else if (msg instanceof DeviceCredentialsUpdateNotificationMsg){
+ break;
+ case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG:
processor.processCredentialsUpdate();
- } else if (msg instanceof DeviceNameOrTypeUpdateMsg){
+ break;
+ case DEVICE_NAME_OR_TYPE_UPDATE_TO_DEVICE_ACTOR_MSG:
processor.processNameOrTypeUpdate((DeviceNameOrTypeUpdateMsg) msg);
- }
- } else if (msg instanceof TimeoutMsg) {
- processor.processTimeout(context(), (TimeoutMsg) msg);
- } else if (msg instanceof ClusterEventMsg) {
- processor.processClusterEventMsg((ClusterEventMsg) msg);
- } else {
- logger.debug("[{}][{}] Unknown msg type.", tenantId, deviceId, msg.getClass().getName());
+ break;
+ case DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG:
+ processor.processRpcRequest(context(), (ToDeviceRpcRequestActorMsg) msg);
+ break;
+ case SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG:
+ processor.processToServerRPCResponse(context(), (ToServerRpcResponseActorMsg) msg);
+ break;
+ case DEVICE_ACTOR_SERVER_SIDE_RPC_TIMEOUT_MSG:
+ processor.processServerSideRpcTimeout(context(), (DeviceActorServerSideRpcTimeoutMsg) msg);
+ break;
+ case DEVICE_ACTOR_CLIENT_SIDE_RPC_TIMEOUT_MSG:
+ processor.processClientSideRpcTimeout(context(), (DeviceActorClientSideRpcTimeoutMsg) msg);
+ break;
+ case DEVICE_ACTOR_QUEUE_TIMEOUT_MSG:
+ processor.processQueueTimeout(context(), (DeviceActorQueueTimeoutMsg) msg);
+ break;
+ case RULE_ENGINE_QUEUE_PUT_ACK_MSG:
+ processor.processQueueAck(context(), (RuleEngineQueuePutAckMsg) msg);
+ break;
+ default:
+ return false;
}
+ return true;
}
public static class ActorCreator extends ContextBasedCreator<DeviceActor> {
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
index 21112bf..85b3285 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
@@ -18,37 +18,75 @@ package org.thingsboard.server.actors.device;
import akka.actor.ActorContext;
import akka.actor.ActorRef;
import akka.event.LoggingAdapter;
+import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import org.thingsboard.rule.engine.api.RpcError;
+import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg;
import org.thingsboard.server.actors.ActorSystemContext;
-import org.thingsboard.server.actors.rule.*;
import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
-import org.thingsboard.server.actors.tenant.RuleChainDeviceMsg;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.SessionId;
+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.rpc.ToDeviceRpcRequestBody;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgDataType;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.core.*;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.core.ActorSystemToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.core.AttributesUpdateNotification;
+import org.thingsboard.server.common.msg.core.AttributesUpdateRequest;
+import org.thingsboard.server.common.msg.core.BasicActorSystemToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.core.BasicCommandAckResponse;
+import org.thingsboard.server.common.msg.core.BasicGetAttributesResponse;
+import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
+import org.thingsboard.server.common.msg.core.GetAttributesRequest;
+import org.thingsboard.server.common.msg.core.RuleEngineError;
+import org.thingsboard.server.common.msg.core.RuleEngineErrorMsg;
+import org.thingsboard.server.common.msg.core.SessionCloseMsg;
+import org.thingsboard.server.common.msg.core.SessionCloseNotification;
+import org.thingsboard.server.common.msg.core.SessionOpenMsg;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.msg.core.ToDeviceRpcRequestMsg;
+import org.thingsboard.server.common.msg.core.ToDeviceRpcResponseMsg;
+import org.thingsboard.server.common.msg.core.ToServerRpcRequestMsg;
+import org.thingsboard.server.common.msg.device.DeviceToDeviceActorMsg;
import org.thingsboard.server.common.msg.kv.BasicAttributeKVMsg;
+import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
import org.thingsboard.server.common.msg.session.SessionType;
import org.thingsboard.server.common.msg.session.ToDeviceMsg;
-import org.thingsboard.server.extensions.api.device.*;
-import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
-import org.thingsboard.server.extensions.api.plugins.msg.RpcError;
-import org.thingsboard.server.extensions.api.plugins.msg.TimeoutIntMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestBody;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg;
-
-import java.util.*;
-import java.util.concurrent.ExecutionException;
+import org.thingsboard.server.common.msg.timeout.DeviceActorClientSideRpcTimeoutMsg;
+import org.thingsboard.server.common.msg.timeout.DeviceActorQueueTimeoutMsg;
+import org.thingsboard.server.common.msg.timeout.DeviceActorServerSideRpcTimeoutMsg;
+import org.thingsboard.server.service.rpc.FromDeviceRpcResponse;
+import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg;
+import org.thingsboard.server.service.rpc.ToServerRpcResponseActorMsg;
+
+import javax.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -59,46 +97,46 @@ import java.util.stream.Collectors;
*/
public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
+ private final TenantId tenantId;
private final DeviceId deviceId;
private final Map<SessionId, SessionInfo> sessions;
private final Map<SessionId, SessionInfo> attributeSubscriptions;
private final Map<SessionId, SessionInfo> rpcSubscriptions;
+ private final Map<Integer, ToDeviceRpcRequestMetadata> toDeviceRpcPendingMap;
+ private final Map<Integer, ToServerRpcRequestMetadata> toServerRpcPendingMap;
+ private final Map<UUID, PendingSessionMsgData> pendingMsgs;
- private final Map<Integer, ToDeviceRpcRequestMetadata> rpcPendingMap;
+ private final Gson gson = new Gson();
+ private final JsonParser jsonParser = new JsonParser();
private int rpcSeq = 0;
private String deviceName;
private String deviceType;
- private DeviceAttributes deviceAttributes;
+ private TbMsgMetaData defaultMetaData;
- public DeviceActorMessageProcessor(ActorSystemContext systemContext, LoggingAdapter logger, DeviceId deviceId) {
+ public DeviceActorMessageProcessor(ActorSystemContext systemContext, LoggingAdapter logger, TenantId tenantId, DeviceId deviceId) {
super(systemContext, logger);
+ this.tenantId = tenantId;
this.deviceId = deviceId;
- this.sessions = new HashMap<>();
+ this.sessions = new LinkedHashMap<>();
this.attributeSubscriptions = new HashMap<>();
this.rpcSubscriptions = new HashMap<>();
- this.rpcPendingMap = new HashMap<>();
+ this.toDeviceRpcPendingMap = new HashMap<>();
+ this.toServerRpcPendingMap = new HashMap<>();
+ this.pendingMsgs = new HashMap<>();
initAttributes();
}
private void initAttributes() {
- //TODO: add invalidation of deviceType cache.
Device device = systemContext.getDeviceService().findDeviceById(deviceId);
this.deviceName = device.getName();
this.deviceType = device.getType();
- this.deviceAttributes = new DeviceAttributes(fetchAttributes(DataConstants.CLIENT_SCOPE),
- fetchAttributes(DataConstants.SERVER_SCOPE), fetchAttributes(DataConstants.SHARED_SCOPE));
+ this.defaultMetaData = new TbMsgMetaData();
+ this.defaultMetaData.putValue("deviceName", deviceName);
+ this.defaultMetaData.putValue("deviceType", deviceType);
}
- private void refreshAttributes(DeviceAttributesEventNotificationMsg msg) {
- if (msg.isDeleted()) {
- msg.getDeletedKeys().forEach(key -> deviceAttributes.remove(key));
- } else {
- deviceAttributes.update(msg.getScope(), msg.getValues());
- }
- }
-
- void processRpcRequest(ActorContext context, ToDeviceRpcRequestPluginMsg msg) {
+ void processRpcRequest(ActorContext context, ToDeviceRpcRequestActorMsg msg) {
ToDeviceRpcRequest request = msg.getMsg();
ToDeviceRpcRequestBody body = request.getBody();
ToDeviceRpcRequestMsg rpcRequest = new ToDeviceRpcRequestMsg(
@@ -116,7 +154,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
boolean sent = rpcSubscriptions.size() > 0;
Set<SessionId> syncSessionSet = new HashSet<>();
rpcSubscriptions.entrySet().forEach(sub -> {
- ToDeviceSessionActorMsg response = new BasicToDeviceSessionActorMsg(rpcRequest, sub.getKey());
+ ActorSystemToDeviceSessionActorMsg response = new BasicActorSystemToDeviceSessionActorMsg(rpcRequest, sub.getKey());
sendMsgToSessionActor(response, sub.getValue().getServer());
if (SessionType.SYNC == sub.getValue().getType()) {
syncSessionSet.add(sub.getKey());
@@ -125,9 +163,8 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
syncSessionSet.forEach(rpcSubscriptions::remove);
if (request.isOneway() && sent) {
- ToPluginRpcResponseDeviceMsg responsePluginMsg = toPluginRpcResponseMsg(msg, (String) null);
- context.parent().tell(responsePluginMsg, ActorRef.noSender());
logger.debug("[{}] Rpc command response sent [{}]!", deviceId, request.getId());
+ systemContext.getDeviceRpcService().processRpcResponseFromDevice(new FromDeviceRpcResponse(msg.getMsg().getId(), msg.getServerAddress(), null, null));
} else {
registerPendingRpcRequest(context, msg, sent, rpcRequest, timeout);
}
@@ -139,24 +176,46 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
- private void registerPendingRpcRequest(ActorContext context, ToDeviceRpcRequestPluginMsg msg, boolean sent, ToDeviceRpcRequestMsg rpcRequest, long timeout) {
- rpcPendingMap.put(rpcRequest.getRequestId(), new ToDeviceRpcRequestMetadata(msg, sent));
- TimeoutIntMsg timeoutMsg = new TimeoutIntMsg(rpcRequest.getRequestId(), timeout);
+ private void registerPendingRpcRequest(ActorContext context, ToDeviceRpcRequestActorMsg msg, boolean sent, ToDeviceRpcRequestMsg rpcRequest, long timeout) {
+ toDeviceRpcPendingMap.put(rpcRequest.getRequestId(), new ToDeviceRpcRequestMetadata(msg, sent));
+ DeviceActorServerSideRpcTimeoutMsg timeoutMsg = new DeviceActorServerSideRpcTimeoutMsg(rpcRequest.getRequestId(), timeout);
scheduleMsgWithDelay(context, timeoutMsg, timeoutMsg.getTimeout());
}
- public void processTimeout(ActorContext context, TimeoutMsg msg) {
- ToDeviceRpcRequestMetadata requestMd = rpcPendingMap.remove(msg.getId());
+ void processServerSideRpcTimeout(ActorContext context, DeviceActorServerSideRpcTimeoutMsg msg) {
+ ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(msg.getId());
if (requestMd != null) {
logger.debug("[{}] RPC request [{}] timeout detected!", deviceId, msg.getId());
- ToPluginRpcResponseDeviceMsg responsePluginMsg = toPluginRpcResponseMsg(requestMd.getMsg(), requestMd.isSent() ? RpcError.TIMEOUT : RpcError.NO_ACTIVE_CONNECTION);
- context.parent().tell(responsePluginMsg, ActorRef.noSender());
+ systemContext.getDeviceRpcService().processRpcResponseFromDevice(new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(),
+ requestMd.getMsg().getServerAddress(), null, requestMd.isSent() ? RpcError.TIMEOUT : RpcError.NO_ACTIVE_CONNECTION));
+ }
+ }
+
+ void processQueueTimeout(ActorContext context, DeviceActorQueueTimeoutMsg msg) {
+ PendingSessionMsgData data = pendingMsgs.remove(msg.getId());
+ if (data != null) {
+ logger.debug("[{}] Queue put [{}] timeout detected!", deviceId, msg.getId());
+ ToDeviceMsg toDeviceMsg = new RuleEngineErrorMsg(data.getSessionMsgType(), RuleEngineError.QUEUE_PUT_TIMEOUT);
+ sendMsgToSessionActor(new BasicActorSystemToDeviceSessionActorMsg(toDeviceMsg, data.getSessionId()), data.getServerAddress());
+ }
+ }
+
+ void processQueueAck(ActorContext context, RuleEngineQueuePutAckMsg msg) {
+ PendingSessionMsgData data = pendingMsgs.remove(msg.getId());
+ if (data != null && data.isReplyOnQueueAck()) {
+ int remainingAcks = data.getAckMsgCount() - 1;
+ data.setAckMsgCount(remainingAcks);
+ logger.debug("[{}] Queue put [{}] ack detected. Remaining acks: {}!", deviceId, msg.getId(), remainingAcks);
+ if (remainingAcks == 0) {
+ ToDeviceMsg toDeviceMsg = BasicStatusCodeResponse.onSuccess(data.getSessionMsgType(), data.getRequestId());
+ sendMsgToSessionActor(new BasicActorSystemToDeviceSessionActorMsg(toDeviceMsg, data.getSessionId()), data.getServerAddress());
+ }
}
}
private void sendPendingRequests(ActorContext context, SessionId sessionId, SessionType type, Optional<ServerAddress> server) {
- if (!rpcPendingMap.isEmpty()) {
- logger.debug("[{}] Pushing {} pending RPC messages to new async session [{}]", deviceId, rpcPendingMap.size(), sessionId);
+ if (!toDeviceRpcPendingMap.isEmpty()) {
+ logger.debug("[{}] Pushing {} pending RPC messages to new async session [{}]", deviceId, toDeviceRpcPendingMap.size(), sessionId);
if (type == SessionType.SYNC) {
logger.debug("[{}] Cleanup sync rpc session [{}]", deviceId, sessionId);
rpcSubscriptions.remove(sessionId);
@@ -166,41 +225,196 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
Set<Integer> sentOneWayIds = new HashSet<>();
if (type == SessionType.ASYNC) {
- rpcPendingMap.entrySet().forEach(processPendingRpc(context, sessionId, server, sentOneWayIds));
+ toDeviceRpcPendingMap.entrySet().forEach(processPendingRpc(context, sessionId, server, sentOneWayIds));
} else {
- rpcPendingMap.entrySet().stream().findFirst().ifPresent(processPendingRpc(context, sessionId, server, sentOneWayIds));
+ toDeviceRpcPendingMap.entrySet().stream().findFirst().ifPresent(processPendingRpc(context, sessionId, server, sentOneWayIds));
}
- sentOneWayIds.forEach(rpcPendingMap::remove);
+ sentOneWayIds.forEach(toDeviceRpcPendingMap::remove);
}
private Consumer<Map.Entry<Integer, ToDeviceRpcRequestMetadata>> processPendingRpc(ActorContext context, SessionId sessionId, Optional<ServerAddress> server, Set<Integer> sentOneWayIds) {
return entry -> {
+ ToDeviceRpcRequestActorMsg requestActorMsg = entry.getValue().getMsg();
ToDeviceRpcRequest request = entry.getValue().getMsg().getMsg();
ToDeviceRpcRequestBody body = request.getBody();
if (request.isOneway()) {
sentOneWayIds.add(entry.getKey());
- ToPluginRpcResponseDeviceMsg responsePluginMsg = toPluginRpcResponseMsg(entry.getValue().getMsg(), (String) null);
- context.parent().tell(responsePluginMsg, ActorRef.noSender());
+ systemContext.getDeviceRpcService().processRpcResponseFromDevice(new FromDeviceRpcResponse(request.getId(), requestActorMsg.getServerAddress(), null, null));
}
ToDeviceRpcRequestMsg rpcRequest = new ToDeviceRpcRequestMsg(
entry.getKey(),
body.getMethod(),
body.getParams()
);
- ToDeviceSessionActorMsg response = new BasicToDeviceSessionActorMsg(rpcRequest, sessionId);
+ ActorSystemToDeviceSessionActorMsg response = new BasicActorSystemToDeviceSessionActorMsg(rpcRequest, sessionId);
sendMsgToSessionActor(response, server);
};
}
- void process(ActorContext context, ToDeviceActorMsg msg) {
+ void process(ActorContext context, DeviceToDeviceActorMsg msg) {
processSubscriptionCommands(context, msg);
processRpcResponses(context, msg);
processSessionStateMsgs(msg);
+
+ SessionMsgType sessionMsgType = msg.getPayload().getMsgType();
+ if (sessionMsgType.requiresRulesProcessing()) {
+ switch (sessionMsgType) {
+ case GET_ATTRIBUTES_REQUEST:
+ handleGetAttributesRequest(msg);
+ break;
+ case POST_ATTRIBUTES_REQUEST:
+ handlePostAttributesRequest(context, msg);
+ reportActivity();
+ break;
+ case POST_TELEMETRY_REQUEST:
+ handlePostTelemetryRequest(context, msg);
+ reportActivity();
+ break;
+ case TO_SERVER_RPC_REQUEST:
+ handleClientSideRPCRequest(context, msg);
+ reportActivity();
+ break;
+ }
+ }
+ }
+
+ private void reportActivity() {
+ systemContext.getDeviceStateService().onDeviceActivity(deviceId);
+ }
+
+ private void reportSessionOpen() {
+ systemContext.getDeviceStateService().onDeviceConnect(deviceId);
+ }
+
+ private void reportSessionClose() {
+ systemContext.getDeviceStateService().onDeviceDisconnect(deviceId);
+ }
+
+ private void handleGetAttributesRequest(DeviceToDeviceActorMsg src) {
+ GetAttributesRequest request = (GetAttributesRequest) src.getPayload();
+ ListenableFuture<List<AttributeKvEntry>> clientAttributesFuture = getAttributeKvEntries(deviceId, DataConstants.CLIENT_SCOPE, request.getClientAttributeNames());
+ ListenableFuture<List<AttributeKvEntry>> sharedAttributesFuture = getAttributeKvEntries(deviceId, DataConstants.SHARED_SCOPE, request.getClientAttributeNames());
+
+ Futures.addCallback(Futures.allAsList(Arrays.asList(clientAttributesFuture, sharedAttributesFuture)), new FutureCallback<List<List<AttributeKvEntry>>>() {
+ @Override
+ public void onSuccess(@Nullable List<List<AttributeKvEntry>> result) {
+ BasicGetAttributesResponse response = BasicGetAttributesResponse.onSuccess(request.getMsgType(),
+ request.getRequestId(), BasicAttributeKVMsg.from(result.get(0), result.get(1)));
+ sendMsgToSessionActor(new BasicActorSystemToDeviceSessionActorMsg(response, src.getSessionId()), src.getServerAddress());
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ if (t instanceof Exception) {
+ ToDeviceMsg toDeviceMsg = BasicStatusCodeResponse.onError(SessionMsgType.GET_ATTRIBUTES_REQUEST, request.getRequestId(), (Exception) t);
+ sendMsgToSessionActor(new BasicActorSystemToDeviceSessionActorMsg(toDeviceMsg, src.getSessionId()), src.getServerAddress());
+ } else {
+ logger.error("[{}] Failed to process attributes request", deviceId, t);
+ }
+ }
+ });
+ }
+
+ private ListenableFuture<List<AttributeKvEntry>> getAttributeKvEntries(DeviceId deviceId, String scope, Optional<Set<String>> names) {
+ if (names.isPresent()) {
+ if (!names.get().isEmpty()) {
+ return systemContext.getAttributesService().find(deviceId, scope, names.get());
+ } else {
+ return systemContext.getAttributesService().findAll(deviceId, scope);
+ }
+ } else {
+ return Futures.immediateFuture(Collections.emptyList());
+ }
+ }
+
+ private void handlePostAttributesRequest(ActorContext context, DeviceToDeviceActorMsg src) {
+ AttributesUpdateRequest request = (AttributesUpdateRequest) src.getPayload();
+
+ JsonObject json = new JsonObject();
+ for (AttributeKvEntry kv : request.getAttributes()) {
+ kv.getBooleanValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
+ kv.getLongValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
+ kv.getDoubleValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
+ kv.getStrValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
+ }
+
+ TbMsg tbMsg = new TbMsg(UUIDs.timeBased(), SessionMsgType.POST_ATTRIBUTES_REQUEST.name(), deviceId, defaultMetaData.copy(), TbMsgDataType.JSON, gson.toJson(json), null, null, 0L);
+ PendingSessionMsgData msgData = new PendingSessionMsgData(src.getSessionId(), src.getServerAddress(),
+ SessionMsgType.POST_ATTRIBUTES_REQUEST, request.getRequestId(), true, 1);
+ pushToRuleEngineWithTimeout(context, tbMsg, msgData);
+ }
+
+ private void handlePostTelemetryRequest(ActorContext context, DeviceToDeviceActorMsg src) {
+ TelemetryUploadRequest request = (TelemetryUploadRequest) src.getPayload();
+
+ Map<Long, List<KvEntry>> tsData = request.getData();
+
+ PendingSessionMsgData msgData = new PendingSessionMsgData(src.getSessionId(), src.getServerAddress(),
+ SessionMsgType.POST_TELEMETRY_REQUEST, request.getRequestId(), true, tsData.size());
+
+ for (Map.Entry<Long, List<KvEntry>> entry : tsData.entrySet()) {
+ JsonObject json = new JsonObject();
+ for (KvEntry kv : entry.getValue()) {
+ kv.getBooleanValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
+ kv.getLongValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
+ kv.getDoubleValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
+ kv.getStrValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
+ }
+ TbMsgMetaData metaData = defaultMetaData.copy();
+ metaData.putValue("ts", entry.getKey() + "");
+ TbMsg tbMsg = new TbMsg(UUIDs.timeBased(), SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, metaData, TbMsgDataType.JSON, gson.toJson(json), null, null, 0L);
+ pushToRuleEngineWithTimeout(context, tbMsg, msgData);
+ }
+ }
+
+ private void handleClientSideRPCRequest(ActorContext context, DeviceToDeviceActorMsg src) {
+ ToServerRpcRequestMsg request = (ToServerRpcRequestMsg) src.getPayload();
+
+ JsonObject json = new JsonObject();
+ json.addProperty("method", request.getMethod());
+ json.add("params", jsonParser.parse(request.getParams()));
+
+ TbMsgMetaData requestMetaData = defaultMetaData.copy();
+ requestMetaData.putValue("requestId", Integer.toString(request.getRequestId()));
+ TbMsg tbMsg = new TbMsg(UUIDs.timeBased(), SessionMsgType.TO_SERVER_RPC_REQUEST.name(), deviceId, requestMetaData, TbMsgDataType.JSON, gson.toJson(json), null, null, 0L);
+ PendingSessionMsgData msgData = new PendingSessionMsgData(src.getSessionId(), src.getServerAddress(), SessionMsgType.TO_SERVER_RPC_REQUEST, request.getRequestId(), false, 1);
+ pushToRuleEngineWithTimeout(context, tbMsg, msgData);
+
+ scheduleMsgWithDelay(context, new DeviceActorClientSideRpcTimeoutMsg(request.getRequestId(), systemContext.getClientSideRpcTimeout()), systemContext.getClientSideRpcTimeout());
+ toServerRpcPendingMap.put(request.getRequestId(), new ToServerRpcRequestMetadata(src.getSessionId(), src.getSessionType(), src.getServerAddress()));
+ }
+
+ public void processClientSideRpcTimeout(ActorContext context, DeviceActorClientSideRpcTimeoutMsg msg) {
+ ToServerRpcRequestMetadata data = toServerRpcPendingMap.remove(msg.getId());
+ if (data != null) {
+ logger.debug("[{}] Client side RPC request [{}] timeout detected!", deviceId, msg.getId());
+ ToDeviceMsg toDeviceMsg = new RuleEngineErrorMsg(SessionMsgType.TO_SERVER_RPC_REQUEST, RuleEngineError.TIMEOUT);
+ sendMsgToSessionActor(new BasicActorSystemToDeviceSessionActorMsg(toDeviceMsg, data.getSessionId()), data.getServer());
+ }
+ }
+
+ void processToServerRPCResponse(ActorContext context, ToServerRpcResponseActorMsg msg) {
+ ToServerRpcRequestMetadata data = toServerRpcPendingMap.remove(msg.getMsg().getRequestId());
+ if (data != null) {
+ sendMsgToSessionActor(new BasicActorSystemToDeviceSessionActorMsg(msg.getMsg(), data.getSessionId()), data.getServer());
+ }
+ }
+
+ private void pushToRuleEngineWithTimeout(ActorContext context, TbMsg tbMsg, PendingSessionMsgData pendingMsgData) {
+ SessionMsgType sessionMsgType = pendingMsgData.getSessionMsgType();
+ int requestId = pendingMsgData.getRequestId();
+ if (systemContext.isQueuePersistenceEnabled()) {
+ pendingMsgs.put(tbMsg.getId(), pendingMsgData);
+ scheduleMsgWithDelay(context, new DeviceActorQueueTimeoutMsg(tbMsg.getId(), systemContext.getQueuePersistenceTimeout()), systemContext.getQueuePersistenceTimeout());
+ } else {
+ ActorSystemToDeviceSessionActorMsg response = new BasicActorSystemToDeviceSessionActorMsg(BasicStatusCodeResponse.onSuccess(sessionMsgType, requestId), pendingMsgData.getSessionId());
+ sendMsgToSessionActor(response, pendingMsgData.getServerAddress());
+ }
+ context.parent().tell(new DeviceActorToRuleEngineMsg(context.self(), tbMsg), context.self());
}
void processAttributesUpdate(ActorContext context, DeviceAttributesEventNotificationMsg msg) {
- refreshAttributes(msg);
if (attributeSubscriptions.size() > 0) {
ToDeviceMsg notification = null;
if (msg.isDeleted()) {
@@ -221,7 +435,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
if (notification != null) {
ToDeviceMsg finalNotification = notification;
attributeSubscriptions.entrySet().forEach(sub -> {
- ToDeviceSessionActorMsg response = new BasicToDeviceSessionActorMsg(finalNotification, sub.getKey());
+ ActorSystemToDeviceSessionActorMsg response = new BasicActorSystemToDeviceSessionActorMsg(finalNotification, sub.getKey());
sendMsgToSessionActor(response, sub.getValue().getServer());
});
}
@@ -230,50 +444,30 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
}
- void process(ActorContext context, RuleChainDeviceMsg srcMsg) {
- ChainProcessingMetaData md = new ChainProcessingMetaData(srcMsg.getRuleChain(),
- srcMsg.getToDeviceActorMsg(), new DeviceMetaData(deviceId, deviceName, deviceType, deviceAttributes), context.self());
- ChainProcessingContext ctx = new ChainProcessingContext(md);
- if (ctx.getChainLength() > 0) {
- RuleProcessingMsg msg = new RuleProcessingMsg(ctx);
- ActorRef ruleActorRef = ctx.getCurrentActor();
- ruleActorRef.tell(msg, ActorRef.noSender());
- } else {
- context.self().tell(new RulesProcessedMsg(ctx), context.self());
- }
- }
-
- void processRpcResponses(ActorContext context, ToDeviceActorMsg msg) {
+ private void processRpcResponses(ActorContext context, DeviceToDeviceActorMsg msg) {
SessionId sessionId = msg.getSessionId();
FromDeviceMsg inMsg = msg.getPayload();
- if (inMsg.getMsgType() == MsgType.TO_DEVICE_RPC_RESPONSE) {
+ if (inMsg.getMsgType() == SessionMsgType.TO_DEVICE_RPC_RESPONSE) {
logger.debug("[{}] Processing rpc command response [{}]", deviceId, sessionId);
ToDeviceRpcResponseMsg responseMsg = (ToDeviceRpcResponseMsg) inMsg;
- ToDeviceRpcRequestMetadata requestMd = rpcPendingMap.remove(responseMsg.getRequestId());
+ ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(responseMsg.getRequestId());
boolean success = requestMd != null;
if (success) {
- ToPluginRpcResponseDeviceMsg responsePluginMsg = toPluginRpcResponseMsg(requestMd.getMsg(), responseMsg.getData());
- Optional<ServerAddress> pluginServerAddress = requestMd.getMsg().getServerAddress();
- if (pluginServerAddress.isPresent()) {
- systemContext.getRpcService().tell(pluginServerAddress.get(), responsePluginMsg);
- logger.debug("[{}] Rpc command response sent to remote plugin actor [{}]!", deviceId, requestMd.getMsg().getMsg().getId());
- } else {
- context.parent().tell(responsePluginMsg, ActorRef.noSender());
- logger.debug("[{}] Rpc command response sent to local plugin actor [{}]!", deviceId, requestMd.getMsg().getMsg().getId());
- }
+ systemContext.getDeviceRpcService().processRpcResponseFromDevice(new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(),
+ requestMd.getMsg().getServerAddress(), responseMsg.getData(), null));
} else {
logger.debug("[{}] Rpc command response [{}] is stale!", deviceId, responseMsg.getRequestId());
}
if (msg.getSessionType() == SessionType.SYNC) {
BasicCommandAckResponse response = success
- ? BasicCommandAckResponse.onSuccess(MsgType.TO_DEVICE_RPC_REQUEST, responseMsg.getRequestId())
- : BasicCommandAckResponse.onError(MsgType.TO_DEVICE_RPC_REQUEST, responseMsg.getRequestId(), new TimeoutException());
- sendMsgToSessionActor(new BasicToDeviceSessionActorMsg(response, msg.getSessionId()), msg.getServerAddress());
+ ? BasicCommandAckResponse.onSuccess(SessionMsgType.TO_DEVICE_RPC_REQUEST, responseMsg.getRequestId())
+ : BasicCommandAckResponse.onError(SessionMsgType.TO_DEVICE_RPC_REQUEST, responseMsg.getRequestId(), new TimeoutException());
+ sendMsgToSessionActor(new BasicActorSystemToDeviceSessionActorMsg(response, msg.getSessionId()), msg.getServerAddress());
}
}
}
- public void processClusterEventMsg(ClusterEventMsg msg) {
+ void processClusterEventMsg(ClusterEventMsg msg) {
if (!msg.isAdded()) {
logger.debug("[{}] Clearing attributes/rpc subscription for server [{}]", deviceId, msg.getServerAddress());
Predicate<Map.Entry<SessionId, SessionInfo>> filter = e -> e.getValue().getServer()
@@ -283,102 +477,79 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
}
- private ToPluginRpcResponseDeviceMsg toPluginRpcResponseMsg(ToDeviceRpcRequestPluginMsg requestMsg, String data) {
- return toPluginRpcResponseMsg(requestMsg, data, null);
- }
-
- private ToPluginRpcResponseDeviceMsg toPluginRpcResponseMsg(ToDeviceRpcRequestPluginMsg requestMsg, RpcError error) {
- return toPluginRpcResponseMsg(requestMsg, null, error);
- }
-
- private ToPluginRpcResponseDeviceMsg toPluginRpcResponseMsg(ToDeviceRpcRequestPluginMsg requestMsg, String data, RpcError error) {
- return new ToPluginRpcResponseDeviceMsg(
- requestMsg.getPluginId(),
- requestMsg.getPluginTenantId(),
- new FromDeviceRpcResponse(requestMsg.getMsg().getId(),
- data,
- error
- )
- );
- }
-
- void onRulesProcessedMsg(ActorContext context, RulesProcessedMsg msg) {
- ChainProcessingContext ctx = msg.getCtx();
- ToDeviceActorMsg inMsg = ctx.getInMsg();
- SessionId sid = inMsg.getSessionId();
- ToDeviceSessionActorMsg response;
- if (ctx.getResponse() != null) {
- response = new BasicToDeviceSessionActorMsg(ctx.getResponse(), sid);
- } else {
- response = new BasicToDeviceSessionActorMsg(ctx.getError(), sid);
- }
- sendMsgToSessionActor(response, inMsg.getServerAddress());
- }
-
- private void processSubscriptionCommands(ActorContext context, ToDeviceActorMsg msg) {
+ private void processSubscriptionCommands(ActorContext context, DeviceToDeviceActorMsg msg) {
SessionId sessionId = msg.getSessionId();
SessionType sessionType = msg.getSessionType();
FromDeviceMsg inMsg = msg.getPayload();
- if (inMsg.getMsgType() == MsgType.SUBSCRIBE_ATTRIBUTES_REQUEST) {
+ if (inMsg.getMsgType() == SessionMsgType.SUBSCRIBE_ATTRIBUTES_REQUEST) {
logger.debug("[{}] Registering attributes subscription for session [{}]", deviceId, sessionId);
attributeSubscriptions.put(sessionId, new SessionInfo(sessionType, msg.getServerAddress()));
- } else if (inMsg.getMsgType() == MsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST) {
+ } else if (inMsg.getMsgType() == SessionMsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST) {
logger.debug("[{}] Canceling attributes subscription for session [{}]", deviceId, sessionId);
attributeSubscriptions.remove(sessionId);
- } else if (inMsg.getMsgType() == MsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST) {
+ } else if (inMsg.getMsgType() == SessionMsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST) {
logger.debug("[{}] Registering rpc subscription for session [{}][{}]", deviceId, sessionId, sessionType);
rpcSubscriptions.put(sessionId, new SessionInfo(sessionType, msg.getServerAddress()));
sendPendingRequests(context, sessionId, sessionType, msg.getServerAddress());
- } else if (inMsg.getMsgType() == MsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST) {
+ } else if (inMsg.getMsgType() == SessionMsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST) {
logger.debug("[{}] Canceling rpc subscription for session [{}][{}]", deviceId, sessionId, sessionType);
rpcSubscriptions.remove(sessionId);
}
}
- private void processSessionStateMsgs(ToDeviceActorMsg msg) {
+ private void processSessionStateMsgs(DeviceToDeviceActorMsg msg) {
SessionId sessionId = msg.getSessionId();
FromDeviceMsg inMsg = msg.getPayload();
if (inMsg instanceof SessionOpenMsg) {
logger.debug("[{}] Processing new session [{}]", deviceId, sessionId);
+ if (sessions.size() >= systemContext.getMaxConcurrentSessionsPerDevice()) {
+ SessionId sessionIdToRemove = sessions.keySet().stream().findFirst().orElse(null);
+ if (sessionIdToRemove != null) {
+ closeSession(sessionIdToRemove, sessions.remove(sessionIdToRemove));
+ }
+ }
sessions.put(sessionId, new SessionInfo(SessionType.ASYNC, msg.getServerAddress()));
+ if (sessions.size() == 1) {
+ reportSessionOpen();
+ }
} else if (inMsg instanceof SessionCloseMsg) {
logger.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId);
sessions.remove(sessionId);
attributeSubscriptions.remove(sessionId);
rpcSubscriptions.remove(sessionId);
+ if (sessions.isEmpty()) {
+ reportSessionClose();
+ }
}
}
- private void sendMsgToSessionActor(ToDeviceSessionActorMsg response, Optional<ServerAddress> sessionAddress) {
+ private void sendMsgToSessionActor(ActorSystemToDeviceSessionActorMsg response, Optional<ServerAddress> sessionAddress) {
if (sessionAddress.isPresent()) {
ServerAddress address = sessionAddress.get();
logger.debug("{} Forwarding msg: {}", address, response);
- systemContext.getRpcService().tell(sessionAddress.get(), response);
+ systemContext.getRpcService().tell(systemContext.getEncodingService()
+ .convertToProtoDataMessage(sessionAddress.get(), response));
} else {
systemContext.getSessionManagerActor().tell(response, ActorRef.noSender());
}
}
- private List<AttributeKvEntry> fetchAttributes(String scope) {
- try {
- //TODO: replace this with async operation. Happens only during actor creation, but is still criticla for performance,
- return systemContext.getAttributesService().findAll(this.deviceId, scope).get();
- } catch (InterruptedException | ExecutionException e) {
- logger.warning("[{}] Failed to fetch attributes for scope: {}", deviceId, scope);
- throw new RuntimeException(e);
- }
- }
-
- public void processCredentialsUpdate() {
- sessions.forEach((k, v) -> {
- sendMsgToSessionActor(new BasicToDeviceSessionActorMsg(new SessionCloseNotification(), k), v.getServer());
- });
+ void processCredentialsUpdate() {
+ sessions.forEach(this::closeSession);
attributeSubscriptions.clear();
rpcSubscriptions.clear();
}
- public void processNameOrTypeUpdate(DeviceNameOrTypeUpdateMsg msg) {
+ private void closeSession(SessionId sessionId, SessionInfo sessionInfo) {
+ sendMsgToSessionActor(new BasicActorSystemToDeviceSessionActorMsg(new SessionCloseNotification(), sessionId), sessionInfo.getServer());
+ }
+
+ void processNameOrTypeUpdate(DeviceNameOrTypeUpdateMsg msg) {
this.deviceName = msg.getDeviceName();
this.deviceType = msg.getDeviceType();
+ this.defaultMetaData = new TbMsgMetaData();
+ this.defaultMetaData.putValue("deviceName", deviceName);
+ this.defaultMetaData.putValue("deviceType", deviceType);
}
+
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/PendingSessionMsgData.java b/application/src/main/java/org/thingsboard/server/actors/device/PendingSessionMsgData.java
new file mode 100644
index 0000000..23ad966
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/device/PendingSessionMsgData.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.device;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+
+import java.util.Optional;
+
+/**
+ * Created by ashvayka on 17.04.18.
+ */
+@Data
+@AllArgsConstructor
+public final class PendingSessionMsgData {
+
+ private final SessionId sessionId;
+ private final Optional<ServerAddress> serverAddress;
+ private final SessionMsgType sessionMsgType;
+ private final int requestId;
+ private final boolean replyOnQueueAck;
+ private int ackMsgCount;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java b/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java
index e039b96..04c457c 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java
@@ -16,7 +16,6 @@
package org.thingsboard.server.actors.device;
import lombok.Data;
-import org.thingsboard.server.common.data.id.SessionId;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.msg.session.SessionType;
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java b/application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java
index 01342fd..8a4262c 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java
@@ -16,13 +16,13 @@
package org.thingsboard.server.actors.device;
import lombok.Data;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
+import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg;
/**
* @author Andrew Shvayka
*/
@Data
public class ToDeviceRpcRequestMetadata {
- private final ToDeviceRpcRequestPluginMsg msg;
+ private final ToDeviceRpcRequestActorMsg msg;
private final boolean sent;
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java b/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
index 4fa2227..14bb636 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
@@ -17,43 +17,23 @@ package org.thingsboard.server.actors.rpc;
import akka.actor.ActorRef;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.util.SerializationUtils;
-import org.springframework.util.StringUtils;
-import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.service.ActorService;
-import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.*;
-import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
-import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
import org.thingsboard.server.service.cluster.rpc.GrpcSession;
import org.thingsboard.server.service.cluster.rpc.GrpcSessionListener;
-import java.io.Serializable;
-import java.util.UUID;
-
/**
* @author Andrew Shvayka
*/
@Slf4j
public class BasicRpcSessionListener implements GrpcSessionListener {
- public static final String SESSION_RECEIVED_SESSION_ACTOR_MSG = "{} session [{}] received session actor msg {}";
- private final ActorSystemContext context;
private final ActorService service;
private final ActorRef manager;
private final ActorRef self;
- public BasicRpcSessionListener(ActorSystemContext context, ActorRef manager, ActorRef self) {
- this.context = context;
- this.service = context.getActorService();
+ public BasicRpcSessionListener(ActorService service, ActorRef manager, ActorRef self) {
+ this.service = service;
this.manager = manager;
this.self = self;
}
@@ -73,47 +53,11 @@ public class BasicRpcSessionListener implements GrpcSessionListener {
}
@Override
- public void onToPluginRpcMsg(GrpcSession session, ClusterAPIProtos.ToPluginRpcMessage msg) {
- if (log.isTraceEnabled()) {
- log.trace("{} session [{}] received plugin msg {}", getType(session), session.getRemoteServer(), msg);
- }
- service.onMsg(convert(session.getRemoteServer(), msg));
- }
-
- @Override
- public void onToDeviceActorRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceActorRpcMessage msg) {
- log.trace("{} session [{}] received device actor msg {}", getType(session), session.getRemoteServer(), msg);
- service.onMsg((ToDeviceActorMsg) deserialize(msg.getData().toByteArray()));
- }
-
- @Override
- public void onToDeviceActorNotificationRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceActorNotificationRpcMessage msg) {
- log.trace("{} session [{}] received device actor notification msg {}", getType(session), session.getRemoteServer(), msg);
- service.onMsg((ToDeviceActorNotificationMsg) deserialize(msg.getData().toByteArray()));
- }
-
- @Override
- public void onToDeviceSessionActorRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceSessionActorRpcMessage msg) {
- log.trace(SESSION_RECEIVED_SESSION_ACTOR_MSG, getType(session), session.getRemoteServer(), msg);
- service.onMsg((ToDeviceSessionActorMsg) deserialize(msg.getData().toByteArray()));
- }
-
- @Override
- public void onToDeviceRpcRequestRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceRpcRequestRpcMessage msg) {
- log.trace(SESSION_RECEIVED_SESSION_ACTOR_MSG, getType(session), session.getRemoteServer(), msg);
- service.onMsg(deserialize(session.getRemoteServer(), msg));
- }
-
- @Override
- public void onFromDeviceRpcResponseRpcMsg(GrpcSession session, ClusterAPIProtos.ToPluginRpcResponseRpcMessage msg) {
- log.trace(SESSION_RECEIVED_SESSION_ACTOR_MSG, getType(session), session.getRemoteServer(), msg);
- service.onMsg(deserialize(session.getRemoteServer(), msg));
- }
-
- @Override
- public void onToAllNodesRpcMessage(GrpcSession session, ClusterAPIProtos.ToAllNodesRpcMessage msg) {
- log.trace(SESSION_RECEIVED_SESSION_ACTOR_MSG, getType(session), session.getRemoteServer(), msg);
- service.onMsg((ToAllNodesMsg) deserialize(msg.getData().toByteArray()));
+ public void onReceiveClusterGrpcMsg(GrpcSession session, ClusterAPIProtos.ClusterMessage clusterMessage) {
+ log.trace("{} Service [{}] received session actor msg {}", getType(session),
+ session.getRemoteServer(),
+ clusterMessage);
+ service.onReceivedMsg(session.getRemoteServer(), clusterMessage);
}
@Override
@@ -127,45 +71,5 @@ public class BasicRpcSessionListener implements GrpcSessionListener {
return session.isClient() ? "Client" : "Server";
}
- private static PluginRpcMsg convert(ServerAddress serverAddress, ClusterAPIProtos.ToPluginRpcMessage msg) {
- ClusterAPIProtos.PluginAddress address = msg.getAddress();
- TenantId tenantId = new TenantId(toUUID(address.getTenantId()));
- PluginId pluginId = new PluginId(toUUID(address.getPluginId()));
- RpcMsg rpcMsg = new RpcMsg(serverAddress, msg.getClazz(), msg.getData().toByteArray());
- return new PluginRpcMsg(tenantId, pluginId, rpcMsg);
- }
-
- private static UUID toUUID(ClusterAPIProtos.Uid uid) {
- return new UUID(uid.getPluginUuidMsb(), uid.getPluginUuidLsb());
- }
-
- private static ToDeviceRpcRequestPluginMsg deserialize(ServerAddress serverAddress, ClusterAPIProtos.ToDeviceRpcRequestRpcMessage msg) {
- ClusterAPIProtos.PluginAddress address = msg.getAddress();
- TenantId pluginTenantId = new TenantId(toUUID(address.getTenantId()));
- PluginId pluginId = new PluginId(toUUID(address.getPluginId()));
-
- TenantId deviceTenantId = new TenantId(toUUID(msg.getDeviceTenantId()));
- DeviceId deviceId = new DeviceId(toUUID(msg.getDeviceId()));
-
- ToDeviceRpcRequestBody requestBody = new ToDeviceRpcRequestBody(msg.getMethod(), msg.getParams());
- ToDeviceRpcRequest request = new ToDeviceRpcRequest(toUUID(msg.getMsgId()), null, deviceTenantId, deviceId, msg.getOneway(), msg.getExpTime(), requestBody);
-
- return new ToDeviceRpcRequestPluginMsg(serverAddress, pluginId, pluginTenantId, request);
- }
-
- private static ToPluginRpcResponseDeviceMsg deserialize(ServerAddress serverAddress, ClusterAPIProtos.ToPluginRpcResponseRpcMessage msg) {
- ClusterAPIProtos.PluginAddress address = msg.getAddress();
- TenantId pluginTenantId = new TenantId(toUUID(address.getTenantId()));
- PluginId pluginId = new PluginId(toUUID(address.getPluginId()));
-
- RpcError error = !StringUtils.isEmpty(msg.getError()) ? RpcError.valueOf(msg.getError()) : null;
- FromDeviceRpcResponse response = new FromDeviceRpcResponse(toUUID(msg.getMsgId()), msg.getResponse(), error);
- return new ToPluginRpcResponseDeviceMsg(pluginId, pluginTenantId, response);
- }
-
- @SuppressWarnings("unchecked")
- private static <T extends Serializable> T deserialize(byte[] data) {
- return (T) SerializationUtils.deserialize(data);
- }
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcBroadcastMsg.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcBroadcastMsg.java
index 3718a22..2dd949e 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcBroadcastMsg.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcBroadcastMsg.java
@@ -23,5 +23,5 @@ import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
*/
@Data
public final class RpcBroadcastMsg {
- private final ClusterAPIProtos.ToRpcServerMessage msg;
+ private final ClusterAPIProtos.ClusterMessage msg;
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
index 9290a8f..3f3f70b 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
@@ -23,12 +23,17 @@ import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.service.ContextAwareActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.actors.service.DefaultActorService;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
import org.thingsboard.server.service.cluster.discovery.ServerInstance;
-import java.util.*;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+import java.util.UUID;
/**
* @author Andrew Shvayka
@@ -39,7 +44,7 @@ public class RpcManagerActor extends ContextAwareActor {
private final Map<ServerAddress, SessionActorInfo> sessionActors;
- private final Map<ServerAddress, Queue<ClusterAPIProtos.ToRpcServerMessage>> pendingMsgs;
+ private final Map<ServerAddress, Queue<ClusterAPIProtos.ClusterMessage>> pendingMsgs;
private final ServerAddress instance;
@@ -57,9 +62,15 @@ public class RpcManagerActor extends ContextAwareActor {
}
@Override
+ protected boolean process(TbActorMsg msg) {
+ //TODO Move everything here, to work with TbActorMsg
+ return false;
+ }
+
+ @Override
public void onReceive(Object msg) throws Exception {
- if (msg instanceof RpcSessionTellMsg) {
- onMsg((RpcSessionTellMsg) msg);
+ if (msg instanceof ClusterAPIProtos.ClusterMessage) {
+ onMsg((ClusterAPIProtos.ClusterMessage) msg);
} else if (msg instanceof RpcBroadcastMsg) {
onMsg((RpcBroadcastMsg) msg);
} else if (msg instanceof RpcSessionCreateRequestMsg) {
@@ -77,24 +88,30 @@ public class RpcManagerActor extends ContextAwareActor {
private void onMsg(RpcBroadcastMsg msg) {
log.debug("Forwarding msg to session actors {}", msg);
- sessionActors.keySet().forEach(address -> onMsg(new RpcSessionTellMsg(address, msg.getMsg())));
+ sessionActors.keySet().forEach(address -> onMsg(msg.getMsg()));
pendingMsgs.values().forEach(queue -> queue.add(msg.getMsg()));
}
- private void onMsg(RpcSessionTellMsg msg) {
- ServerAddress address = msg.getServerAddress();
- SessionActorInfo session = sessionActors.get(address);
- if (session != null) {
- log.debug("{} Forwarding msg to session actor", address);
- session.actor.tell(msg, ActorRef.noSender());
- } else {
- log.debug("{} Storing msg to pending queue", address);
- Queue<ClusterAPIProtos.ToRpcServerMessage> queue = pendingMsgs.get(address);
- if (queue == null) {
- queue = new LinkedList<>();
- pendingMsgs.put(address, queue);
+ private void onMsg(ClusterAPIProtos.ClusterMessage msg) {
+ if (msg.hasServerAddress()) {
+ ServerAddress address = new ServerAddress(msg.getServerAddress().getHost(),
+ msg.getServerAddress().getPort());
+ SessionActorInfo session = sessionActors.get(address);
+ if (session != null) {
+ log.debug("{} Forwarding msg to session actor", address);
+ session.getActor().tell(msg, ActorRef.noSender());
+ } else {
+ log.debug("{} Storing msg to pending queue", address);
+ Queue<ClusterAPIProtos.ClusterMessage> queue = pendingMsgs.get(address);
+ if (queue == null) {
+ queue = new LinkedList<>();
+ pendingMsgs.put(new ServerAddress(
+ msg.getServerAddress().getHost(), msg.getServerAddress().getPort()), queue);
+ }
+ queue.add(msg);
}
- queue.add(msg.getMsg());
+ } else {
+ logger.warning("Cluster msg doesn't have set Server Address [{}]", msg);
}
}
@@ -141,7 +158,7 @@ public class RpcManagerActor extends ContextAwareActor {
private void onSessionClose(boolean reconnect, ServerAddress remoteAddress) {
log.debug("[{}] session closed. Should reconnect: {}", remoteAddress, reconnect);
SessionActorInfo sessionRef = sessionActors.get(remoteAddress);
- if (context().sender().equals(sessionRef.actor)) {
+ if (context().sender() != null && context().sender().equals(sessionRef.actor)) {
sessionActors.remove(remoteAddress);
pendingMsgs.remove(remoteAddress);
if (reconnect) {
@@ -160,10 +177,10 @@ public class RpcManagerActor extends ContextAwareActor {
private void register(ServerAddress remoteAddress, UUID uuid, ActorRef sender) {
sessionActors.put(remoteAddress, new SessionActorInfo(uuid, sender));
log.debug("[{}][{}] Registering session actor.", remoteAddress, uuid);
- Queue<ClusterAPIProtos.ToRpcServerMessage> data = pendingMsgs.remove(remoteAddress);
+ Queue<ClusterAPIProtos.ClusterMessage> data = pendingMsgs.remove(remoteAddress);
if (data != null) {
log.debug("[{}][{}] Forwarding {} pending messages.", remoteAddress, uuid, data.size());
- data.forEach(msg -> sender.tell(new RpcSessionTellMsg(remoteAddress, msg), ActorRef.noSender()));
+ data.forEach(msg -> sender.tell(new RpcSessionTellMsg(msg), ActorRef.noSender()));
} else {
log.debug("[{}][{}] No pending messages to forward.", remoteAddress, uuid);
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
index db029fa..c9cf869 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
@@ -23,6 +23,7 @@ import io.grpc.stub.StreamObserver;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.service.ContextAwareActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
import org.thingsboard.server.gen.cluster.ClusterRpcServiceGrpc;
@@ -31,6 +32,8 @@ import org.thingsboard.server.service.cluster.rpc.GrpcSessionListener;
import java.util.UUID;
+import static org.thingsboard.server.gen.cluster.ClusterAPIProtos.MessageType.CONNECT_RPC_MESSAGE;
+
/**
* @author Andrew Shvayka
*/
@@ -48,16 +51,22 @@ public class RpcSessionActor extends ContextAwareActor {
}
@Override
+ protected boolean process(TbActorMsg msg) {
+ //TODO Move everything here, to work with TbActorMsg
+ return false;
+ }
+
+ @Override
public void onReceive(Object msg) throws Exception {
- if (msg instanceof RpcSessionTellMsg) {
- tell((RpcSessionTellMsg) msg);
+ if (msg instanceof ClusterAPIProtos.ClusterMessage) {
+ tell((ClusterAPIProtos.ClusterMessage) msg);
} else if (msg instanceof RpcSessionCreateRequestMsg) {
initSession((RpcSessionCreateRequestMsg) msg);
}
}
- private void tell(RpcSessionTellMsg msg) {
- session.sendMsg(msg.getMsg());
+ private void tell(ClusterAPIProtos.ClusterMessage msg) {
+ session.sendMsg(msg);
}
@Override
@@ -69,7 +78,7 @@ public class RpcSessionActor extends ContextAwareActor {
private void initSession(RpcSessionCreateRequestMsg msg) {
log.info("[{}] Initializing session", context().self());
ServerAddress remoteServer = msg.getRemoteAddress();
- listener = new BasicRpcSessionListener(systemContext, context().parent(), context().self());
+ listener = new BasicRpcSessionListener(systemContext.getActorService(), context().parent(), context().self());
if (msg.getRemoteAddress() == null) {
// Server session
session = new GrpcSession(listener);
@@ -84,7 +93,7 @@ public class RpcSessionActor extends ContextAwareActor {
session.initInputStream();
ClusterRpcServiceGrpc.ClusterRpcServiceStub stub = ClusterRpcServiceGrpc.newStub(channel);
- StreamObserver<ClusterAPIProtos.ToRpcServerMessage> outputStream = stub.handlePluginMsgs(session.getInputStream());
+ StreamObserver<ClusterAPIProtos.ClusterMessage> outputStream = stub.handleMsgs(session.getInputStream());
session.setOutputStream(outputStream);
session.initOutputStream();
@@ -108,11 +117,10 @@ public class RpcSessionActor extends ContextAwareActor {
}
}
- private ClusterAPIProtos.ToRpcServerMessage toConnectMsg() {
+ private ClusterAPIProtos.ClusterMessage toConnectMsg() {
ServerAddress instance = systemContext.getDiscoveryService().getCurrentServer().getServerAddress();
- return ClusterAPIProtos.ToRpcServerMessage.newBuilder().setConnectMsg(
- ClusterAPIProtos.ConnectRpcMessage.newBuilder().setServerAddress(
- ClusterAPIProtos.ServerAddress.newBuilder().setHost(instance.getHost()).setPort(instance.getPort()).build()).build()).build();
-
+ return ClusterAPIProtos.ClusterMessage.newBuilder().setMessageType(CONNECT_RPC_MESSAGE).setServerAddress(
+ ClusterAPIProtos.ServerAddress.newBuilder().setHost(instance.getHost())
+ .setPort(instance.getPort()).build()).build();
}
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionCreateRequestMsg.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionCreateRequestMsg.java
index 5bcf1d6..0c1136e 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionCreateRequestMsg.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionCreateRequestMsg.java
@@ -30,6 +30,6 @@ public final class RpcSessionCreateRequestMsg {
private final UUID msgUid;
private final ServerAddress remoteAddress;
- private final StreamObserver<ClusterAPIProtos.ToRpcServerMessage> responseObserver;
+ private final StreamObserver<ClusterAPIProtos.ClusterMessage> responseObserver;
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionTellMsg.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionTellMsg.java
index 5a61044..50db43f 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionTellMsg.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionTellMsg.java
@@ -16,7 +16,6 @@
package org.thingsboard.server.actors.rpc;
import lombok.Data;
-import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
/**
@@ -24,6 +23,5 @@ import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
*/
@Data
public final class RpcSessionTellMsg {
- private final ServerAddress serverAddress;
- private final ClusterAPIProtos.ToRpcServerMessage msg;
+ private final ClusterAPIProtos.ClusterMessage msg;
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
new file mode 100644
index 0000000..bcadd0c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
@@ -0,0 +1,241 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import akka.actor.ActorRef;
+import com.datastax.driver.core.utils.UUIDs;
+import org.thingsboard.rule.engine.api.ListeningExecutor;
+import org.thingsboard.rule.engine.api.MailService;
+import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest;
+import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcResponse;
+import org.thingsboard.rule.engine.api.RuleEngineRpcService;
+import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
+import org.thingsboard.rule.engine.api.ScriptEngine;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbRelationTypes;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
+import org.thingsboard.server.dao.alarm.AlarmService;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.dao.user.UserService;
+import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
+import scala.concurrent.duration.Duration;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+class DefaultTbContext implements TbContext {
+
+ private final ActorSystemContext mainCtx;
+ private final RuleNodeCtx nodeCtx;
+
+ public DefaultTbContext(ActorSystemContext mainCtx, RuleNodeCtx nodeCtx) {
+ this.mainCtx = mainCtx;
+ this.nodeCtx = nodeCtx;
+ }
+
+ @Override
+ public void tellNext(TbMsg msg, String relationType) {
+ tellNext(msg, Collections.singleton(relationType), null);
+ }
+
+ @Override
+ public void tellNext(TbMsg msg, Set<String> relationTypes) {
+ tellNext(msg, relationTypes, null);
+ }
+
+ @Override
+ public void tellNext(TbMsg msg, String relationType, Throwable th) {
+ tellNext(msg, Collections.singleton(relationType), th);
+ }
+
+ private void tellNext(TbMsg msg, Set<String> relationTypes, Throwable th) {
+ if (nodeCtx.getSelf().isDebugMode()) {
+ relationTypes.forEach(relationType -> mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, relationType, th));
+ }
+ nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), relationTypes, msg), nodeCtx.getSelfActor());
+ }
+
+ @Override
+ public void tellSelf(TbMsg msg, long delayMs) {
+ //TODO: add persistence layer
+ scheduleMsgWithDelay(new RuleNodeToSelfMsg(msg), delayMs, nodeCtx.getSelfActor());
+ }
+
+ private void scheduleMsgWithDelay(Object msg, long delayInMs, ActorRef target) {
+ mainCtx.getScheduler().scheduleOnce(Duration.create(delayInMs, TimeUnit.MILLISECONDS), target, msg, mainCtx.getActorSystem().dispatcher(), nodeCtx.getSelfActor());
+ }
+
+ @Override
+ public void tellFailure(TbMsg msg, Throwable th) {
+ if (nodeCtx.getSelf().isDebugMode()) {
+ mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbRelationTypes.FAILURE, th);
+ }
+ nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), Collections.singleton(TbRelationTypes.FAILURE), msg), nodeCtx.getSelfActor());
+ }
+
+ @Override
+ public void updateSelf(RuleNode self) {
+ nodeCtx.setSelf(self);
+ }
+
+ @Override
+ public TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data) {
+ return new TbMsg(UUIDs.timeBased(), type, originator, metaData.copy(), data, nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId(), mainCtx.getQueuePartitionId());
+ }
+
+ @Override
+ public TbMsg transformMsg(TbMsg origMsg, String type, EntityId originator, TbMsgMetaData metaData, String data) {
+ return new TbMsg(origMsg.getId(), type, originator, metaData.copy(), data, origMsg.getRuleChainId(), origMsg.getRuleNodeId(), mainCtx.getQueuePartitionId());
+ }
+
+ @Override
+ public RuleNodeId getSelfId() {
+ return nodeCtx.getSelf().getId();
+ }
+
+ @Override
+ public TenantId getTenantId() {
+ return nodeCtx.getTenantId();
+ }
+
+ @Override
+ public ListeningExecutor getJsExecutor() {
+ return mainCtx.getJsExecutor();
+ }
+
+ @Override
+ public ListeningExecutor getMailExecutor() {
+ return mainCtx.getMailExecutor();
+ }
+
+ @Override
+ public ListeningExecutor getDbCallbackExecutor() {
+ return mainCtx.getDbCallbackExecutor();
+ }
+
+ @Override
+ public ListeningExecutor getExternalCallExecutor() {
+ return mainCtx.getExternalCallExecutorService();
+ }
+
+ @Override
+ public ScriptEngine createJsScriptEngine(String script, String... argNames) {
+ return new RuleNodeJsScriptEngine(mainCtx.getJsSandbox(), script, argNames);
+ }
+
+ @Override
+ public AttributesService getAttributesService() {
+ return mainCtx.getAttributesService();
+ }
+
+ @Override
+ public CustomerService getCustomerService() {
+ return mainCtx.getCustomerService();
+ }
+
+ @Override
+ public UserService getUserService() {
+ return mainCtx.getUserService();
+ }
+
+ @Override
+ public AssetService getAssetService() {
+ return mainCtx.getAssetService();
+ }
+
+ @Override
+ public DeviceService getDeviceService() {
+ return mainCtx.getDeviceService();
+ }
+
+ @Override
+ public AlarmService getAlarmService() {
+ return mainCtx.getAlarmService();
+ }
+
+ @Override
+ public RuleChainService getRuleChainService() {
+ return mainCtx.getRuleChainService();
+ }
+
+ @Override
+ public TimeseriesService getTimeseriesService() {
+ return mainCtx.getTsService();
+ }
+
+ @Override
+ public RuleEngineTelemetryService getTelemetryService() {
+ return mainCtx.getTsSubService();
+ }
+
+ @Override
+ public RelationService getRelationService() {
+ return mainCtx.getRelationService();
+ }
+
+ @Override
+ public MailService getMailService() {
+ if (mainCtx.isAllowSystemMailService()) {
+ return mainCtx.getMailService();
+ } else {
+ throw new RuntimeException("Access to System Mail Service is forbidden!");
+ }
+ }
+
+ @Override
+ public RuleEngineRpcService getRpcService() {
+ return new RuleEngineRpcService() {
+ @Override
+ public void sendRpcReply(DeviceId deviceId, int requestId, String body) {
+ mainCtx.getDeviceRpcService().sendRpcReplyToDevice(nodeCtx.getTenantId(), deviceId, requestId, body);
+ }
+
+ @Override
+ public void sendRpcRequest(RuleEngineDeviceRpcRequest src, Consumer<RuleEngineDeviceRpcResponse> consumer) {
+ ToDeviceRpcRequest request = new ToDeviceRpcRequest(UUIDs.timeBased(), nodeCtx.getTenantId(), src.getDeviceId(),
+ src.isOneway(), System.currentTimeMillis() + src.getTimeout(), new ToDeviceRpcRequestBody(src.getMethod(), src.getBody()));
+ mainCtx.getDeviceRpcService().processRpcRequestToDevice(request, response -> {
+ consumer.accept(RuleEngineDeviceRpcResponse.builder()
+ .deviceId(src.getDeviceId())
+ .requestId(src.getRequestId())
+ .error(response.getError())
+ .response(response.getResponse())
+ .build());
+ });
+ }
+ };
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java
new file mode 100644
index 0000000..3ba646a
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import akka.actor.OneForOneStrategy;
+import akka.actor.SupervisorStrategy;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.device.DeviceActorToRuleEngineMsg;
+import org.thingsboard.server.actors.service.ComponentActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import scala.concurrent.duration.Duration;
+
+public class RuleChainActor extends ComponentActor<RuleChainId, RuleChainActorMessageProcessor> {
+
+ private RuleChainActor(ActorSystemContext systemContext, TenantId tenantId, RuleChainId ruleChainId) {
+ super(systemContext, tenantId, ruleChainId);
+ setProcessor(new RuleChainActorMessageProcessor(tenantId, ruleChainId, systemContext,
+ logger, context().parent(), context().self()));
+ }
+
+ @Override
+ protected boolean process(TbActorMsg msg) {
+ switch (msg.getMsgType()) {
+ case COMPONENT_LIFE_CYCLE_MSG:
+ onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+ break;
+ case SERVICE_TO_RULE_ENGINE_MSG:
+ processor.onServiceToRuleEngineMsg((ServiceToRuleEngineMsg) msg);
+ break;
+ case DEVICE_ACTOR_TO_RULE_ENGINE_MSG:
+ processor.onDeviceActorToRuleEngineMsg((DeviceActorToRuleEngineMsg) msg);
+ break;
+ case RULE_TO_RULE_CHAIN_TELL_NEXT_MSG:
+ processor.onTellNext((RuleNodeToRuleChainTellNextMsg) msg);
+ break;
+ case RULE_CHAIN_TO_RULE_CHAIN_MSG:
+ processor.onRuleChainToRuleChainMsg((RuleChainToRuleChainMsg) msg);
+ break;
+ case CLUSTER_EVENT_MSG:
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ public static class ActorCreator extends ContextBasedCreator<RuleChainActor> {
+ private static final long serialVersionUID = 1L;
+
+ private final TenantId tenantId;
+ private final RuleChainId ruleChainId;
+
+ public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleChainId pluginId) {
+ super(context);
+ this.tenantId = tenantId;
+ this.ruleChainId = pluginId;
+ }
+
+ @Override
+ public RuleChainActor create() throws Exception {
+ return new RuleChainActor(context, tenantId, ruleChainId);
+ }
+ }
+
+ @Override
+ protected long getErrorPersistFrequency() {
+ return systemContext.getRuleChainErrorPersistFrequency();
+ }
+
+ @Override
+ public SupervisorStrategy supervisorStrategy() {
+ return strategy;
+ }
+
+ private final SupervisorStrategy strategy = new OneForOneStrategy(3, Duration.create("1 minute"), t -> {
+ logAndPersist("Unknown Failure", ActorSystemContext.toException(t));
+ return SupervisorStrategy.resume();
+ });
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
new file mode 100644
index 0000000..7d560db
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
@@ -0,0 +1,297 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.actor.Props;
+import akka.event.LoggingAdapter;
+import com.datastax.driver.core.utils.UUIDs;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.device.DeviceActorToRuleEngineMsg;
+import org.thingsboard.server.actors.device.RuleEngineQueuePutAckMsg;
+import org.thingsboard.server.actors.service.DefaultActorService;
+import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleChainId> {
+
+ private static final long DEFAULT_CLUSTER_PARTITION = 0L;
+ private final ActorRef parent;
+ private final ActorRef self;
+ private final Map<RuleNodeId, RuleNodeCtx> nodeActors;
+ private final Map<RuleNodeId, List<RuleNodeRelation>> nodeRoutes;
+ private final RuleChainService service;
+
+ private RuleNodeId firstId;
+ private RuleNodeCtx firstNode;
+ private boolean started;
+
+ RuleChainActorMessageProcessor(TenantId tenantId, RuleChainId ruleChainId, ActorSystemContext systemContext
+ , LoggingAdapter logger, ActorRef parent, ActorRef self) {
+ super(systemContext, logger, tenantId, ruleChainId);
+ this.parent = parent;
+ this.self = self;
+ this.nodeActors = new HashMap<>();
+ this.nodeRoutes = new HashMap<>();
+ this.service = systemContext.getRuleChainService();
+ }
+
+ @Override
+ public void start(ActorContext context) throws Exception {
+ if (!started) {
+ RuleChain ruleChain = service.findRuleChainById(entityId);
+ List<RuleNode> ruleNodeList = service.getRuleChainNodes(entityId);
+ // Creating and starting the actors;
+ for (RuleNode ruleNode : ruleNodeList) {
+ ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode);
+ nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode));
+ }
+ initRoutes(ruleChain, ruleNodeList);
+ reprocess(ruleNodeList);
+ started = true;
+ } else {
+ onUpdate(context);
+ }
+ }
+
+ private void reprocess(List<RuleNode> ruleNodeList) {
+ for (RuleNode ruleNode : ruleNodeList) {
+ for (TbMsg tbMsg : queue.findUnprocessed(tenantId, ruleNode.getId().getId(), systemContext.getQueuePartitionId())) {
+ pushMsgToNode(nodeActors.get(ruleNode.getId()), tbMsg, "");
+ }
+ }
+ if (firstNode != null) {
+ for (TbMsg tbMsg : queue.findUnprocessed(tenantId, entityId.getId(), systemContext.getQueuePartitionId())) {
+ pushMsgToNode(firstNode, tbMsg, "");
+ }
+ }
+ }
+
+ @Override
+ public void onUpdate(ActorContext context) throws Exception {
+ RuleChain ruleChain = service.findRuleChainById(entityId);
+ List<RuleNode> ruleNodeList = service.getRuleChainNodes(entityId);
+
+ for (RuleNode ruleNode : ruleNodeList) {
+ RuleNodeCtx existing = nodeActors.get(ruleNode.getId());
+ if (existing == null) {
+ ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode);
+ nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode));
+ } else {
+ existing.setSelf(ruleNode);
+ existing.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, existing.getSelf().getId(), ComponentLifecycleEvent.UPDATED), self);
+ }
+ }
+
+ Set<RuleNodeId> existingNodes = ruleNodeList.stream().map(RuleNode::getId).collect(Collectors.toSet());
+ List<RuleNodeId> removedRules = nodeActors.keySet().stream().filter(node -> !existingNodes.contains(node)).collect(Collectors.toList());
+ removedRules.forEach(ruleNodeId -> {
+ RuleNodeCtx removed = nodeActors.remove(ruleNodeId);
+ removed.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, removed.getSelf().getId(), ComponentLifecycleEvent.DELETED), self);
+ });
+
+ initRoutes(ruleChain, ruleNodeList);
+ reprocess(ruleNodeList);
+ }
+
+ @Override
+ public void stop(ActorContext context) throws Exception {
+ nodeActors.values().stream().map(RuleNodeCtx::getSelfActor).forEach(context::stop);
+ nodeActors.clear();
+ nodeRoutes.clear();
+ context.stop(self);
+ started = false;
+ }
+
+ @Override
+ public void onClusterEventMsg(ClusterEventMsg msg) throws Exception {
+
+ }
+
+ private ActorRef createRuleNodeActor(ActorContext context, RuleNode ruleNode) {
+ String dispatcherName = tenantId.getId().equals(EntityId.NULL_UUID) ?
+ DefaultActorService.SYSTEM_RULE_DISPATCHER_NAME : DefaultActorService.TENANT_RULE_DISPATCHER_NAME;
+ return context.actorOf(
+ Props.create(new RuleNodeActor.ActorCreator(systemContext, tenantId, entityId, ruleNode.getId()))
+ .withDispatcher(dispatcherName), ruleNode.getId().toString());
+ }
+
+ private void initRoutes(RuleChain ruleChain, List<RuleNode> ruleNodeList) {
+ nodeRoutes.clear();
+ // Populating the routes map;
+ for (RuleNode ruleNode : ruleNodeList) {
+ List<EntityRelation> relations = service.getRuleNodeRelations(ruleNode.getId());
+ if (relations.size() == 0) {
+ nodeRoutes.put(ruleNode.getId(), Collections.emptyList());
+ } else {
+ for (EntityRelation relation : relations) {
+ if (relation.getTo().getEntityType() == EntityType.RULE_NODE) {
+ RuleNodeCtx ruleNodeCtx = nodeActors.get(new RuleNodeId(relation.getTo().getId()));
+ if (ruleNodeCtx == null) {
+ throw new IllegalArgumentException("Rule Node [" + relation.getFrom() + "] has invalid relation to Rule node [" + relation.getTo() + "]");
+ }
+ }
+ nodeRoutes.computeIfAbsent(ruleNode.getId(), k -> new ArrayList<>())
+ .add(new RuleNodeRelation(ruleNode.getId(), relation.getTo(), relation.getType()));
+ }
+ }
+ }
+
+ firstId = ruleChain.getFirstRuleNodeId();
+ firstNode = nodeActors.get(ruleChain.getFirstRuleNodeId());
+ state = ComponentLifecycleState.ACTIVE;
+ }
+
+ void onServiceToRuleEngineMsg(ServiceToRuleEngineMsg envelope) {
+ checkActive();
+ if (firstNode != null) {
+ putToQueue(enrichWithRuleChainId(envelope.getTbMsg()), msg -> pushMsgToNode(firstNode, msg, ""));
+ }
+ }
+
+ void onDeviceActorToRuleEngineMsg(DeviceActorToRuleEngineMsg envelope) {
+ checkActive();
+ if (firstNode != null) {
+ putToQueue(enrichWithRuleChainId(envelope.getTbMsg()), msg -> {
+ pushMsgToNode(firstNode, msg, "");
+ envelope.getCallbackRef().tell(new RuleEngineQueuePutAckMsg(msg.getId()), self);
+ });
+ }
+ }
+
+ void onRuleChainToRuleChainMsg(RuleChainToRuleChainMsg envelope) {
+ checkActive();
+ if (envelope.isEnqueue()) {
+ if (firstNode != null) {
+ putToQueue(enrichWithRuleChainId(envelope.getMsg()), msg -> pushMsgToNode(firstNode, msg, envelope.getFromRelationType()));
+ }
+ } else {
+ if (firstNode != null) {
+ pushMsgToNode(firstNode, envelope.getMsg(), envelope.getFromRelationType());
+ } else {
+ TbMsg msg = envelope.getMsg();
+ EntityId ackId = msg.getRuleNodeId() != null ? msg.getRuleNodeId() : msg.getRuleChainId();
+ queue.ack(tenantId, envelope.getMsg(), ackId.getId(), msg.getClusterPartition());
+ }
+ }
+ }
+
+ void onTellNext(RuleNodeToRuleChainTellNextMsg envelope) {
+ checkActive();
+ RuleNodeId originator = envelope.getOriginator();
+ List<RuleNodeRelation> relations = nodeRoutes.get(originator).stream()
+ .filter(r -> contains(envelope.getRelationTypes(), r.getType()))
+ .collect(Collectors.toList());
+
+ TbMsg msg = envelope.getMsg();
+ int relationsCount = relations.size();
+ EntityId ackId = msg.getRuleNodeId() != null ? msg.getRuleNodeId() : msg.getRuleChainId();
+ if (relationsCount == 0) {
+ queue.ack(tenantId, msg, ackId.getId(), msg.getClusterPartition());
+ } else if (relationsCount == 1) {
+ for (RuleNodeRelation relation : relations) {
+ pushToTarget(msg, relation.getOut(), relation.getType());
+ }
+ } else {
+ for (RuleNodeRelation relation : relations) {
+ EntityId target = relation.getOut();
+ switch (target.getEntityType()) {
+ case RULE_NODE:
+ enqueueAndForwardMsgCopyToNode(msg, target, relation.getType());
+ break;
+ case RULE_CHAIN:
+ enqueueAndForwardMsgCopyToChain(msg, target, relation.getType());
+ break;
+ }
+ }
+ //TODO: Ideally this should happen in async way when all targets confirm that the copied messages are successfully written to corresponding target queues.
+ queue.ack(tenantId, msg, ackId.getId(), msg.getClusterPartition());
+ }
+ }
+
+ private boolean contains(Set<String> relationTypes, String type) {
+ if (relationTypes == null) {
+ return true;
+ }
+ for (String relationType : relationTypes) {
+ if (relationType.equalsIgnoreCase(type)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void enqueueAndForwardMsgCopyToChain(TbMsg msg, EntityId target, String fromRelationType) {
+ RuleChainId targetRCId = new RuleChainId(target.getId());
+ TbMsg copyMsg = msg.copy(UUIDs.timeBased(), targetRCId, null, DEFAULT_CLUSTER_PARTITION);
+ parent.tell(new RuleChainToRuleChainMsg(new RuleChainId(target.getId()), entityId, copyMsg, fromRelationType, true), self);
+ }
+
+ private void enqueueAndForwardMsgCopyToNode(TbMsg msg, EntityId target, String fromRelationType) {
+ RuleNodeId targetId = new RuleNodeId(target.getId());
+ RuleNodeCtx targetNodeCtx = nodeActors.get(targetId);
+ TbMsg copy = msg.copy(UUIDs.timeBased(), entityId, targetId, DEFAULT_CLUSTER_PARTITION);
+ putToQueue(copy, queuedMsg -> pushMsgToNode(targetNodeCtx, queuedMsg, fromRelationType));
+ }
+
+ private void pushToTarget(TbMsg msg, EntityId target, String fromRelationType) {
+ switch (target.getEntityType()) {
+ case RULE_NODE:
+ pushMsgToNode(nodeActors.get(new RuleNodeId(target.getId())), msg, fromRelationType);
+ break;
+ case RULE_CHAIN:
+ parent.tell(new RuleChainToRuleChainMsg(new RuleChainId(target.getId()), entityId, msg, fromRelationType, false), self);
+ break;
+ }
+ }
+
+ private void pushMsgToNode(RuleNodeCtx nodeCtx, TbMsg msg, String fromRelationType) {
+ if (nodeCtx != null) {
+ nodeCtx.getSelfActor().tell(new RuleChainToRuleNodeMsg(new DefaultTbContext(systemContext, nodeCtx), msg, fromRelationType), self);
+ }
+ }
+
+ private TbMsg enrichWithRuleChainId(TbMsg tbMsg) {
+ // We don't put firstNodeId because it may change over time;
+ return new TbMsg(tbMsg.getId(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.getMetaData().copy(), tbMsg.getData(), entityId, null, systemContext.getQueuePartitionId());
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java
new file mode 100644
index 0000000..47ee796
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import akka.actor.ActorRef;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.shared.rulechain.RuleChainManager;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+/**
+ * Created by ashvayka on 15.03.18.
+ */
+public abstract class RuleChainManagerActor extends ContextAwareActor {
+
+ protected final RuleChainManager ruleChainManager;
+ protected final RuleChainService ruleChainService;
+
+ public RuleChainManagerActor(ActorSystemContext systemContext, RuleChainManager ruleChainManager) {
+ super(systemContext);
+ this.ruleChainManager = ruleChainManager;
+ this.ruleChainService = systemContext.getRuleChainService();
+ }
+
+ protected void initRuleChains() {
+ ruleChainManager.init(this.context());
+ }
+
+ protected ActorRef getEntityActorRef(EntityId entityId) {
+ ActorRef target = null;
+ switch (entityId.getEntityType()) {
+ case RULE_CHAIN:
+ target = ruleChainManager.getOrCreateActor(this.context(), (RuleChainId) entityId);
+ break;
+ }
+ return target;
+ }
+
+ protected void broadcast(Object msg) {
+ ruleChainManager.broadcast(msg);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleChainMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleChainMsg.java
new file mode 100644
index 0000000..8b13747
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleChainMsg.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+public final class RuleChainToRuleChainMsg implements TbActorMsg {
+
+ private final RuleChainId target;
+ private final RuleChainId source;
+ private final TbMsg msg;
+ private final String fromRelationType;
+ private final boolean enqueue;
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.RULE_CHAIN_TO_RULE_CHAIN_MSG;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java
new file mode 100644
index 0000000..abcddc9
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+final class RuleChainToRuleNodeMsg implements TbActorMsg {
+
+ private final TbContext ctx;
+ private final TbMsg msg;
+ private final String fromRelationType;
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.RULE_CHAIN_TO_RULE_MSG;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java
new file mode 100644
index 0000000..f7ca0d8
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java
@@ -0,0 +1,109 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ComponentActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+
+public class RuleNodeActor extends ComponentActor<RuleNodeId, RuleNodeActorMessageProcessor> {
+
+ private final RuleChainId ruleChainId;
+
+ private RuleNodeActor(ActorSystemContext systemContext, TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
+ super(systemContext, tenantId, ruleNodeId);
+ this.ruleChainId = ruleChainId;
+ setProcessor(new RuleNodeActorMessageProcessor(tenantId, ruleChainId, ruleNodeId, systemContext,
+ logger, context().parent(), context().self()));
+ }
+
+ @Override
+ protected boolean process(TbActorMsg msg) {
+ switch (msg.getMsgType()) {
+ case COMPONENT_LIFE_CYCLE_MSG:
+ onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+ break;
+ case RULE_CHAIN_TO_RULE_MSG:
+ onRuleChainToRuleNodeMsg((RuleChainToRuleNodeMsg) msg);
+ break;
+ case RULE_TO_SELF_ERROR_MSG:
+ onRuleNodeToSelfErrorMsg((RuleNodeToSelfErrorMsg) msg);
+ break;
+ case RULE_TO_SELF_MSG:
+ onRuleNodeToSelfMsg((RuleNodeToSelfMsg) msg);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ private void onRuleNodeToSelfMsg(RuleNodeToSelfMsg msg) {
+ logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg());
+ try {
+ processor.onRuleToSelfMsg(msg);
+ increaseMessagesProcessedCount();
+ } catch (Exception e) {
+ logAndPersist("onRuleMsg", e);
+ }
+ }
+
+ private void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) {
+ logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg());
+ try {
+ processor.onRuleChainToRuleNodeMsg(msg);
+ increaseMessagesProcessedCount();
+ } catch (Exception e) {
+ logAndPersist("onRuleMsg", e);
+ }
+ }
+
+ private void onRuleNodeToSelfErrorMsg(RuleNodeToSelfErrorMsg msg) {
+ logAndPersist("onRuleMsg", ActorSystemContext.toException(msg.getError()));
+ }
+
+ public static class ActorCreator extends ContextBasedCreator<RuleNodeActor> {
+ private static final long serialVersionUID = 1L;
+
+ private final TenantId tenantId;
+ private final RuleChainId ruleChainId;
+ private final RuleNodeId ruleNodeId;
+
+ public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
+ super(context);
+ this.tenantId = tenantId;
+ this.ruleChainId = ruleChainId;
+ this.ruleNodeId = ruleNodeId;
+
+ }
+
+ @Override
+ public RuleNodeActor create() throws Exception {
+ return new RuleNodeActor(context, tenantId, ruleChainId, ruleNodeId);
+ }
+ }
+
+ @Override
+ protected long getErrorPersistFrequency() {
+ return systemContext.getRuleNodeErrorPersistFrequency();
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
new file mode 100644
index 0000000..acb171d
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
@@ -0,0 +1,121 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.event.LoggingAdapter;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNode;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> {
+
+ private final ActorRef parent;
+ private final ActorRef self;
+ private final RuleChainService service;
+ private RuleNode ruleNode;
+ private TbNode tbNode;
+ private TbContext defaultCtx;
+
+ RuleNodeActorMessageProcessor(TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId, ActorSystemContext systemContext
+ , LoggingAdapter logger, ActorRef parent, ActorRef self) {
+ super(systemContext, logger, tenantId, ruleNodeId);
+ this.parent = parent;
+ this.self = self;
+ this.service = systemContext.getRuleChainService();
+ this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(entityId);
+ this.defaultCtx = new DefaultTbContext(systemContext, new RuleNodeCtx(tenantId, parent, self, ruleNode));
+ }
+
+ @Override
+ public void start(ActorContext context) throws Exception {
+ tbNode = initComponent(ruleNode);
+ state = ComponentLifecycleState.ACTIVE;
+ }
+
+ @Override
+ public void onUpdate(ActorContext context) throws Exception {
+ RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(entityId);
+ boolean restartRequired = !(ruleNode.getType().equals(newRuleNode.getType())
+ && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration()));
+ this.ruleNode = newRuleNode;
+ this.defaultCtx.updateSelf(newRuleNode);
+ if (restartRequired) {
+ if (tbNode != null) {
+ tbNode.destroy();
+ }
+ start(context);
+ }
+ }
+
+ @Override
+ public void stop(ActorContext context) throws Exception {
+ if (tbNode != null) {
+ tbNode.destroy();
+ }
+ context.stop(self);
+ }
+
+ @Override
+ public void onClusterEventMsg(ClusterEventMsg msg) throws Exception {
+
+ }
+
+ public void onRuleToSelfMsg(RuleNodeToSelfMsg msg) throws Exception {
+ checkActive();
+ if (ruleNode.isDebugMode()) {
+ systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), "Self");
+ }
+ try {
+ tbNode.onMsg(defaultCtx, msg.getMsg());
+ } catch (Exception e) {
+ defaultCtx.tellFailure(msg.getMsg(), e);
+ }
+ }
+
+ void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception {
+ checkActive();
+ if (ruleNode.isDebugMode()) {
+ systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), msg.getFromRelationType());
+ }
+ try {
+ tbNode.onMsg(msg.getCtx(), msg.getMsg());
+ } catch (Exception e) {
+ msg.getCtx().tellFailure(msg.getMsg(), e);
+ }
+ }
+
+ private TbNode initComponent(RuleNode ruleNode) throws Exception {
+ Class<?> componentClazz = Class.forName(ruleNode.getType());
+ TbNode tbNode = (TbNode) (componentClazz.newInstance());
+ tbNode.init(defaultCtx, new TbNodeConfiguration(ruleNode.getConfiguration()));
+ return tbNode;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java
new file mode 100644
index 0000000..c0a475c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.Set;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+final class RuleNodeToRuleChainTellNextMsg implements TbActorMsg {
+
+ private final RuleNodeId originator;
+ private final Set<String> relationTypes;
+ private final TbMsg msg;
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.RULE_TO_RULE_CHAIN_TELL_NEXT_MSG;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfErrorMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfErrorMsg.java
new file mode 100644
index 0000000..e6248f1
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfErrorMsg.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+final class RuleNodeToSelfErrorMsg implements TbActorMsg {
+
+ private final TbMsg msg;
+ private final Throwable error;
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.RULE_TO_SELF_ERROR_MSG;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
index baae376..f7e80e4 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
@@ -16,21 +16,24 @@
package org.thingsboard.server.actors.service;
import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.service.cluster.discovery.DiscoveryServiceListener;
import org.thingsboard.server.service.cluster.rpc.RpcMsgListener;
-public interface ActorService extends SessionMsgProcessor, WebSocketMsgProcessor, RestMsgProcessor, RpcMsgListener, DiscoveryServiceListener {
+public interface ActorService extends SessionMsgProcessor, RpcMsgListener, DiscoveryServiceListener {
- void onPluginStateChange(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent state);
+ void onEntityStateChange(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state);
- void onRuleStateChange(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent state);
+ void onMsg(SendToClusterMsg msg);
void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId);
void onDeviceNameOrTypeUpdate(TenantId tenantId, DeviceId deviceId, String deviceName, String deviceType);
+
+ void onMsg(ServiceToRuleEngineMsg serviceToRuleEngineMsg);
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
index 76b9be9..6aa68d3 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
@@ -54,7 +54,7 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
@Override
public void preStart() {
try {
- processor.start();
+ processor.start(context());
logLifecycleEvent(ComponentLifecycleEvent.STARTED);
if (systemContext.isStatisticsEnabled()) {
scheduleStatsPersistTick();
@@ -78,7 +78,7 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
@Override
public void postStop() {
try {
- processor.stop();
+ processor.stop(context());
logLifecycleEvent(ComponentLifecycleEvent.STOPPED);
} catch (Exception e) {
logger.warning("[{}][{}] Failed to stop {} processor: {}", tenantId, id, id.getEntityType(), e.getMessage());
@@ -141,7 +141,6 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
messagesProcessed++;
}
-
protected void logAndPersist(String method, Exception e) {
logAndPersist(method, e, false);
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java b/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java
index 825c971..3624127 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java
@@ -16,9 +16,13 @@
package org.thingsboard.server.actors.service;
import akka.actor.UntypedActor;
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.msg.TbActorMsg;
public abstract class ContextAwareActor extends UntypedActor {
+ protected final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
public static final int ENTITY_PACK_LIMIT = 1024;
@@ -28,4 +32,24 @@ public abstract class ContextAwareActor extends UntypedActor {
super();
this.systemContext = systemContext;
}
+
+ @Override
+ public void onReceive(Object msg) throws Exception {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Processing msg: {}", msg);
+ }
+ if (msg instanceof TbActorMsg) {
+ try {
+ if (!process((TbActorMsg) msg)) {
+ logger.warning("Unknown message: {}!", msg);
+ }
+ } catch (Exception e) {
+ throw e;
+ }
+ } else {
+ logger.warning("Unknown message: {}!", msg);
+ }
+ }
+
+ protected abstract boolean process(TbActorMsg msg);
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ContextBasedCreator.java b/application/src/main/java/org/thingsboard/server/actors/service/ContextBasedCreator.java
index e6d7ff8..b5f1f61 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ContextBasedCreator.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ContextBasedCreator.java
@@ -15,9 +15,8 @@
*/
package org.thingsboard.server.actors.service;
-import org.thingsboard.server.actors.ActorSystemContext;
-
import akka.japi.Creator;
+import org.thingsboard.server.actors.ActorSystemContext;
public abstract class ContextBasedCreator<T> implements Creator<T> {
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
index bb84a30..3507f24 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
@@ -19,35 +19,32 @@ import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import akka.actor.Props;
import akka.actor.Terminated;
+import com.google.protobuf.ByteString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
+import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg;
+import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.app.AppActor;
import org.thingsboard.server.actors.rpc.RpcBroadcastMsg;
import org.thingsboard.server.actors.rpc.RpcManagerActor;
import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg;
-import org.thingsboard.server.actors.rpc.RpcSessionTellMsg;
import org.thingsboard.server.actors.session.SessionManagerActor;
import org.thingsboard.server.actors.stats.StatsActor;
import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
-import org.thingsboard.server.extensions.api.device.DeviceNameOrTypeUpdateMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
-import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
-import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
-import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
-import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
import org.thingsboard.server.service.cluster.discovery.ServerInstance;
import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
@@ -57,7 +54,8 @@ import scala.concurrent.duration.Duration;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
-import java.util.Optional;
+
+import static org.thingsboard.server.gen.cluster.ClusterAPIProtos.MessageType.CLUSTER_ACTOR_MESSAGE;
@Service
@Slf4j
@@ -93,7 +91,7 @@ public class DefaultActorService implements ActorService {
@PostConstruct
public void initActorSystem() {
- log.info("Initializing Actor system. {}", actorContext.getRuleService());
+ log.info("Initializing Actor system. {}", actorContext.getRuleChainService());
actorContext.setActorService(this);
system = ActorSystem.create(ACTOR_SYSTEM_NAME, actorContext.getConfig());
actorContext.setActorSystem(system);
@@ -129,72 +127,17 @@ public class DefaultActorService implements ActorService {
}
@Override
- public void process(SessionAwareMsg msg) {
- log.debug("Processing session aware msg: {}", msg);
- sessionManagerActor.tell(msg, ActorRef.noSender());
- }
-
- @Override
- public void process(PluginWebsocketMsg<?> msg) {
- log.debug("Processing websocket msg: {}", msg);
- appActor.tell(msg, ActorRef.noSender());
- }
-
- @Override
- public void process(PluginRestMsg msg) {
- log.debug("Processing rest msg: {}", msg);
- appActor.tell(msg, ActorRef.noSender());
- }
-
- @Override
- public void onMsg(ToPluginActorMsg msg) {
- log.trace("Processing plugin rpc msg: {}", msg);
- appActor.tell(msg, ActorRef.noSender());
- }
-
- @Override
- public void onMsg(ToDeviceActorMsg msg) {
- log.trace("Processing device rpc msg: {}", msg);
+ public void onMsg(SendToClusterMsg msg) {
appActor.tell(msg, ActorRef.noSender());
}
@Override
- public void onMsg(ToDeviceActorNotificationMsg msg) {
- log.trace("Processing notification rpc msg: {}", msg);
- appActor.tell(msg, ActorRef.noSender());
- }
-
- @Override
- public void onMsg(ToDeviceSessionActorMsg msg) {
- log.trace("Processing session rpc msg: {}", msg);
+ public void process(SessionAwareMsg msg) {
+ log.debug("Processing session aware msg: {}", msg);
sessionManagerActor.tell(msg, ActorRef.noSender());
}
@Override
- public void onMsg(ToAllNodesMsg msg) {
- log.trace("Processing broadcast rpc msg: {}", msg);
- appActor.tell(msg, ActorRef.noSender());
- }
-
- @Override
- public void onMsg(RpcSessionCreateRequestMsg msg) {
- log.trace("Processing session create msg: {}", msg);
- rpcManagerActor.tell(msg, ActorRef.noSender());
- }
-
- @Override
- public void onMsg(RpcSessionTellMsg msg) {
- log.trace("Processing session rpc msg: {}", msg);
- rpcManagerActor.tell(msg, ActorRef.noSender());
- }
-
- @Override
- public void onMsg(RpcBroadcastMsg msg) {
- log.trace("Processing broadcast rpc msg: {}", msg);
- rpcManagerActor.tell(msg, ActorRef.noSender());
- }
-
- @Override
public void onServerAdded(ServerInstance server) {
log.trace("Processing onServerAdded msg: {}", server);
broadcast(new ClusterEventMsg(server.getServerAddress(), true));
@@ -212,42 +155,37 @@ public class DefaultActorService implements ActorService {
}
@Override
- public void onPluginStateChange(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent state) {
- log.trace("[{}] Processing onPluginStateChange event: {}", pluginId, state);
- broadcast(ComponentLifecycleMsg.forPlugin(tenantId, pluginId, state));
- }
-
- @Override
- public void onRuleStateChange(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent state) {
- log.trace("[{}] Processing onRuleStateChange event: {}", ruleId, state);
- broadcast(ComponentLifecycleMsg.forRule(tenantId, ruleId, state));
+ public void onEntityStateChange(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state) {
+ log.trace("[{}] Processing {} state change event: {}", tenantId, entityId.getEntityType(), state);
+ broadcast(new ComponentLifecycleMsg(tenantId, entityId, state));
}
@Override
public void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId) {
DeviceCredentialsUpdateNotificationMsg msg = new DeviceCredentialsUpdateNotificationMsg(tenantId, deviceId);
- Optional<ServerAddress> address = actorContext.getRoutingService().resolveById(deviceId);
- if (address.isPresent()) {
- rpcService.tell(address.get(), msg);
- } else {
- onMsg(msg);
- }
+ appActor.tell(new SendToClusterMsg(deviceId, msg), ActorRef.noSender());
}
@Override
public void onDeviceNameOrTypeUpdate(TenantId tenantId, DeviceId deviceId, String deviceName, String deviceType) {
log.trace("[{}] Processing onDeviceNameOrTypeUpdate event, deviceName: {}, deviceType: {}", deviceId, deviceName, deviceType);
DeviceNameOrTypeUpdateMsg msg = new DeviceNameOrTypeUpdateMsg(tenantId, deviceId, deviceName, deviceType);
- Optional<ServerAddress> address = actorContext.getRoutingService().resolveById(deviceId);
- if (address.isPresent()) {
- rpcService.tell(address.get(), msg);
- } else {
- onMsg(msg);
- }
+ appActor.tell(new SendToClusterMsg(deviceId, msg), ActorRef.noSender());
+ }
+
+ @Override
+ public void onMsg(ServiceToRuleEngineMsg msg) {
+ appActor.tell(msg, ActorRef.noSender());
}
public void broadcast(ToAllNodesMsg msg) {
- rpcService.broadcast(msg);
+ actorContext.getEncodingService().encode(msg);
+ rpcService.broadcast(new RpcBroadcastMsg(ClusterAPIProtos.ClusterMessage
+ .newBuilder()
+ .setPayload(ByteString
+ .copyFrom(actorContext.getEncodingService().encode(msg)))
+ .setMessageType(CLUSTER_ACTOR_MESSAGE)
+ .build()));
appActor.tell(msg, ActorRef.noSender());
}
@@ -256,4 +194,64 @@ public class DefaultActorService implements ActorService {
this.sessionManagerActor.tell(msg, ActorRef.noSender());
this.rpcManagerActor.tell(msg, ActorRef.noSender());
}
+
+ @Override
+ public void onReceivedMsg(ServerAddress source, ClusterAPIProtos.ClusterMessage msg) {
+ ServerAddress serverAddress = new ServerAddress(source.getHost(), source.getPort());
+ log.info("Received msg [{}] from [{}]", msg.getMessageType().name(), serverAddress);
+ if(log.isDebugEnabled()){
+ log.info("MSG: ", msg);
+ }
+ switch (msg.getMessageType()) {
+ case CLUSTER_ACTOR_MESSAGE:
+ java.util.Optional<TbActorMsg> decodedMsg = actorContext.getEncodingService()
+ .decode(msg.getPayload().toByteArray());
+ if (decodedMsg.isPresent()) {
+ appActor.tell(decodedMsg.get(), ActorRef.noSender());
+ } else {
+ log.error("Error during decoding cluster proto message");
+ }
+ break;
+ case TO_ALL_NODES_MSG:
+ //TODO
+ break;
+ case CLUSTER_TELEMETRY_SUBSCRIPTION_CREATE_MESSAGE:
+ actorContext.getTsSubService().onNewRemoteSubscription(serverAddress, msg.getPayload().toByteArray());
+ break;
+ case CLUSTER_TELEMETRY_SUBSCRIPTION_UPDATE_MESSAGE:
+ actorContext.getTsSubService().onRemoteSubscriptionUpdate(serverAddress, msg.getPayload().toByteArray());
+ break;
+ case CLUSTER_TELEMETRY_SUBSCRIPTION_CLOSE_MESSAGE:
+ actorContext.getTsSubService().onRemoteSubscriptionClose(serverAddress, msg.getPayload().toByteArray());
+ break;
+ case CLUSTER_TELEMETRY_SESSION_CLOSE_MESSAGE:
+ actorContext.getTsSubService().onRemoteSessionClose(serverAddress, msg.getPayload().toByteArray());
+ break;
+ case CLUSTER_TELEMETRY_ATTR_UPDATE_MESSAGE:
+ actorContext.getTsSubService().onRemoteAttributesUpdate(serverAddress, msg.getPayload().toByteArray());
+ break;
+ case CLUSTER_TELEMETRY_TS_UPDATE_MESSAGE:
+ actorContext.getTsSubService().onRemoteTsUpdate(serverAddress, msg.getPayload().toByteArray());
+ break;
+ case CLUSTER_RPC_FROM_DEVICE_RESPONSE_MESSAGE:
+ actorContext.getDeviceRpcService().processRemoteResponseFromDevice(serverAddress, msg.getPayload().toByteArray());
+ break;
+ }
+ }
+
+ @Override
+ public void onSendMsg(ClusterAPIProtos.ClusterMessage msg) {
+ rpcManagerActor.tell(msg, ActorRef.noSender());
+ }
+
+ @Override
+ public void onRpcSessionCreateRequestMsg(RpcSessionCreateRequestMsg msg) {
+ rpcManagerActor.tell(msg, ActorRef.noSender());
+ }
+
+ @Override
+ public void onBroadcastMsg(RpcBroadcastMsg msg) {
+ rpcManagerActor.tell(msg, ActorRef.noSender());
+ }
+
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java
index 96526dd..469cda9 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java
@@ -15,36 +15,42 @@
*/
package org.thingsboard.server.actors.session;
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.event.LoggingAdapter;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.SessionId;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.device.BasicToDeviceActorMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-import org.thingsboard.server.common.msg.session.*;
+import org.thingsboard.server.common.msg.device.BasicDeviceToDeviceActorMsg;
+import org.thingsboard.server.common.msg.device.DeviceToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.SessionContext;
+import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
+import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.common.msg.session.TransportToDeviceSessionActorMsg;
import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
-import akka.actor.ActorContext;
-import akka.actor.ActorRef;
-import akka.event.LoggingAdapter;
-
import java.util.Optional;
abstract class AbstractSessionActorMsgProcessor extends AbstractContextAwareMsgProcessor {
protected final SessionId sessionId;
protected SessionContext sessionCtx;
- protected ToDeviceActorMsg toDeviceActorMsgPrototype;
+ protected DeviceToDeviceActorMsg deviceToDeviceActorMsgPrototype;
protected AbstractSessionActorMsgProcessor(ActorSystemContext ctx, LoggingAdapter logger, SessionId sessionId) {
super(ctx, logger);
this.sessionId = sessionId;
}
- protected abstract void processToDeviceActorMsg(ActorContext ctx, ToDeviceActorSessionMsg msg);
+ protected abstract void processToDeviceActorMsg(ActorContext ctx, TransportToDeviceSessionActorMsg msg);
protected abstract void processTimeoutMsg(ActorContext context, SessionTimeoutMsg msg);
@@ -62,47 +68,44 @@ abstract class AbstractSessionActorMsgProcessor extends AbstractContextAwareMsgP
protected void cleanupSession(ActorContext ctx) {
}
- protected void updateSessionCtx(ToDeviceActorSessionMsg msg, SessionType type) {
+ protected void updateSessionCtx(TransportToDeviceSessionActorMsg msg, SessionType type) {
sessionCtx = msg.getSessionMsg().getSessionContext();
- toDeviceActorMsgPrototype = new BasicToDeviceActorMsg(msg, type);
+ deviceToDeviceActorMsgPrototype = new BasicDeviceToDeviceActorMsg(msg, type);
}
- protected ToDeviceActorMsg toDeviceMsg(ToDeviceActorSessionMsg msg) {
+ protected DeviceToDeviceActorMsg toDeviceMsg(TransportToDeviceSessionActorMsg msg) {
AdaptorToSessionActorMsg adaptorMsg = msg.getSessionMsg();
- return new BasicToDeviceActorMsg(toDeviceActorMsgPrototype, adaptorMsg.getMsg());
+ return new BasicDeviceToDeviceActorMsg(deviceToDeviceActorMsgPrototype, adaptorMsg.getMsg());
}
- protected Optional<ToDeviceActorMsg> toDeviceMsg(FromDeviceMsg msg) {
- if (toDeviceActorMsgPrototype != null) {
- return Optional.of(new BasicToDeviceActorMsg(toDeviceActorMsgPrototype, msg));
+ protected Optional<DeviceToDeviceActorMsg> toDeviceMsg(FromDeviceMsg msg) {
+ if (deviceToDeviceActorMsgPrototype != null) {
+ return Optional.of(new BasicDeviceToDeviceActorMsg(deviceToDeviceActorMsgPrototype, msg));
} else {
return Optional.empty();
}
}
- protected Optional<ServerAddress> forwardToAppActor(ActorContext ctx, ToDeviceActorMsg toForward) {
+ protected Optional<ServerAddress> forwardToAppActor(ActorContext ctx, DeviceToDeviceActorMsg toForward) {
Optional<ServerAddress> address = systemContext.getRoutingService().resolveById(toForward.getDeviceId());
forwardToAppActor(ctx, toForward, address);
return address;
}
- protected Optional<ServerAddress> forwardToAppActorIfAdressChanged(ActorContext ctx, ToDeviceActorMsg toForward, Optional<ServerAddress> oldAddress) {
+ protected Optional<ServerAddress> forwardToAppActorIfAddressChanged(ActorContext ctx, DeviceToDeviceActorMsg toForward, Optional<ServerAddress> oldAddress) {
+
Optional<ServerAddress> newAddress = systemContext.getRoutingService().resolveById(toForward.getDeviceId());
if (!newAddress.equals(oldAddress)) {
- if (newAddress.isPresent()) {
- systemContext.getRpcService().tell(newAddress.get(),
- toForward.toOtherAddress(systemContext.getRoutingService().getCurrentServer()));
- } else {
- getAppActor().tell(toForward, ctx.self());
- }
+ getAppActor().tell(new SendToClusterMsg(toForward.getDeviceId(), toForward
+ .toOtherAddress(systemContext.getRoutingService().getCurrentServer())), ctx.self());
}
return newAddress;
}
- protected void forwardToAppActor(ActorContext ctx, ToDeviceActorMsg toForward, Optional<ServerAddress> address) {
+ protected void forwardToAppActor(ActorContext ctx, DeviceToDeviceActorMsg toForward, Optional<ServerAddress> address) {
if (address.isPresent()) {
- systemContext.getRpcService().tell(address.get(),
- toForward.toOtherAddress(systemContext.getRoutingService().getCurrentServer()));
+ systemContext.getRpcService().tell(systemContext.getEncodingService().convertToProtoDataMessage(address.get(),
+ toForward.toOtherAddress(systemContext.getRoutingService().getCurrentServer())));
} else {
getAppActor().tell(toForward, ctx.self());
}
@@ -114,6 +117,6 @@ abstract class AbstractSessionActorMsgProcessor extends AbstractContextAwareMsgP
}
public DeviceId getDeviceId() {
- return toDeviceActorMsgPrototype.getDeviceId();
+ return deviceToDeviceActorMsgPrototype.getDeviceId();
}
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
index ab75cdb..a8f14fe 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
@@ -15,19 +15,26 @@
*/
package org.thingsboard.server.actors.session;
+import akka.actor.ActorContext;
+import akka.event.LoggingAdapter;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
import org.thingsboard.server.common.data.id.SessionId;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.core.*;
+import org.thingsboard.server.common.msg.core.AttributesSubscribeMsg;
+import org.thingsboard.server.common.msg.core.ResponseMsg;
+import org.thingsboard.server.common.msg.core.RpcSubscribeMsg;
import org.thingsboard.server.common.msg.core.SessionCloseMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-import org.thingsboard.server.common.msg.session.*;
-
-import akka.actor.ActorContext;
-import akka.event.LoggingAdapter;
-import org.thingsboard.server.common.msg.session.ctrl.*;
+import org.thingsboard.server.common.msg.core.SessionOpenMsg;
+import org.thingsboard.server.common.msg.device.DeviceToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.BasicSessionActorToAdaptorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.common.msg.session.TransportToDeviceSessionActorMsg;
import org.thingsboard.server.common.msg.session.ex.SessionException;
import java.util.HashMap;
@@ -37,7 +44,7 @@ import java.util.Optional;
class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
private boolean firstMsg = true;
- private Map<Integer, ToDeviceActorMsg> pendingMap = new HashMap<>();
+ private Map<Integer, DeviceToDeviceActorMsg> pendingMap = new HashMap<>();
private Optional<ServerAddress> currentTargetServer;
private boolean subscribedToAttributeUpdates;
private boolean subscribedToRpcCommands;
@@ -47,14 +54,16 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
}
@Override
- protected void processToDeviceActorMsg(ActorContext ctx, ToDeviceActorSessionMsg msg) {
+ protected void processToDeviceActorMsg(ActorContext ctx, TransportToDeviceSessionActorMsg msg) {
updateSessionCtx(msg, SessionType.ASYNC);
+ DeviceToDeviceActorMsg pendingMsg = toDeviceMsg(msg);
+ FromDeviceMsg fromDeviceMsg = pendingMsg.getPayload();
if (firstMsg) {
- toDeviceMsg(new SessionOpenMsg()).ifPresent(m -> forwardToAppActor(ctx, m));
+ if (fromDeviceMsg.getMsgType() != SessionMsgType.SESSION_OPEN) {
+ toDeviceMsg(new SessionOpenMsg()).ifPresent(m -> forwardToAppActor(ctx, m));
+ }
firstMsg = false;
}
- ToDeviceActorMsg pendingMsg = toDeviceMsg(msg);
- FromDeviceMsg fromDeviceMsg = pendingMsg.getPayload();
switch (fromDeviceMsg.getMsgType()) {
case POST_TELEMETRY_REQUEST:
case POST_ATTRIBUTES_REQUEST:
@@ -86,8 +95,8 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
@Override
public void processToDeviceMsg(ActorContext context, ToDeviceMsg msg) {
try {
- if (msg.getMsgType() != MsgType.SESSION_CLOSE) {
- switch (msg.getMsgType()) {
+ if (msg.getSessionMsgType() != SessionMsgType.SESSION_CLOSE) {
+ switch (msg.getSessionMsgType()) {
case STATUS_CODE_RESPONSE:
case GET_ATTRIBUTES_RESPONSE:
ResponseMsg responseMsg = (ResponseMsg) msg;
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java b/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java
index 37827d6..05926c1 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java
@@ -17,22 +17,21 @@ package org.thingsboard.server.actors.session;
import akka.actor.OneForOneStrategy;
import akka.actor.SupervisorStrategy;
-import akka.japi.Function;
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.service.ContextAwareActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
-import org.thingsboard.server.common.msg.session.ToDeviceActorSessionMsg;
+import org.thingsboard.server.common.msg.core.ActorSystemToDeviceSessionActorMsg;
import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
import org.thingsboard.server.common.msg.session.SessionMsg;
import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.TransportToDeviceSessionActorMsg;
import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
-
-import akka.event.Logging;
-import akka.event.LoggingAdapter;
import scala.concurrent.duration.Duration;
public class SessionActor extends ContextAwareActor {
@@ -61,33 +60,38 @@ public class SessionActor extends ContextAwareActor {
}
@Override
- public void onReceive(Object msg) throws Exception {
- logger.debug("[{}] Processing: {}.", sessionId, msg);
- if (msg instanceof ToDeviceActorSessionMsg) {
- processDeviceMsg((ToDeviceActorSessionMsg) msg);
- } else if (msg instanceof ToDeviceSessionActorMsg) {
- processToDeviceMsg((ToDeviceSessionActorMsg) msg);
- } else if (msg instanceof SessionTimeoutMsg) {
- processTimeoutMsg((SessionTimeoutMsg) msg);
- } else if (msg instanceof SessionCtrlMsg) {
- processSessionCtrlMsg((SessionCtrlMsg) msg);
- } else if (msg instanceof ClusterEventMsg) {
- processClusterEvent((ClusterEventMsg) msg);
- } else {
- logger.warning("[{}] Unknown msg: {}", sessionId, msg);
+ protected boolean process(TbActorMsg msg) {
+ switch (msg.getMsgType()) {
+ case TRANSPORT_TO_DEVICE_SESSION_ACTOR_MSG:
+ processTransportToSessionMsg((TransportToDeviceSessionActorMsg) msg);
+ break;
+ case ACTOR_SYSTEM_TO_DEVICE_SESSION_ACTOR_MSG:
+ processActorsToSessionMsg((ActorSystemToDeviceSessionActorMsg) msg);
+ break;
+ case SESSION_TIMEOUT_MSG:
+ processTimeoutMsg((SessionTimeoutMsg) msg);
+ break;
+ case SESSION_CTRL_MSG:
+ processSessionCloseMsg((SessionCtrlMsg) msg);
+ break;
+ case CLUSTER_EVENT_MSG:
+ processClusterEvent((ClusterEventMsg) msg);
+ break;
+ default: return false;
}
+ return true;
}
private void processClusterEvent(ClusterEventMsg msg) {
processor.processClusterEvent(context(), msg);
}
- private void processDeviceMsg(ToDeviceActorSessionMsg msg) {
+ private void processTransportToSessionMsg(TransportToDeviceSessionActorMsg msg) {
initProcessor(msg);
processor.processToDeviceActorMsg(context(), msg);
}
- private void processToDeviceMsg(ToDeviceSessionActorMsg msg) {
+ private void processActorsToSessionMsg(ActorSystemToDeviceSessionActorMsg msg) {
processor.processToDeviceMsg(context(), msg.getMsg());
}
@@ -99,7 +103,7 @@ public class SessionActor extends ContextAwareActor {
}
}
- private void processSessionCtrlMsg(SessionCtrlMsg msg) {
+ private void processSessionCloseMsg(SessionCtrlMsg msg) {
if (processor != null) {
processor.processSessionCtrlMsg(context(), msg);
} else if (msg instanceof SessionCloseMsg) {
@@ -109,7 +113,7 @@ public class SessionActor extends ContextAwareActor {
}
}
- private void initProcessor(ToDeviceActorSessionMsg msg) {
+ private void initProcessor(TransportToDeviceSessionActorMsg msg) {
if (processor == null) {
SessionMsg sessionMsg = (SessionMsg) msg.getSessionMsg();
if (sessionMsg.getSessionContext().getSessionType() == SessionType.SYNC) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
index 9d67dab..624a9f4 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
@@ -15,26 +15,29 @@
*/
package org.thingsboard.server.actors.session;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.UUID;
-
-import akka.actor.*;
+import akka.actor.ActorRef;
+import akka.actor.InvalidActorNameException;
+import akka.actor.LocalActorRef;
+import akka.actor.Props;
+import akka.actor.Terminated;
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.service.ContextAwareActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
-
-import akka.event.Logging;
-import akka.event.LoggingAdapter;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.core.ActorSystemToDeviceSessionActorMsg;
import org.thingsboard.server.common.msg.core.SessionCloseMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
+import java.util.HashMap;
+import java.util.Map;
+
public class SessionManagerActor extends ContextAwareActor {
private static final int INITIAL_SESSION_MAP_SIZE = 1024;
@@ -49,6 +52,12 @@ public class SessionManagerActor extends ContextAwareActor {
}
@Override
+ protected boolean process(TbActorMsg msg) {
+ //TODO Move everything here, to work with TbActorMsg
+ return false;
+ }
+
+ @Override
public void onReceive(Object msg) throws Exception {
if (msg instanceof SessionCtrlMsg) {
onSessionCtrlMsg((SessionCtrlMsg) msg);
@@ -97,7 +106,7 @@ public class SessionManagerActor extends ContextAwareActor {
}
private void forwardToSessionActor(SessionAwareMsg msg) {
- if (msg instanceof ToDeviceSessionActorMsg || msg instanceof SessionCloseMsg) {
+ if (msg instanceof ActorSystemToDeviceSessionActorMsg || msg instanceof SessionCloseMsg) {
String sessionIdStr = msg.getSessionId().toUidStr();
ActorRef sessionActor = sessionActors.get(sessionIdStr);
if (sessionActor != null) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
index d696503..cf8df13 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
@@ -15,24 +15,26 @@
*/
package org.thingsboard.server.actors.session;
+import akka.actor.ActorContext;
+import akka.event.LoggingAdapter;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
import org.thingsboard.server.common.data.id.SessionId;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-import org.thingsboard.server.common.msg.session.*;
-import org.thingsboard.server.common.msg.session.ToDeviceActorSessionMsg;
+import org.thingsboard.server.common.msg.device.DeviceToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.BasicSessionActorToAdaptorMsg;
+import org.thingsboard.server.common.msg.session.SessionContext;
+import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.common.msg.session.TransportToDeviceSessionActorMsg;
import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
import org.thingsboard.server.common.msg.session.ex.SessionException;
-import akka.actor.ActorContext;
-import akka.event.LoggingAdapter;
-
import java.util.Optional;
class SyncMsgProcessor extends AbstractSessionActorMsgProcessor {
- private ToDeviceActorMsg pendingMsg;
+ private DeviceToDeviceActorMsg pendingMsg;
private Optional<ServerAddress> currentTargetServer;
private boolean pendingResponse;
@@ -41,7 +43,7 @@ class SyncMsgProcessor extends AbstractSessionActorMsgProcessor {
}
@Override
- protected void processToDeviceActorMsg(ActorContext ctx, ToDeviceActorSessionMsg msg) {
+ protected void processToDeviceActorMsg(ActorContext ctx, TransportToDeviceSessionActorMsg msg) {
updateSessionCtx(msg, SessionType.SYNC);
pendingMsg = toDeviceMsg(msg);
pendingResponse = true;
@@ -73,7 +75,7 @@ class SyncMsgProcessor extends AbstractSessionActorMsgProcessor {
@Override
public void processClusterEvent(ActorContext context, ClusterEventMsg msg) {
if (pendingResponse) {
- Optional<ServerAddress> newTargetServer = forwardToAppActorIfAdressChanged(context, pendingMsg, currentTargetServer);
+ Optional<ServerAddress> newTargetServer = forwardToAppActorIfAddressChanged(context, pendingMsg, currentTargetServer);
if (logger.isDebugEnabled()) {
if (!newTargetServer.equals(currentTargetServer)) {
if (newTargetServer.isPresent()) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
index 73b221f..8864486 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
@@ -19,18 +19,13 @@ import akka.actor.ActorContext;
import akka.actor.ActorRef;
import akka.actor.Scheduler;
import akka.event.LoggingAdapter;
-import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.thingsboard.server.actors.ActorSystemContext;
-import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
-import org.thingsboard.server.common.data.plugin.ComponentType;
-import org.thingsboard.server.extensions.api.component.*;
import scala.concurrent.ExecutionContextExecutor;
import scala.concurrent.duration.Duration;
-import java.io.IOException;
import java.util.concurrent.TimeUnit;
public abstract class AbstractContextAwareMsgProcessor {
@@ -76,53 +71,6 @@ public abstract class AbstractContextAwareMsgProcessor {
getScheduler().scheduleOnce(Duration.create(delayInMs, TimeUnit.MILLISECONDS), target, msg, getSystemDispatcher(), null);
}
- protected <T extends ConfigurableComponent> T initComponent(JsonNode componentNode) throws Exception {
- ComponentConfiguration configuration = new ComponentConfiguration(
- componentNode.get("clazz").asText(),
- componentNode.get("name").asText(),
- mapper.writeValueAsString(componentNode.get("configuration"))
- );
- logger.info("Initializing [{}][{}] component", configuration.getName(), configuration.getClazz());
- ComponentDescriptor componentDescriptor = systemContext.getComponentService().getComponent(configuration.getClazz())
- .orElseThrow(() -> new InstantiationException("Component Not found!"));
- return initComponent(componentDescriptor, configuration);
- }
-
- protected <T extends ConfigurableComponent> T initComponent(ComponentDescriptor componentDefinition, ComponentConfiguration configuration)
- throws Exception {
- return initComponent(componentDefinition.getClazz(), componentDefinition.getType(), configuration.getConfiguration());
- }
-
- protected <T extends ConfigurableComponent> T initComponent(String clazz, ComponentType type, String configuration)
- throws Exception {
- Class<?> componentClazz = Class.forName(clazz);
- T component = (T) (componentClazz.newInstance());
- Class<?> configurationClazz;
- switch (type) {
- case FILTER:
- configurationClazz = ((Filter) componentClazz.getAnnotation(Filter.class)).configuration();
- break;
- case PROCESSOR:
- configurationClazz = ((Processor) componentClazz.getAnnotation(Processor.class)).configuration();
- break;
- case ACTION:
- configurationClazz = ((Action) componentClazz.getAnnotation(Action.class)).configuration();
- break;
- case PLUGIN:
- configurationClazz = ((Plugin) componentClazz.getAnnotation(Plugin.class)).configuration();
- break;
- default:
- throw new IllegalStateException("Component with type: " + type + " is not supported!");
- }
- component.init(decode(configuration, configurationClazz));
- return component;
- }
-
- public <C> C decode(String configuration, Class<C> configurationClazz) throws IOException, RuntimeException {
- logger.info("Initializing using configuration: {}", configuration);
- return mapper.readValue(configuration, configurationClazz);
- }
-
@Data
@AllArgsConstructor
private static class ComponentConfiguration {
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
index 18d32d9..1cf8339 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
@@ -17,39 +17,87 @@ package org.thingsboard.server.actors.shared;
import akka.actor.ActorContext;
import akka.event.LoggingAdapter;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.stats.StatsPersistTick;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.service.queue.MsgQueueService;
-public abstract class ComponentMsgProcessor<T> extends AbstractContextAwareMsgProcessor {
+import javax.annotation.Nullable;
+import java.util.function.Consumer;
+
+public abstract class ComponentMsgProcessor<T extends EntityId> extends AbstractContextAwareMsgProcessor {
protected final TenantId tenantId;
protected final T entityId;
+ protected final MsgQueueService queue;
+ protected ComponentLifecycleState state;
protected ComponentMsgProcessor(ActorSystemContext systemContext, LoggingAdapter logger, TenantId tenantId, T id) {
super(systemContext, logger);
this.tenantId = tenantId;
this.entityId = id;
+ this.queue = systemContext.getMsgQueueService();
}
- public abstract void start() throws Exception;
+ public abstract void start(ActorContext context) throws Exception;
- public abstract void stop() throws Exception;
+ public abstract void stop(ActorContext context) throws Exception;
- public abstract void onCreated(ActorContext context) throws Exception;
+ public abstract void onClusterEventMsg(ClusterEventMsg msg) throws Exception;
- public abstract void onUpdate(ActorContext context) throws Exception;
+ public void onCreated(ActorContext context) throws Exception {
+ start(context);
+ }
- public abstract void onActivate(ActorContext context) throws Exception;
+ public void onUpdate(ActorContext context) throws Exception {
+ restart(context);
+ }
- public abstract void onSuspend(ActorContext context) throws Exception;
+ public void onActivate(ActorContext context) throws Exception {
+ restart(context);
+ }
- public abstract void onStop(ActorContext context) throws Exception;
+ public void onSuspend(ActorContext context) throws Exception {
+ stop(context);
+ }
- public abstract void onClusterEventMsg(ClusterEventMsg msg) throws Exception;
+ public void onStop(ActorContext context) throws Exception {
+ stop(context);
+ }
+
+ private void restart(ActorContext context) throws Exception {
+ stop(context);
+ start(context);
+ }
public void scheduleStatsPersistTick(ActorContext context, long statsPersistFrequency) {
schedulePeriodicMsgWithDelay(context, new StatsPersistTick(), statsPersistFrequency, statsPersistFrequency);
}
+
+ protected void checkActive() {
+ if (state != ComponentLifecycleState.ACTIVE) {
+ throw new IllegalStateException("Rule chain is not active!");
+ }
+ }
+
+ protected void putToQueue(final TbMsg tbMsg, final Consumer<TbMsg> onSuccess) {
+ EntityId entityId = tbMsg.getRuleNodeId() != null ? tbMsg.getRuleNodeId() : tbMsg.getRuleChainId();
+ Futures.addCallback(queue.put(this.tenantId, tbMsg, entityId.getId(), tbMsg.getClusterPartition()), new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(@Nullable Void result) {
+ onSuccess.accept(tbMsg);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ logger.debug("Failed to push message [{}] to queue due to [{}]", tbMsg, t);
+ }
+ });
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java
new file mode 100644
index 0000000..11ed5a3
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.shared.rulechain;
+
+import akka.actor.ActorRef;
+import akka.japi.Creator;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.ruleChain.RuleChainActor;
+import org.thingsboard.server.actors.shared.EntityActorsManager;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+/**
+ * Created by ashvayka on 15.03.18.
+ */
+@Slf4j
+public abstract class RuleChainManager extends EntityActorsManager<RuleChainId, RuleChainActor, RuleChain> {
+
+ protected final RuleChainService service;
+ @Getter
+ protected RuleChain rootChain;
+ @Getter
+ protected ActorRef rootChainActor;
+
+ public RuleChainManager(ActorSystemContext systemContext) {
+ super(systemContext);
+ this.service = systemContext.getRuleChainService();
+ }
+
+ @Override
+ public Creator<RuleChainActor> creator(RuleChainId entityId) {
+ return new RuleChainActor.ActorCreator(systemContext, getTenantId(), entityId);
+ }
+
+ @Override
+ public void visit(RuleChain entity, ActorRef actorRef) {
+ if (entity.isRoot()) {
+ rootChain = entity;
+ rootChainActor = actorRef;
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/SessionTimeoutMsg.java b/application/src/main/java/org/thingsboard/server/actors/shared/SessionTimeoutMsg.java
index 7d6dbca..d015fe0 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/SessionTimeoutMsg.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/SessionTimeoutMsg.java
@@ -17,13 +17,20 @@ package org.thingsboard.server.actors.shared;
import lombok.Data;
import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
import java.io.Serializable;
@Data
-public class SessionTimeoutMsg implements Serializable {
+public class SessionTimeoutMsg implements Serializable, TbActorMsg {
private static final long serialVersionUID = 1L;
private final SessionId sessionId;
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.SESSION_TIMEOUT_MSG;
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java b/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java
index ccc31cc..8623370 100644
--- a/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java
@@ -24,6 +24,7 @@ import org.thingsboard.server.actors.service.ContextAwareActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
public class StatsActor extends ContextAwareActor {
@@ -36,6 +37,12 @@ public class StatsActor extends ContextAwareActor {
}
@Override
+ protected boolean process(TbActorMsg msg) {
+ //TODO Move everything here, to work with TbActorMsg\
+ return false;
+ }
+
+ @Override
public void onReceive(Object msg) throws Exception {
logger.debug("Received message: {}", msg);
if (msg instanceof StatsPersistMsg) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
index b923fe1..7a3127d 100644
--- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
@@ -15,61 +15,55 @@
*/
package org.thingsboard.server.actors.tenant;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-
+import akka.actor.ActorRef;
+import akka.actor.OneForOneStrategy;
+import akka.actor.Props;
+import akka.actor.SupervisorStrategy;
+import akka.japi.Function;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.device.DeviceActor;
-import org.thingsboard.server.actors.plugin.PluginTerminationMsg;
-import org.thingsboard.server.actors.rule.ComplexRuleActorChain;
-import org.thingsboard.server.actors.rule.RuleActorChain;
-import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.device.DeviceActorToRuleEngineMsg;
+import org.thingsboard.server.actors.ruleChain.RuleChainManagerActor;
+import org.thingsboard.server.actors.ruleChain.RuleChainToRuleChainMsg;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.actors.service.DefaultActorService;
-import org.thingsboard.server.actors.shared.plugin.PluginManager;
-import org.thingsboard.server.actors.shared.plugin.TenantPluginManager;
-import org.thingsboard.server.actors.shared.rule.RuleManager;
-import org.thingsboard.server.actors.shared.rule.TenantRuleManager;
+import org.thingsboard.server.actors.shared.rulechain.TenantRuleChainManager;
+import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-
-import akka.actor.ActorRef;
-import akka.actor.Props;
-import akka.event.Logging;
-import akka.event.LoggingAdapter;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.aware.DeviceAwareMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
-import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
-import org.thingsboard.server.extensions.api.rules.ToRuleActorMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import scala.concurrent.duration.Duration;
-public class TenantActor extends ContextAwareActor {
+import java.util.HashMap;
+import java.util.Map;
- private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
+public class TenantActor extends RuleChainManagerActor {
private final TenantId tenantId;
- private final RuleManager ruleManager;
- private final PluginManager pluginManager;
private final Map<DeviceId, ActorRef> deviceActors;
private TenantActor(ActorSystemContext systemContext, TenantId tenantId) {
- super(systemContext);
+ super(systemContext, new TenantRuleChainManager(systemContext, tenantId));
this.tenantId = tenantId;
- this.ruleManager = new TenantRuleManager(systemContext, tenantId);
- this.pluginManager = new TenantPluginManager(systemContext, tenantId);
this.deviceActors = new HashMap<>();
}
+
+ @Override
+ public SupervisorStrategy supervisorStrategy() {
+ return strategy;
+ }
+
@Override
public void preStart() {
logger.info("[{}] Starting tenant actor.", tenantId);
try {
- ruleManager.init(this.context());
- pluginManager.init(this.context());
+ initRuleChains();
logger.info("[{}] Tenant actor started.", tenantId);
} catch (Exception e) {
logger.error(e, "[{}] Unknown failure", tenantId);
@@ -77,89 +71,74 @@ public class TenantActor extends ContextAwareActor {
}
@Override
- public void onReceive(Object msg) throws Exception {
- logger.debug("[{}] Received message: {}", tenantId, msg);
- if (msg instanceof RuleChainDeviceMsg) {
- process((RuleChainDeviceMsg) msg);
- } else if (msg instanceof ToDeviceActorMsg) {
- onToDeviceActorMsg((ToDeviceActorMsg) msg);
- } else if (msg instanceof ToPluginActorMsg) {
- onToPluginMsg((ToPluginActorMsg) msg);
- } else if (msg instanceof ToRuleActorMsg) {
- onToRuleMsg((ToRuleActorMsg) msg);
- } else if (msg instanceof ToDeviceActorNotificationMsg) {
- onToDeviceActorMsg((ToDeviceActorNotificationMsg) msg);
- } else if (msg instanceof ClusterEventMsg) {
- broadcast(msg);
- } else if (msg instanceof ComponentLifecycleMsg) {
- onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
- } else if (msg instanceof PluginTerminationMsg) {
- onPluginTerminated((PluginTerminationMsg) msg);
- } else {
- logger.warning("[{}] Unknown message: {}!", tenantId, msg);
+ protected boolean process(TbActorMsg msg) {
+ switch (msg.getMsgType()) {
+ case CLUSTER_EVENT_MSG:
+ broadcast(msg);
+ break;
+ case COMPONENT_LIFE_CYCLE_MSG:
+ onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+ break;
+ case SERVICE_TO_RULE_ENGINE_MSG:
+ onServiceToRuleEngineMsg((ServiceToRuleEngineMsg) msg);
+ break;
+ case DEVICE_ACTOR_TO_RULE_ENGINE_MSG:
+ onDeviceActorToRuleEngineMsg((DeviceActorToRuleEngineMsg) msg);
+ break;
+ case DEVICE_SESSION_TO_DEVICE_ACTOR_MSG:
+ case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG:
+ case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG:
+ case DEVICE_NAME_OR_TYPE_UPDATE_TO_DEVICE_ACTOR_MSG:
+ case DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG:
+ case SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG:
+ onToDeviceActorMsg((DeviceAwareMsg) msg);
+ break;
+ case RULE_CHAIN_TO_RULE_CHAIN_MSG:
+ onRuleChainMsg((RuleChainToRuleChainMsg) msg);
+ break;
+ default:
+ return false;
}
+ return true;
}
- private void broadcast(Object msg) {
- pluginManager.broadcast(msg);
+ @Override
+ protected void broadcast(Object msg) {
+ super.broadcast(msg);
deviceActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
}
- private void onToDeviceActorMsg(ToDeviceActorMsg msg) {
- getOrCreateDeviceActor(msg.getDeviceId()).tell(msg, ActorRef.noSender());
+ private void onServiceToRuleEngineMsg(ServiceToRuleEngineMsg msg) {
+ ruleChainManager.getRootChainActor().tell(msg, self());
}
- private void onToDeviceActorMsg(ToDeviceActorNotificationMsg msg) {
- getOrCreateDeviceActor(msg.getDeviceId()).tell(msg, ActorRef.noSender());
+ private void onDeviceActorToRuleEngineMsg(DeviceActorToRuleEngineMsg msg) {
+ ruleChainManager.getRootChainActor().tell(msg, self());
}
- private void onToRuleMsg(ToRuleActorMsg msg) {
- ActorRef target = ruleManager.getOrCreateRuleActor(this.context(), msg.getRuleId());
- target.tell(msg, ActorRef.noSender());
+ private void onRuleChainMsg(RuleChainToRuleChainMsg msg) {
+ ruleChainManager.getOrCreateActor(context(), msg.getTarget()).tell(msg, self());
}
- private void onToPluginMsg(ToPluginActorMsg msg) {
- if (msg.getPluginTenantId().equals(tenantId)) {
- ActorRef pluginActor = pluginManager.getOrCreatePluginActor(this.context(), msg.getPluginId());
- pluginActor.tell(msg, ActorRef.noSender());
- } else {
- context().parent().tell(msg, ActorRef.noSender());
- }
+
+ private void onToDeviceActorMsg(DeviceAwareMsg msg) {
+ getOrCreateDeviceActor(msg.getDeviceId()).tell(msg, ActorRef.noSender());
}
private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
- Optional<PluginId> pluginId = msg.getPluginId();
- Optional<RuleId> ruleId = msg.getRuleId();
- if (pluginId.isPresent()) {
- ActorRef pluginActor = pluginManager.getOrCreatePluginActor(this.context(), pluginId.get());
- pluginActor.tell(msg, ActorRef.noSender());
- } else if (ruleId.isPresent()) {
- ActorRef target;
- Optional<ActorRef> ref = ruleManager.update(this.context(), ruleId.get(), msg.getEvent());
- if (ref.isPresent()) {
- target = ref.get();
- } else {
- logger.debug("Failed to find actor for rule: [{}]", ruleId);
- return;
+ ActorRef target = getEntityActorRef(msg.getEntityId());
+ if (target != null) {
+ if (msg.getEntityId().getEntityType() == EntityType.RULE_CHAIN) {
+ RuleChain ruleChain = systemContext.getRuleChainService().
+ findRuleChainById(new RuleChainId(msg.getEntityId().getId()));
+ ruleChainManager.visit(ruleChain, target);
}
target.tell(msg, ActorRef.noSender());
} else {
- logger.debug("[{}] Invalid component lifecycle msg.", tenantId);
+ logger.debug("Invalid component lifecycle msg: {}", msg);
}
}
- private void onPluginTerminated(PluginTerminationMsg msg) {
- pluginManager.remove(msg.getId());
- }
-
- private void process(RuleChainDeviceMsg msg) {
- ToDeviceActorMsg toDeviceActorMsg = msg.getToDeviceActorMsg();
- ActorRef deviceActor = getOrCreateDeviceActor(toDeviceActorMsg.getDeviceId());
- RuleActorChain tenantChain = ruleManager.getRuleChain(this.context());
- RuleActorChain chain = new ComplexRuleActorChain(msg.getRuleChain(), tenantChain);
- deviceActor.tell(new RuleChainDeviceMsg(toDeviceActorMsg, chain), context().self());
- }
-
private ActorRef getOrCreateDeviceActor(DeviceId deviceId) {
return deviceActors.computeIfAbsent(deviceId, k -> context().actorOf(Props.create(new DeviceActor.ActorCreator(systemContext, tenantId, deviceId))
.withDispatcher(DefaultActorService.CORE_DISPATCHER_NAME), deviceId.toString()));
@@ -181,4 +160,12 @@ public class TenantActor extends ContextAwareActor {
}
}
+ private final SupervisorStrategy strategy = new OneForOneStrategy(3, Duration.create("1 minute"), new Function<Throwable, SupervisorStrategy.Directive>() {
+ @Override
+ public SupervisorStrategy.Directive apply(Throwable t) {
+ logger.error(t, "Unknown failure");
+ return SupervisorStrategy.resume();
+ }
+ });
+
}
diff --git a/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java b/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java
index 34afd35..4c36a15 100644
--- a/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java
+++ b/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java
@@ -17,8 +17,6 @@ package org.thingsboard.server.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
-import org.thingsboard.server.common.data.EntityType;
-import org.thingsboard.server.common.data.audit.ActionType;
import java.util.HashMap;
import java.util.Map;
diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
index 87b187e..2254cf3 100644
--- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
@@ -24,7 +24,11 @@ import org.springframework.context.annotation.Configuration;
import org.thingsboard.server.common.data.security.Authority;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.schema.AlternateTypeRule;
-import springfox.documentation.service.*;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.service.ApiKey;
+import springfox.documentation.service.AuthorizationScope;
+import springfox.documentation.service.Contact;
+import springfox.documentation.service.SecurityReference;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
index 2952529..6afa6b2 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
@@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
@@ -37,15 +36,18 @@ import org.springframework.security.web.authentication.AuthenticationFailureHand
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
-import org.springframework.web.cors.CorsUtils;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
+import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
+import org.thingsboard.server.service.security.auth.jwt.JwtTokenAuthenticationProcessingFilter;
+import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider;
+import org.thingsboard.server.service.security.auth.jwt.RefreshTokenProcessingFilter;
+import org.thingsboard.server.service.security.auth.jwt.SkipPathRequestMatcher;
+import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider;
import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter;
-import org.thingsboard.server.service.security.auth.jwt.*;
-import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor;
import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter;
import java.util.ArrayList;
diff --git a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
index a75ecb1..59b7da2 100644
--- a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
@@ -15,12 +15,6 @@
*/
package org.thingsboard.server.config;
-import java.util.Map;
-
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.controller.plugin.PluginWebSocketHandler;
-import org.thingsboard.server.service.security.model.SecurityUser;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
@@ -35,6 +29,12 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.controller.plugin.TbWebSocketHandler;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+import java.util.Map;
@Configuration
@EnableWebSocket
@@ -54,7 +54,7 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
- registry.addHandler(pluginWsHandler(), WS_PLUGIN_MAPPING).setAllowedOrigins("*")
+ registry.addHandler(wsHandler(), WS_PLUGIN_MAPPING).setAllowedOrigins("*")
.addInterceptors(new HttpSessionHandshakeInterceptor(), new HandshakeInterceptor() {
@Override
@@ -82,8 +82,8 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
}
@Bean
- public WebSocketHandler pluginWsHandler() {
- return new PluginWebSocketHandler();
+ public WebSocketHandler wsHandler() {
+ return new TbWebSocketHandler();
}
protected SecurityUser getCurrentUser() throws ThingsboardException {
diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
index e9a6ba3..0cf1491 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
@@ -17,11 +17,16 @@ package org.thingsboard.server.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.dao.settings.AdminSettingsService;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.service.mail.MailService;
import org.thingsboard.server.service.update.UpdateService;
import org.thingsboard.server.service.update.model.UpdateMessage;
diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
index 1959f4e..28cc2fd 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
@@ -18,13 +18,27 @@ package org.thingsboard.server.controller;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
-import org.thingsboard.server.common.data.alarm.*;
-import org.thingsboard.server.common.data.id.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmId;
+import org.thingsboard.server.common.data.alarm.AlarmInfo;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
+import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
@RestController
@RequestMapping("/api")
@@ -93,7 +107,7 @@ public class AlarmController extends BaseController {
try {
AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
checkAlarmId(alarmId);
- alarmService.clearAlarm(alarmId, System.currentTimeMillis()).get();
+ alarmService.clearAlarm(alarmId, null, System.currentTimeMillis()).get();
} catch (Exception e) {
throw handleException(e);
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
index 9b43913..35865ab 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
@@ -18,23 +18,30 @@ package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.AssetSearchQuery;
import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
-import org.thingsboard.server.common.data.asset.AssetSearchQuery;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.ArrayList;
diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
index 75bcf2a..34bcf84 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
@@ -16,15 +16,20 @@
package org.thingsboard.server.controller;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
-import org.thingsboard.server.exception.ThingsboardException;
import java.util.UUID;
diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
index ef38d80..af24c9c 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
@@ -25,12 +25,18 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.security.UserCredentials;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.service.mail.MailService;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index 3264af4..f044228 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -15,12 +15,13 @@
*/
package org.thingsboard.server.controller;
-import com.fasterxml.jackson.databind.JsonNode;
+import com.datastax.driver.core.utils.UUIDs;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -30,18 +31,24 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmId;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.asset.Asset;
-import org.thingsboard.server.common.data.audit.ActionStatus;
import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentType;
-import org.thingsboard.server.common.data.plugin.PluginMetaData;
-import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgDataType;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.audit.AuditLogService;
@@ -52,23 +59,23 @@ import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.dao.plugin.PluginService;
import org.thingsboard.server.dao.relation.RelationService;
-import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
-import org.thingsboard.server.exception.ThingsboardException;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.state.DeviceStateService;
import javax.mail.MessagingException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import java.util.UUID;
import static org.thingsboard.server.dao.service.Validator.validateId;
@@ -79,10 +86,15 @@ public abstract class BaseController {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!";
+ private static final ObjectMapper json = new ObjectMapper();
+
@Autowired
private ThingsboardErrorResponseHandler errorResponseHandler;
@Autowired
+ protected TenantService tenantService;
+
+ @Autowired
protected CustomerService customerService;
@Autowired
@@ -113,10 +125,7 @@ public abstract class BaseController {
protected ComponentDiscoveryService componentDescriptorService;
@Autowired
- protected RuleService ruleService;
-
- @Autowired
- protected PluginService pluginService;
+ protected RuleChainService ruleChainService;
@Autowired
protected ActorService actorService;
@@ -127,6 +136,9 @@ public abstract class BaseController {
@Autowired
protected AuditLogService auditLogService;
+ @Autowired
+ protected DeviceStateService deviceStateService;
+
@ExceptionHandler(ThingsboardException.class)
public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
errorResponseHandler.handle(ex, response);
@@ -289,11 +301,8 @@ public abstract class BaseController {
case TENANT:
checkTenantId(new TenantId(entityId.getId()));
return;
- case PLUGIN:
- checkPlugin(new PluginId(entityId.getId()));
- return;
- case RULE:
- checkRule(new RuleId(entityId.getId()));
+ case RULE_CHAIN:
+ checkRuleChain(new RuleChainId(entityId.getId()));
return;
case ASSET:
checkAsset(assetService.findAssetById(new AssetId(entityId.getId())));
@@ -472,60 +481,34 @@ public abstract class BaseController {
}
}
- List<ComponentDescriptor> checkPluginActionsByPluginClazz(String pluginClazz) throws ThingsboardException {
+ List<ComponentDescriptor> checkComponentDescriptorsByTypes(Set<ComponentType> types) throws ThingsboardException {
try {
- checkComponentDescriptorByClazz(pluginClazz);
- log.debug("[{}] Lookup plugin actions", pluginClazz);
- return componentDescriptorService.getPluginActions(pluginClazz);
+ log.debug("[{}] Lookup component descriptors", types);
+ return componentDescriptorService.getComponents(types);
} catch (Exception e) {
throw handleException(e, false);
}
}
- protected PluginMetaData checkPlugin(PluginMetaData plugin) throws ThingsboardException {
- checkNotNull(plugin);
- SecurityUser authUser = getCurrentUser();
- TenantId tenantId = plugin.getTenantId();
- validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
- if (authUser.getAuthority() != Authority.SYS_ADMIN) {
- if (authUser.getTenantId() == null ||
- !tenantId.getId().equals(ModelConstants.NULL_UUID) && !authUser.getTenantId().equals(tenantId)) {
- throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION,
- ThingsboardErrorCode.PERMISSION_DENIED);
-
- } else if (tenantId.getId().equals(ModelConstants.NULL_UUID)) {
- plugin.setConfiguration(null);
- }
- }
- return plugin;
- }
-
- protected PluginMetaData checkPlugin(PluginId pluginId) throws ThingsboardException {
- checkNotNull(pluginId);
- return checkPlugin(pluginService.findPluginById(pluginId));
- }
-
- protected RuleMetaData checkRule(RuleId ruleId) throws ThingsboardException {
- checkNotNull(ruleId);
- return checkRule(ruleService.findRuleById(ruleId));
+ protected RuleChain checkRuleChain(RuleChainId ruleChainId) throws ThingsboardException {
+ checkNotNull(ruleChainId);
+ return checkRuleChain(ruleChainService.findRuleChainById(ruleChainId));
}
- protected RuleMetaData checkRule(RuleMetaData rule) throws ThingsboardException {
- checkNotNull(rule);
+ protected RuleChain checkRuleChain(RuleChain ruleChain) throws ThingsboardException {
+ checkNotNull(ruleChain);
SecurityUser authUser = getCurrentUser();
- TenantId tenantId = rule.getTenantId();
+ TenantId tenantId = ruleChain.getTenantId();
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
- if (authUser.getAuthority() != Authority.SYS_ADMIN) {
- if (authUser.getTenantId() == null ||
- !tenantId.getId().equals(ModelConstants.NULL_UUID) && !authUser.getTenantId().equals(tenantId)) {
- throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION,
- ThingsboardErrorCode.PERMISSION_DENIED);
-
- }
+ if (authUser.getAuthority() != Authority.TENANT_ADMIN ||
+ !authUser.getTenantId().equals(tenantId)) {
+ throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION,
+ ThingsboardErrorCode.PERMISSION_DENIED);
}
- return rule;
+ return ruleChain;
}
+
protected String constructBaseUrl(HttpServletRequest request) {
String scheme = request.getScheme();
if (request.getHeader("x-forwarded-proto") != null) {
@@ -553,12 +536,127 @@ public abstract class BaseController {
protected <E extends BaseData<I> & HasName,
I extends UUIDBased & EntityId> void logEntityAction(I entityId, E entity, CustomerId customerId,
ActionType actionType, Exception e, Object... additionalInfo) throws ThingsboardException {
- User user = getCurrentUser();
+ logEntityAction(getCurrentUser(), entityId, entity, customerId, actionType, e, additionalInfo);
+ }
+
+ protected <E extends BaseData<I> & HasName,
+ I extends UUIDBased & EntityId> void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
+ ActionType actionType, Exception e, Object... additionalInfo) throws ThingsboardException {
if (customerId == null || customerId.isNullUid()) {
customerId = user.getCustomerId();
}
+ if (e == null) {
+ pushEntityActionToRuleEngine(entityId, entity, user, customerId, actionType, additionalInfo);
+ }
auditLogService.logEntityAction(user.getTenantId(), customerId, user.getId(), user.getName(), entityId, entity, actionType, e, additionalInfo);
}
+ public static Exception toException(Throwable error) {
+ return error != null ? (Exception.class.isInstance(error) ? (Exception) error : new Exception(error)) : null;
+ }
+
+ private <E extends BaseData<I> & HasName,
+ I extends UUIDBased & EntityId> void pushEntityActionToRuleEngine(I entityId, E entity, User user, CustomerId customerId,
+ ActionType actionType, Object... additionalInfo) {
+ String msgType = null;
+ switch (actionType) {
+ case ADDED:
+ msgType = DataConstants.ENTITY_CREATED;
+ break;
+ case DELETED:
+ msgType = DataConstants.ENTITY_DELETED;
+ break;
+ case UPDATED:
+ msgType = DataConstants.ENTITY_UPDATED;
+ break;
+ case ASSIGNED_TO_CUSTOMER:
+ msgType = DataConstants.ENTITY_ASSIGNED;
+ break;
+ case UNASSIGNED_FROM_CUSTOMER:
+ msgType = DataConstants.ENTITY_UNASSIGNED;
+ break;
+ case ATTRIBUTES_UPDATED:
+ msgType = DataConstants.ATTRIBUTES_UPDATED;
+ break;
+ case ATTRIBUTES_DELETED:
+ msgType = DataConstants.ATTRIBUTES_DELETED;
+ break;
+ }
+ if (!StringUtils.isEmpty(msgType)) {
+ try {
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("userId", user.getId().toString());
+ metaData.putValue("userName", user.getName());
+ if (customerId != null && !customerId.isNullUid()) {
+ metaData.putValue("customerId", customerId.toString());
+ }
+ if (actionType == ActionType.ASSIGNED_TO_CUSTOMER) {
+ String strCustomerId = extractParameter(String.class, 1, additionalInfo);
+ String strCustomerName = extractParameter(String.class, 2, additionalInfo);
+ metaData.putValue("assignedCustomerId", strCustomerId);
+ metaData.putValue("assignedCustomerName", strCustomerName);
+ } else if (actionType == ActionType.UNASSIGNED_FROM_CUSTOMER) {
+ String strCustomerId = extractParameter(String.class, 1, additionalInfo);
+ String strCustomerName = extractParameter(String.class, 2, additionalInfo);
+ metaData.putValue("unassignedCustomerId", strCustomerId);
+ metaData.putValue("unassignedCustomerName", strCustomerName);
+ }
+ ObjectNode entityNode;
+ if (entity != null) {
+ entityNode = json.valueToTree(entity);
+ if (entityId.getEntityType() == EntityType.DASHBOARD) {
+ entityNode.put("configuration", "");
+ }
+ } else {
+ entityNode = json.createObjectNode();
+ if (actionType == ActionType.ATTRIBUTES_UPDATED) {
+ String scope = extractParameter(String.class, 0, additionalInfo);
+ List<AttributeKvEntry> attributes = extractParameter(List.class, 1, additionalInfo);
+ metaData.putValue("scope", scope);
+ if (attributes != null) {
+ for (AttributeKvEntry attr : attributes) {
+ if (attr.getDataType() == DataType.BOOLEAN) {
+ entityNode.put(attr.getKey(), attr.getBooleanValue().get());
+ } else if (attr.getDataType() == DataType.DOUBLE) {
+ entityNode.put(attr.getKey(), attr.getDoubleValue().get());
+ } else if (attr.getDataType() == DataType.LONG) {
+ entityNode.put(attr.getKey(), attr.getLongValue().get());
+ } else {
+ entityNode.put(attr.getKey(), attr.getValueAsString());
+ }
+ }
+ }
+ } else if (actionType == ActionType.ATTRIBUTES_DELETED) {
+ String scope = extractParameter(String.class, 0, additionalInfo);
+ List<String> keys = extractParameter(List.class, 1, additionalInfo);
+ metaData.putValue("scope", scope);
+ ArrayNode attrsArrayNode = entityNode.putArray("attributes");
+ if (keys != null) {
+ keys.forEach(attrsArrayNode::add);
+ }
+ }
+ }
+ TbMsg tbMsg = new TbMsg(UUIDs.timeBased(), msgType, entityId, metaData, TbMsgDataType.JSON
+ , json.writeValueAsString(entityNode)
+ , null, null, 0L);
+ actorService.onMsg(new ServiceToRuleEngineMsg(user.getTenantId(), tbMsg));
+ } catch (Exception e) {
+ log.warn("[{}] Failed to push entity action to rule engine: {}", entityId, actionType, e);
+ }
+ }
+ }
+
+ private <T> T extractParameter(Class<T> clazz, int index, Object... additionalInfo) {
+ T result = null;
+ if (additionalInfo != null && additionalInfo.length > index) {
+ Object paramObject = additionalInfo[index];
+ if (clazz.isInstance(paramObject)) {
+ result = clazz.cast(paramObject);
+ }
+ }
+ return result;
+ }
+
+
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
index e63a443..fe4829f 100644
--- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
@@ -16,12 +16,19 @@
package org.thingsboard.server.controller;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentType;
-import org.thingsboard.server.exception.ThingsboardException;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
@RestController
@RequestMapping("/api")
@@ -52,12 +59,16 @@ public class ComponentDescriptorController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
- @RequestMapping(value = "/components/actions/{pluginClazz:.+}", method = RequestMethod.GET)
+ @RequestMapping(value = "/components", params = {"componentTypes"}, method = RequestMethod.GET)
@ResponseBody
- public List<ComponentDescriptor> getPluginActionsByPluginClazz(@PathVariable("pluginClazz") String pluginClazz) throws ThingsboardException {
- checkParameter("pluginClazz", pluginClazz);
+ public List<ComponentDescriptor> getComponentDescriptorsByTypes(@RequestParam("componentTypes") String[] strComponentTypes) throws ThingsboardException {
+ checkArrayParameter("componentTypes", strComponentTypes);
try {
- return checkPluginActionsByPluginClazz(pluginClazz);
+ Set<ComponentType> componentTypes = new HashSet<>();
+ for (String strComponentType : strComponentTypes) {
+ componentTypes.add(ComponentType.valueOf(strComponentType));
+ }
+ return checkComponentDescriptorsByTypes(componentTypes);
} catch (Exception e) {
throw handleException(e);
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
index b164702..34843ce 100644
--- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
@@ -20,15 +20,22 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
-import org.thingsboard.server.exception.ThingsboardException;
@RestController
@RequestMapping("/api")
diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
index d2952a1..85227e7 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
@@ -17,9 +17,21 @@ package org.thingsboard.server.controller;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
-import org.thingsboard.server.common.data.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.DashboardInfo;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ShortCustomerInfo;
import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -27,9 +39,6 @@ import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
-import org.thingsboard.server.dao.exception.IncorrectParameterException;
-import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardException;
import java.util.HashSet;
import java.util.Set;
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
index bceea54..a6daeec 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -18,14 +18,22 @@ package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
-import org.thingsboard.server.common.data.audit.ActionStatus;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -35,8 +43,6 @@ import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.ArrayList;
@@ -90,6 +96,11 @@ public class DeviceController extends BaseController {
savedDevice.getCustomerId(),
device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
+ if (device.getId() == null) {
+ deviceStateService.onDeviceAdded(savedDevice);
+ } else {
+ deviceStateService.onDeviceUpdated(savedDevice);
+ }
return savedDevice;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE), device,
@@ -112,6 +123,7 @@ public class DeviceController extends BaseController {
device.getCustomerId(),
ActionType.DELETED, null, strDeviceId);
+ deviceStateService.onDeviceDeleted(device);
} catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE),
null,
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
index 03054df..844dbd3 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
@@ -17,15 +17,21 @@ package org.thingsboard.server.controller;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationInfo;
-import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import java.util.List;
diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java
index 331b15e..7c8b2ae 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EventController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java
@@ -17,15 +17,21 @@ package org.thingsboard.server.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Event;
-import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
@RestController
@RequestMapping("/api")
diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcController.java b/application/src/main/java/org/thingsboard/server/controller/RpcController.java
new file mode 100644
index 0000000..e39a3f6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/RpcController.java
@@ -0,0 +1,217 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.FutureCallback;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.rule.engine.api.RpcError;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.rpc.RpcRequest;
+import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody;
+import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
+import org.thingsboard.server.service.rpc.DeviceRpcService;
+import org.thingsboard.server.service.rpc.FromDeviceRpcResponse;
+import org.thingsboard.server.service.rpc.LocalRequestMetaData;
+import org.thingsboard.server.service.security.AccessValidator;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.telemetry.exception.ToErrorResponseEntity;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.io.IOException;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Created by ashvayka on 22.03.18.
+ */
+@RestController
+@RequestMapping(TbUrlConstants.RPC_URL_PREFIX)
+@Slf4j
+public class RpcController extends BaseController {
+
+ public static final int DEFAULT_TIMEOUT = 10000;
+ protected final ObjectMapper jsonMapper = new ObjectMapper();
+
+ @Autowired
+ private DeviceRpcService deviceRpcService;
+
+ @Autowired
+ private AccessValidator accessValidator;
+
+ private ExecutorService executor;
+
+ @PostConstruct
+ public void initExecutor() {
+ executor = Executors.newSingleThreadExecutor();
+ }
+
+ @PreDestroy
+ public void shutdownExecutor() {
+ if (executor != null) {
+ executor.shutdownNow();
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/oneway/{deviceId}", method = RequestMethod.POST)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> handleOneWayDeviceRPCRequest(@PathVariable("deviceId") String deviceIdStr, @RequestBody String requestBody) throws ThingsboardException {
+ return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody);
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/twoway/{deviceId}", method = RequestMethod.POST)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> handleTwoWayDeviceRPCRequest(@PathVariable("deviceId") String deviceIdStr, @RequestBody String requestBody) throws ThingsboardException {
+ return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody);
+ }
+
+
+ private DeferredResult<ResponseEntity> handleDeviceRPCRequest(boolean oneWay, DeviceId deviceId, String requestBody) throws ThingsboardException {
+ try {
+ JsonNode rpcRequestBody = jsonMapper.readTree(requestBody);
+ RpcRequest cmd = new RpcRequest(rpcRequestBody.get("method").asText(),
+ jsonMapper.writeValueAsString(rpcRequestBody.get("params")));
+
+ if (rpcRequestBody.has("timeout")) {
+ cmd.setTimeout(rpcRequestBody.get("timeout").asLong());
+ }
+ SecurityUser currentUser = getCurrentUser();
+ TenantId tenantId = currentUser.getTenantId();
+ final DeferredResult<ResponseEntity> response = new DeferredResult<>();
+ long timeout = System.currentTimeMillis() + (cmd.getTimeout() != null ? cmd.getTimeout() : DEFAULT_TIMEOUT);
+ ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(cmd.getMethodName(), cmd.getRequestData());
+ accessValidator.validate(currentUser, deviceId, new HttpValidationCallback(response, new FutureCallback<DeferredResult<ResponseEntity>>() {
+ @Override
+ public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) {
+ ToDeviceRpcRequest rpcRequest = new ToDeviceRpcRequest(UUID.randomUUID(),
+ tenantId,
+ deviceId,
+ oneWay,
+ timeout,
+ body
+ );
+ deviceRpcService.processRpcRequestToDevice(rpcRequest, fromDeviceRpcResponse -> reply(new LocalRequestMetaData(rpcRequest, currentUser, result), fromDeviceRpcResponse));
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ ResponseEntity entity;
+ if (e instanceof ToErrorResponseEntity) {
+ entity = ((ToErrorResponseEntity) e).toErrorResponseEntity();
+ } else {
+ entity = new ResponseEntity(HttpStatus.UNAUTHORIZED);
+ }
+ logRpcCall(currentUser, deviceId, body, oneWay, Optional.empty(), e);
+ response.setResult(entity);
+ }
+ }));
+ return response;
+ } catch (IOException ioe) {
+ throw new ThingsboardException("Invalid request body", ioe, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
+ }
+ }
+
+ public void reply(LocalRequestMetaData rpcRequest, FromDeviceRpcResponse response) {
+ Optional<RpcError> rpcError = response.getError();
+ DeferredResult<ResponseEntity> responseWriter = rpcRequest.getResponseWriter();
+ if (rpcError.isPresent()) {
+ logRpcCall(rpcRequest, rpcError, null);
+ RpcError error = rpcError.get();
+ switch (error) {
+ case TIMEOUT:
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.REQUEST_TIMEOUT));
+ break;
+ case NO_ACTIVE_CONNECTION:
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.CONFLICT));
+ break;
+ default:
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.REQUEST_TIMEOUT));
+ break;
+ }
+ } else {
+ Optional<String> responseData = response.getResponse();
+ if (responseData.isPresent() && !StringUtils.isEmpty(responseData.get())) {
+ String data = responseData.get();
+ try {
+ logRpcCall(rpcRequest, rpcError, null);
+ responseWriter.setResult(new ResponseEntity<>(jsonMapper.readTree(data), HttpStatus.OK));
+ } catch (IOException e) {
+ log.debug("Failed to decode device response: {}", data, e);
+ logRpcCall(rpcRequest, rpcError, e);
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE));
+ }
+ } else {
+ logRpcCall(rpcRequest, rpcError, null);
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.OK));
+ }
+ }
+ }
+
+ private void logRpcCall(LocalRequestMetaData rpcRequest, Optional<RpcError> rpcError, Throwable e) {
+ logRpcCall(rpcRequest.getUser(), rpcRequest.getRequest().getDeviceId(), rpcRequest.getRequest().getBody(), rpcRequest.getRequest().isOneway(), rpcError, null);
+ }
+
+
+ private void logRpcCall(SecurityUser user, EntityId entityId, ToDeviceRpcRequestBody body, boolean oneWay, Optional<RpcError> rpcError, Throwable e) {
+ String rpcErrorStr = "";
+ if (rpcError.isPresent()) {
+ rpcErrorStr = "RPC Error: " + rpcError.get().name();
+ }
+ String method = body.getMethod();
+ String params = body.getParams();
+
+ auditLogService.logEntityAction(
+ user.getTenantId(),
+ user.getCustomerId(),
+ user.getId(),
+ user.getName(),
+ (UUIDBased & EntityId) entityId,
+ null,
+ ActionType.RPC_CALL,
+ BaseController.toException(e),
+ rpcErrorStr,
+ oneWay,
+ method,
+ params);
+ }
+
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
new file mode 100644
index 0000000..86b8fda
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
@@ -0,0 +1,334 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.rule.engine.api.ScriptEngine;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.event.EventService;
+import org.thingsboard.server.service.script.JsSandboxService;
+import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@Slf4j
+@RestController
+@RequestMapping("/api")
+public class RuleChainController extends BaseController {
+
+ public static final String RULE_CHAIN_ID = "ruleChainId";
+ public static final String RULE_NODE_ID = "ruleNodeId";
+
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ @Autowired
+ private EventService eventService;
+
+ @Autowired
+ private JsSandboxService jsSandboxService;
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET)
+ @ResponseBody
+ public RuleChain getRuleChainById(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
+ checkParameter(RULE_CHAIN_ID, strRuleChainId);
+ try {
+ RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
+ return checkRuleChain(ruleChainId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/ruleChain/{ruleChainId}/metadata", method = RequestMethod.GET)
+ @ResponseBody
+ public RuleChainMetaData getRuleChainMetaData(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
+ checkParameter(RULE_CHAIN_ID, strRuleChainId);
+ try {
+ RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
+ checkRuleChain(ruleChainId);
+ return ruleChainService.loadRuleChainMetaData(ruleChainId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/ruleChain", method = RequestMethod.POST)
+ @ResponseBody
+ public RuleChain saveRuleChain(@RequestBody RuleChain ruleChain) throws ThingsboardException {
+ try {
+ boolean created = ruleChain.getId() == null;
+ ruleChain.setTenantId(getCurrentUser().getTenantId());
+ RuleChain savedRuleChain = checkNotNull(ruleChainService.saveRuleChain(ruleChain));
+
+ actorService.onEntityStateChange(ruleChain.getTenantId(), savedRuleChain.getId(),
+ created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
+
+ logEntityAction(savedRuleChain.getId(), savedRuleChain,
+ null,
+ created ? ActionType.ADDED : ActionType.UPDATED, null);
+
+ return savedRuleChain;
+ } catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.RULE_CHAIN), ruleChain,
+ null, ruleChain.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
+
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/ruleChain/{ruleChainId}/root", method = RequestMethod.POST)
+ @ResponseBody
+ public RuleChain setRootRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
+ checkParameter(RULE_CHAIN_ID, strRuleChainId);
+ try {
+ RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
+ RuleChain ruleChain = checkRuleChain(ruleChainId);
+ TenantId tenantId = getCurrentUser().getTenantId();
+ RuleChain previousRootRuleChain = ruleChainService.getRootTenantRuleChain(tenantId);
+ if (ruleChainService.setRootRuleChain(ruleChainId)) {
+
+ previousRootRuleChain = ruleChainService.findRuleChainById(previousRootRuleChain.getId());
+
+ actorService.onEntityStateChange(previousRootRuleChain.getTenantId(), previousRootRuleChain.getId(),
+ ComponentLifecycleEvent.UPDATED);
+
+ logEntityAction(previousRootRuleChain.getId(), previousRootRuleChain,
+ null, ActionType.UPDATED, null);
+
+ ruleChain = ruleChainService.findRuleChainById(ruleChainId);
+
+ actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(),
+ ComponentLifecycleEvent.UPDATED);
+
+ logEntityAction(ruleChain.getId(), ruleChain,
+ null, ActionType.UPDATED, null);
+
+ }
+ return ruleChain;
+ } catch (Exception e) {
+ logEntityAction(emptyId(EntityType.RULE_CHAIN),
+ null,
+ null,
+ ActionType.UPDATED, e, strRuleChainId);
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/ruleChain/metadata", method = RequestMethod.POST)
+ @ResponseBody
+ public RuleChainMetaData saveRuleChainMetaData(@RequestBody RuleChainMetaData ruleChainMetaData) throws ThingsboardException {
+ try {
+ RuleChain ruleChain = checkRuleChain(ruleChainMetaData.getRuleChainId());
+ RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.saveRuleChainMetaData(ruleChainMetaData));
+
+ actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.UPDATED);
+
+ logEntityAction(ruleChain.getId(), ruleChain,
+ null,
+ ActionType.UPDATED, null, ruleChainMetaData);
+
+ return savedRuleChainMetaData;
+ } catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.RULE_CHAIN), null,
+ null, ActionType.UPDATED, e, ruleChainMetaData);
+
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/ruleChains", params = {"limit"}, method = RequestMethod.GET)
+ @ResponseBody
+ public TextPageData<RuleChain> getRuleChains(
+ @RequestParam int limit,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String idOffset,
+ @RequestParam(required = false) String textOffset) throws ThingsboardException {
+ try {
+ TenantId tenantId = getCurrentUser().getTenantId();
+ TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+ return checkNotNull(ruleChainService.findTenantRuleChains(tenantId, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.DELETE)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void deleteRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
+ checkParameter(RULE_CHAIN_ID, strRuleChainId);
+ try {
+ RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
+ RuleChain ruleChain = checkRuleChain(ruleChainId);
+
+ ruleChainService.deleteRuleChainById(ruleChainId);
+
+ actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.DELETED);
+
+ logEntityAction(ruleChainId, ruleChain,
+ null,
+ ActionType.DELETED, null, strRuleChainId);
+
+ } catch (Exception e) {
+ logEntityAction(emptyId(EntityType.RULE_CHAIN),
+ null,
+ null,
+ ActionType.DELETED, e, strRuleChainId);
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/ruleNode/{ruleNodeId}/debugIn", method = RequestMethod.GET)
+ @ResponseBody
+ public JsonNode getLatestRuleNodeDebugInput(@PathVariable(RULE_NODE_ID) String strRuleNodeId) throws ThingsboardException {
+ checkParameter(RULE_NODE_ID, strRuleNodeId);
+ try {
+ RuleNodeId ruleNodeId = new RuleNodeId(toUUID(strRuleNodeId));
+ TenantId tenantId = getCurrentUser().getTenantId();
+ List<Event> events = eventService.findLatestEvents(tenantId, ruleNodeId, DataConstants.DEBUG_RULE_NODE, 2);
+ JsonNode result = null;
+ if (events != null) {
+ for (Event event : events) {
+ JsonNode body = event.getBody();
+ if (body.has("type") && body.get("type").asText().equals("IN")) {
+ result = body;
+ break;
+ }
+ }
+ }
+ return result;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST)
+ @ResponseBody
+ public JsonNode testScript(@RequestBody JsonNode inputParams) throws ThingsboardException {
+ try {
+ String script = inputParams.get("script").asText();
+ String scriptType = inputParams.get("scriptType").asText();
+ JsonNode argNamesJson = inputParams.get("argNames");
+ String[] argNames = objectMapper.treeToValue(argNamesJson, String[].class);
+
+ String data = inputParams.get("msg").asText();
+ JsonNode metadataJson = inputParams.get("metadata");
+ Map<String, String> metadata = objectMapper.convertValue(metadataJson, new TypeReference<Map<String, String>>() {});
+ String msgType = inputParams.get("msgType").asText();
+ String output = "";
+ String errorText = "";
+ ScriptEngine engine = null;
+ try {
+ engine = new RuleNodeJsScriptEngine(jsSandboxService, script, argNames);
+ TbMsg inMsg = new TbMsg(UUIDs.timeBased(), msgType, null, new TbMsgMetaData(metadata), data, null, null, 0L);
+ switch (scriptType) {
+ case "update":
+ output = msgToOutput(engine.executeUpdate(inMsg));
+ break;
+ case "generate":
+ output = msgToOutput(engine.executeGenerate(inMsg));
+ break;
+ case "filter":
+ boolean result = engine.executeFilter(inMsg);
+ output = Boolean.toString(result);
+ break;
+ case "switch":
+ Set<String> states = engine.executeSwitch(inMsg);
+ output = objectMapper.writeValueAsString(states);
+ break;
+ case "json":
+ JsonNode json = engine.executeJson(inMsg);
+ output = objectMapper.writeValueAsString(json);
+ break;
+ case "string":
+ output = engine.executeToString(inMsg);
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported script type: " + scriptType);
+ }
+ } catch (Exception e) {
+ log.error("Error evaluating JS function", e);
+ errorText = e.getMessage();
+ } finally {
+ if (engine != null) {
+ engine.destroy();
+ }
+ }
+ ObjectNode result = objectMapper.createObjectNode();
+ result.put("output", output);
+ result.put("error", errorText);
+ return result;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ private String msgToOutput(TbMsg msg) throws Exception {
+ ObjectNode msgData = objectMapper.createObjectNode();
+ if (!StringUtils.isEmpty(msg.getData())) {
+ msgData.set("msg", objectMapper.readTree(msg.getData()));
+ }
+ Map<String, String> metadata = msg.getMetaData().getData();
+ msgData.set("metadata", objectMapper.valueToTree(metadata));
+ msgData.put("msgType", msg.getType());
+ return objectMapper.writeValueAsString(msgData);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
new file mode 100644
index 0000000..c2839ff
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
@@ -0,0 +1,570 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+import org.thingsboard.server.common.data.kv.DoubleDataEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.service.security.AccessValidator;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.telemetry.AttributeData;
+import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
+import org.thingsboard.server.service.telemetry.TsData;
+import org.thingsboard.server.service.telemetry.exception.InvalidParametersException;
+import org.thingsboard.server.service.telemetry.exception.UncheckedApiException;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/**
+ * Created by ashvayka on 22.03.18.
+ */
+@RestController
+@RequestMapping(TbUrlConstants.TELEMETRY_URL_PREFIX)
+@Slf4j
+public class TelemetryController extends BaseController {
+
+ @Autowired
+ private AttributesService attributesService;
+
+ @Autowired
+ private TimeseriesService tsService;
+
+ @Autowired
+ private TelemetrySubscriptionService tsSubService;
+
+ @Autowired
+ private AccessValidator accessValidator;
+
+ private ExecutorService executor;
+
+ @PostConstruct
+ public void initExecutor() {
+ executor = Executors.newSingleThreadExecutor();
+ }
+
+ @PreDestroy
+ public void shutdownExecutor() {
+ if (executor != null) {
+ executor.shutdownNow();
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/keys/attributes", method = RequestMethod.GET)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> getAttributeKeys(
+ @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr) throws ThingsboardException {
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, this::getAttributeKeysCallback);
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/keys/attributes/{scope}", method = RequestMethod.GET)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> getAttributeKeysByScope(
+ @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr
+ , @PathVariable("scope") String scope) throws ThingsboardException {
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+ (result, entityId) -> getAttributeKeysCallback(result, entityId, scope));
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/values/attributes", method = RequestMethod.GET)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> getAttributes(
+ @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
+ SecurityUser user = getCurrentUser();
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+ (result, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr));
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/values/attributes/{scope}", method = RequestMethod.GET)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> getAttributesByScope(
+ @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @PathVariable("scope") String scope,
+ @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
+ SecurityUser user = getCurrentUser();
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+ (result, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr));
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/keys/timeseries", method = RequestMethod.GET)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> getTimeseriesKeys(
+ @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr) throws ThingsboardException {
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+ (result, entityId) -> {
+ Futures.addCallback(tsService.findAllLatest(entityId), getTsKeysToResponseCallback(result));
+ });
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> getLatestTimeseries(
+ @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
+ SecurityUser user = getCurrentUser();
+
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+ (result, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr));
+ }
+
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET, params = {"keys", "startTs", "endTs"})
+ @ResponseBody
+ public DeferredResult<ResponseEntity> getTimeseries(
+ @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @RequestParam(name = "keys") String keys,
+ @RequestParam(name = "startTs") Long startTs,
+ @RequestParam(name = "endTs") Long endTs,
+ @RequestParam(name = "interval", defaultValue = "0") Long interval,
+ @RequestParam(name = "limit", defaultValue = "100") Integer limit,
+ @RequestParam(name = "agg", defaultValue = "NONE") String aggStr
+ ) throws ThingsboardException {
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+ (result, entityId) -> {
+ // If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted
+ Aggregation agg = interval == 0L ? Aggregation.valueOf(Aggregation.NONE.name()) : Aggregation.valueOf(aggStr);
+ List<TsKvQuery> queries = toKeysList(keys).stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, interval, limit, agg))
+ .collect(Collectors.toList());
+
+ Futures.addCallback(tsService.findAll(entityId, queries), getTsKvListCallback(result));
+ });
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.POST)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> saveDeviceAttributes(@PathVariable("deviceId") String deviceIdStr, @PathVariable("scope") String scope,
+ @RequestBody JsonNode request) throws ThingsboardException {
+ EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
+ return saveAttributes(entityId, scope, request);
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.POST)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> saveEntityAttributesV1(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @PathVariable("scope") String scope,
+ @RequestBody JsonNode request) throws ThingsboardException {
+ EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+ return saveAttributes(entityId, scope, request);
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/attributes/{scope}", method = RequestMethod.POST)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> saveEntityAttributesV2(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @PathVariable("scope") String scope,
+ @RequestBody JsonNode request) throws ThingsboardException {
+ EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+ return saveAttributes(entityId, scope, request);
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}", method = RequestMethod.POST)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> saveEntityTelemetry(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @PathVariable("scope") String scope,
+ @RequestBody String requestBody) throws ThingsboardException {
+ EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+ return saveTelemetry(entityId, requestBody, 0L);
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}/{ttl}", method = RequestMethod.POST)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> saveEntityTelemetryWithTTL(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @PathVariable("scope") String scope, @PathVariable("ttl") Long ttl,
+ @RequestBody String requestBody) throws ThingsboardException {
+ EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+ return saveTelemetry(entityId, requestBody, ttl);
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> deleteEntityAttributes(@PathVariable("deviceId") String deviceIdStr,
+ @PathVariable("scope") String scope,
+ @RequestParam(name = "keys") String keysStr) throws ThingsboardException {
+ EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
+ return deleteAttributes(entityId, scope, keysStr);
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> deleteEntityAttributes(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @PathVariable("scope") String scope,
+ @RequestParam(name = "keys") String keysStr) throws ThingsboardException {
+ EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+ return deleteAttributes(entityId, scope, keysStr);
+ }
+
+ private DeferredResult<ResponseEntity> deleteAttributes(EntityId entityIdStr, String scope, String keysStr) throws ThingsboardException {
+ List<String> keys = toKeysList(keysStr);
+ if (keys.isEmpty()) {
+ return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST);
+ }
+ SecurityUser user = getCurrentUser();
+ if (DataConstants.SERVER_SCOPE.equals(scope) ||
+ DataConstants.SHARED_SCOPE.equals(scope) ||
+ DataConstants.CLIENT_SCOPE.equals(scope)) {
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdStr, (result, entityId) -> {
+ ListenableFuture<List<Void>> future = attributesService.removeAll(entityId, scope, keys);
+ Futures.addCallback(future, new FutureCallback<List<Void>>() {
+ @Override
+ public void onSuccess(@Nullable List<Void> tmp) {
+ logAttributesDeleted(user, entityId, scope, keys, null);
+ result.setResult(new ResponseEntity<>(HttpStatus.OK));
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ logAttributesDeleted(user, entityId, scope, keys, t);
+ result.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+ }, executor);
+ });
+ } else {
+ return getImmediateDeferredResult("Invalid attribute scope: " + scope, HttpStatus.BAD_REQUEST);
+ }
+ }
+
+ private DeferredResult<ResponseEntity> saveAttributes(EntityId entityIdSrc, String scope, JsonNode json) throws ThingsboardException {
+ if (!DataConstants.SERVER_SCOPE.equals(scope) && !DataConstants.SHARED_SCOPE.equals(scope)) {
+ return getImmediateDeferredResult("Invalid scope: " + scope, HttpStatus.BAD_REQUEST);
+ }
+ if (json.isObject()) {
+ List<AttributeKvEntry> attributes = extractRequestAttributes(json);
+ if (attributes.isEmpty()) {
+ return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST);
+ }
+ SecurityUser user = getCurrentUser();
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdSrc, (result, entityId) -> {
+ tsSubService.saveAndNotify(entityId, scope, attributes, new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(@Nullable Void tmp) {
+ logAttributesUpdated(user, entityId, scope, attributes, null);
+ result.setResult(new ResponseEntity(HttpStatus.OK));
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ logAttributesUpdated(user, entityId, scope, attributes, t);
+ AccessValidator.handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ });
+ });
+ } else {
+ return getImmediateDeferredResult("Request is not a JSON object", HttpStatus.BAD_REQUEST);
+ }
+ }
+
+ private DeferredResult<ResponseEntity> saveTelemetry(EntityId entityIdSrc, String requestBody, long ttl) throws ThingsboardException {
+ TelemetryUploadRequest telemetryRequest;
+ JsonElement telemetryJson;
+ try {
+ telemetryJson = new JsonParser().parse(requestBody);
+ } catch (Exception e) {
+ return getImmediateDeferredResult("Unable to parse timeseries payload: Invalid JSON body!", HttpStatus.BAD_REQUEST);
+ }
+ try {
+ telemetryRequest = JsonConverter.convertToTelemetry(telemetryJson);
+ } catch (Exception e) {
+ return getImmediateDeferredResult("Unable to parse timeseries payload. Invalid JSON body: " + e.getMessage(), HttpStatus.BAD_REQUEST);
+ }
+ List<TsKvEntry> entries = new ArrayList<>();
+ for (Map.Entry<Long, List<KvEntry>> entry : telemetryRequest.getData().entrySet()) {
+ for (KvEntry kv : entry.getValue()) {
+ entries.add(new BasicTsKvEntry(entry.getKey(), kv));
+ }
+ }
+ if (entries.isEmpty()) {
+ return getImmediateDeferredResult("No timeseries data found in request body!", HttpStatus.BAD_REQUEST);
+ }
+ SecurityUser user = getCurrentUser();
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdSrc, (result, entityId) -> {
+ tsSubService.saveAndNotify(entityId, entries, ttl, new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(@Nullable Void tmp) {
+ result.setResult(new ResponseEntity(HttpStatus.OK));
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ AccessValidator.handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ });
+ });
+ }
+
+ private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, String keys) {
+ ListenableFuture<List<TsKvEntry>> future;
+ if (StringUtils.isEmpty(keys)) {
+ future = tsService.findAllLatest(entityId);
+ } else {
+ future = tsService.findLatest(entityId, toKeysList(keys));
+ }
+ Futures.addCallback(future, getTsKvListCallback(result));
+ }
+
+ private void getAttributeValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, String scope, String keys) {
+ List<String> keyList = toKeysList(keys);
+ FutureCallback<List<AttributeKvEntry>> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keyList);
+ if (!StringUtils.isEmpty(scope)) {
+ if (keyList != null && !keyList.isEmpty()) {
+ Futures.addCallback(attributesService.find(entityId, scope, keyList), callback);
+ } else {
+ Futures.addCallback(attributesService.findAll(entityId, scope), callback);
+ }
+ } else {
+ List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
+ for (String tmpScope : DataConstants.allScopes()) {
+ if (keyList != null && !keyList.isEmpty()) {
+ futures.add(attributesService.find(entityId, tmpScope, keyList));
+ } else {
+ futures.add(attributesService.findAll(entityId, tmpScope));
+ }
+ }
+
+ ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures);
+
+ Futures.addCallback(future, callback);
+ }
+ }
+
+ private void getAttributeKeysCallback(@Nullable DeferredResult<ResponseEntity> result, EntityId entityId, String scope) {
+ Futures.addCallback(attributesService.findAll(entityId, scope), getAttributeKeysToResponseCallback(result));
+ }
+
+ private void getAttributeKeysCallback(@Nullable DeferredResult<ResponseEntity> result, EntityId entityId) {
+ List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
+ for (String scope : DataConstants.allScopes()) {
+ futures.add(attributesService.findAll(entityId, scope));
+ }
+
+ ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures);
+
+ Futures.addCallback(future, getAttributeKeysToResponseCallback(result));
+ }
+
+ private FutureCallback<List<TsKvEntry>> getTsKeysToResponseCallback(final DeferredResult<ResponseEntity> response) {
+ return new FutureCallback<List<TsKvEntry>>() {
+ @Override
+ public void onSuccess(List<TsKvEntry> values) {
+ List<String> keys = values.stream().map(KvEntry::getKey).collect(Collectors.toList());
+ response.setResult(new ResponseEntity<>(keys, HttpStatus.OK));
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ log.error("Failed to fetch attributes", e);
+ AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ };
+ }
+
+ private FutureCallback<List<AttributeKvEntry>> getAttributeKeysToResponseCallback(final DeferredResult<ResponseEntity> response) {
+ return new FutureCallback<List<AttributeKvEntry>>() {
+
+ @Override
+ public void onSuccess(List<AttributeKvEntry> attributes) {
+ List<String> keys = attributes.stream().map(KvEntry::getKey).collect(Collectors.toList());
+ response.setResult(new ResponseEntity<>(keys, HttpStatus.OK));
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ log.error("Failed to fetch attributes", e);
+ AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ };
+ }
+
+ private FutureCallback<List<AttributeKvEntry>> getAttributeValuesToResponseCallback(final DeferredResult<ResponseEntity> response,
+ final SecurityUser user, final String scope,
+ final EntityId entityId, final List<String> keyList) {
+ return new FutureCallback<List<AttributeKvEntry>>() {
+ @Override
+ public void onSuccess(List<AttributeKvEntry> attributes) {
+ List<AttributeData> values = attributes.stream().map(attribute -> new AttributeData(attribute.getLastUpdateTs(),
+ attribute.getKey(), attribute.getValue())).collect(Collectors.toList());
+ logAttributesRead(user, entityId, scope, keyList, null);
+ response.setResult(new ResponseEntity<>(values, HttpStatus.OK));
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ log.error("Failed to fetch attributes", e);
+ logAttributesRead(user, entityId, scope, keyList, e);
+ AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ };
+ }
+
+ private FutureCallback<List<TsKvEntry>> getTsKvListCallback(final DeferredResult<ResponseEntity> response) {
+ return new FutureCallback<List<TsKvEntry>>() {
+ @Override
+ public void onSuccess(List<TsKvEntry> data) {
+ Map<String, List<TsData>> result = new LinkedHashMap<>();
+ for (TsKvEntry entry : data) {
+ result.computeIfAbsent(entry.getKey(), k -> new ArrayList<>())
+ .add(new TsData(entry.getTs(), entry.getValueAsString()));
+ }
+ response.setResult(new ResponseEntity<>(result, HttpStatus.OK));
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ log.error("Failed to fetch historical data", e);
+ AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ };
+ }
+
+ private void logAttributesDeleted(SecurityUser user, EntityId entityId, String scope, List<String> keys, Throwable e) {
+ try {
+ logEntityAction(user, (UUIDBased & EntityId)entityId, null, null, ActionType.ATTRIBUTES_DELETED, toException(e),
+ scope, keys);
+ } catch (ThingsboardException te) {
+ log.warn("Failed to log attributes delete", te);
+ }
+ }
+
+ private void logAttributesUpdated(SecurityUser user, EntityId entityId, String scope, List<AttributeKvEntry> attributes, Throwable e) {
+ try {
+ logEntityAction(user, (UUIDBased & EntityId)entityId, null, null, ActionType.ATTRIBUTES_UPDATED, toException(e),
+ scope, attributes);
+ } catch (ThingsboardException te) {
+ log.warn("Failed to log attributes update", te);
+ }
+ }
+
+
+ private void logAttributesRead(SecurityUser user, EntityId entityId, String scope, List<String> keys, Throwable e) {
+ try {
+ logEntityAction(user, (UUIDBased & EntityId)entityId, null, null, ActionType.ATTRIBUTES_READ, toException(e),
+ scope, keys);
+ } catch (ThingsboardException te) {
+ log.warn("Failed to log attributes read", te);
+ }
+ }
+
+ private ListenableFuture<List<AttributeKvEntry>> mergeAllAttributesFutures(List<ListenableFuture<List<AttributeKvEntry>>> futures) {
+ return Futures.transform(Futures.successfulAsList(futures),
+ (Function<? super List<List<AttributeKvEntry>>, ? extends List<AttributeKvEntry>>) input -> {
+ List<AttributeKvEntry> tmp = new ArrayList<>();
+ if (input != null) {
+ input.forEach(tmp::addAll);
+ }
+ return tmp;
+ }, executor);
+ }
+
+ private List<String> toKeysList(String keys) {
+ List<String> keyList = null;
+ if (!StringUtils.isEmpty(keys)) {
+ keyList = Arrays.asList(keys.split(","));
+ }
+ return keyList;
+ }
+
+ private DeferredResult<ResponseEntity> getImmediateDeferredResult(String message, HttpStatus status) {
+ DeferredResult<ResponseEntity> result = new DeferredResult<>();
+ result.setResult(new ResponseEntity<>(message, status));
+ return result;
+ }
+
+ private List<AttributeKvEntry> extractRequestAttributes(JsonNode jsonNode) {
+ long ts = System.currentTimeMillis();
+ List<AttributeKvEntry> attributes = new ArrayList<>();
+ jsonNode.fields().forEachRemaining(entry -> {
+ String key = entry.getKey();
+ JsonNode value = entry.getValue();
+ if (entry.getValue().isTextual()) {
+ attributes.add(new BaseAttributeKvEntry(new StringDataEntry(key, value.textValue()), ts));
+ } else if (entry.getValue().isBoolean()) {
+ attributes.add(new BaseAttributeKvEntry(new BooleanDataEntry(key, value.booleanValue()), ts));
+ } else if (entry.getValue().isDouble()) {
+ attributes.add(new BaseAttributeKvEntry(new DoubleDataEntry(key, value.doubleValue()), ts));
+ } else if (entry.getValue().isNumber()) {
+ if (entry.getValue().isBigInteger()) {
+ throw new UncheckedApiException(new InvalidParametersException("Big integer values are not supported!"));
+ } else {
+ attributes.add(new BaseAttributeKvEntry(new LongDataEntry(key, value.longValue()), ts));
+ }
+ }
+ });
+ return attributes;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java
index 5acb4eb..1a7c116 100644
--- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java
@@ -15,21 +15,34 @@
*/
package org.thingsboard.server.controller;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.tenant.TenantService;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.service.install.InstallScripts;
@RestController
@RequestMapping("/api")
+@Slf4j
public class TenantController extends BaseController {
-
+
+ @Autowired
+ private InstallScripts installScripts;
+
@Autowired
private TenantService tenantService;
@@ -49,10 +62,15 @@ public class TenantController extends BaseController {
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenant", method = RequestMethod.POST)
- @ResponseBody
+ @ResponseBody
public Tenant saveTenant(@RequestBody Tenant tenant) throws ThingsboardException {
try {
- return checkNotNull(tenantService.saveTenant(tenant));
+ boolean newTenant = tenant.getId() == null;
+ tenant = checkNotNull(tenantService.saveTenant(tenant));
+ if (newTenant) {
+ installScripts.createDefaultRuleChains(tenant.getId());
+ }
+ return tenant;
} catch (Exception e) {
throw handleException(e);
}
@@ -72,7 +90,7 @@ public class TenantController extends BaseController {
}
@PreAuthorize("hasAuthority('SYS_ADMIN')")
- @RequestMapping(value = "/tenants", params = { "limit" }, method = RequestMethod.GET)
+ @RequestMapping(value = "/tenants", params = {"limit"}, method = RequestMethod.GET)
@ResponseBody
public TextPageData<Tenant> getTenants(@RequestParam int limit,
@RequestParam(required = false) String textSearch,
@@ -85,5 +103,5 @@ public class TenantController extends BaseController {
throw handleException(e);
}
}
-
+
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java
index 2a1531a..5f6c1ce 100644
--- a/application/src/main/java/org/thingsboard/server/controller/UserController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java
@@ -18,10 +18,20 @@ package org.thingsboard.server.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
@@ -29,9 +39,6 @@ import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.service.mail.MailService;
import org.thingsboard.server.service.security.model.SecurityUser;
import javax.servlet.http.HttpServletRequest;
diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
index 757f765..eb229fe 100644
--- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
@@ -17,7 +17,15 @@ package org.thingsboard.server.controller;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.page.TextPageData;
@@ -25,7 +33,6 @@ import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardException;
import java.util.List;
diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
index 44c7d94..60f40a8 100644
--- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
@@ -17,13 +17,20 @@ package org.thingsboard.server.controller;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardException;
import java.util.List;
diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java
index 3b897d6..031073f 100644
--- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java
+++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.exception;
import org.springframework.http.HttpStatus;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import java.util.Date;
diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
index c70c561..63fe17a 100644
--- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
+++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
@@ -17,8 +17,6 @@ package org.thingsboard.server.exception;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -27,6 +25,8 @@ import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
index e765c40..f863d0b 100644
--- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
+++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
@@ -23,12 +23,11 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
+import org.thingsboard.server.service.install.DataUpdateService;
import org.thingsboard.server.service.install.DatabaseSchemaService;
import org.thingsboard.server.service.install.DatabaseUpgradeService;
import org.thingsboard.server.service.install.SystemDataLoaderService;
-import java.nio.file.Paths;
-
@Service
@Profile("install")
@Slf4j
@@ -40,9 +39,6 @@ public class ThingsboardInstallService {
@Value("${install.upgrade.from_version:1.2.3}")
private String upgradeFromVersion;
- @Value("${install.data_dir}")
- private String dataDir;
-
@Value("${install.load_demo:false}")
private Boolean loadDemo;
@@ -61,6 +57,9 @@ public class ThingsboardInstallService {
@Autowired
private SystemDataLoaderService systemDataLoaderService;
+ @Autowired
+ private DataUpdateService dataUpdateService;
+
public void performInstall() {
try {
if (isUpgrade) {
@@ -77,11 +76,18 @@ public class ThingsboardInstallService {
databaseUpgradeService.upgradeDatabase("1.3.0");
- case "1.3.1":
+ case "1.3.1": //NOSONAR, Need to execute gradual upgrade starting from upgradeFromVersion
log.info("Upgrading ThingsBoard from version 1.3.1 to 1.4.0 ...");
databaseUpgradeService.upgradeDatabase("1.3.1");
+ case "1.4.0":
+ log.info("Upgrading ThingsBoard from version 1.4.0 to 2.0.0 ...");
+
+ databaseUpgradeService.upgradeDatabase("1.4.0");
+
+ dataUpdateService.updateData("1.4.0");
+
log.info("Updating system data...");
systemDataLoaderService.deleteSystemWidgetBundle("charts");
@@ -108,13 +114,6 @@ public class ThingsboardInstallService {
log.info("Starting ThingsBoard Installation...");
- if (this.dataDir == null) {
- throw new RuntimeException("'install.data_dir' property should specified!");
- }
- if (!Paths.get(this.dataDir).toFile().isDirectory()) {
- throw new RuntimeException("'install.data_dir' property value is not a valid directory!");
- }
-
log.info("Installing DataBase schema...");
databaseSchemaService.createDatabaseSchema();
@@ -126,8 +125,8 @@ public class ThingsboardInstallService {
systemDataLoaderService.createSysAdmin();
systemDataLoaderService.createAdminSettings();
systemDataLoaderService.loadSystemWidgets();
- systemDataLoaderService.loadSystemPlugins();
- systemDataLoaderService.loadSystemRules();
+// systemDataLoaderService.loadSystemPlugins();
+// systemDataLoaderService.loadSystemRules();
if (loadDemo) {
log.info("Loading demo data...");
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DummyDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DummyDiscoveryService.java
index 865325f..c21f1aa 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DummyDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DummyDiscoveryService.java
@@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Service;
-import org.thingsboard.server.service.environment.EnvironmentLogService;
import javax.annotation.PostConstruct;
import java.util.Collections;
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstance.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstance.java
index 03c9694..6eee5f3 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstance.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstance.java
@@ -15,12 +15,11 @@
*/
package org.thingsboard.server.service.cluster.discovery;
-import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.gen.discovery.ServerInstanceProtos.ServerInfo;
+import org.thingsboard.server.gen.discovery.ServerInstanceProtos;
/**
* @author Andrew Shvayka
@@ -29,8 +28,6 @@ import org.thingsboard.server.gen.discovery.ServerInstanceProtos.ServerInfo;
@EqualsAndHashCode(exclude = {"serverInfo", "serverAddress"})
public final class ServerInstance implements Comparable<ServerInstance> {
- @Getter(AccessLevel.PACKAGE)
- private final ServerInfo serverInfo;
@Getter
private final String host;
@Getter
@@ -38,8 +35,13 @@ public final class ServerInstance implements Comparable<ServerInstance> {
@Getter
private final ServerAddress serverAddress;
- public ServerInstance(ServerInfo serverInfo) {
- this.serverInfo = serverInfo;
+ public ServerInstance(ServerAddress serverAddress) {
+ this.serverAddress = serverAddress;
+ this.host = serverAddress.getHost();
+ this.port = serverAddress.getPort();
+ }
+
+ public ServerInstance(ServerInstanceProtos.ServerInfo serverInfo) {
this.host = serverInfo.getHost();
this.port = serverInfo.getPort();
this.serverAddress = new ServerAddress(host, port);
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
index 818d2b1..6002b0e 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
@@ -15,8 +15,9 @@
*/
package org.thingsboard.server.service.cluster.discovery;
-import com.google.protobuf.InvalidProtocolBufferException;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.SerializationException;
+import org.apache.commons.lang3.SerializationUtils;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.ChildData;
@@ -31,15 +32,17 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
+import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
-import org.thingsboard.server.gen.discovery.ServerInstanceProtos.ServerInfo;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import org.thingsboard.server.utils.MiscUtils;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
-import java.io.IOException;
import java.util.List;
+import java.util.NoSuchElementException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
@@ -67,6 +70,10 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
@Autowired
private ServerInstanceService serverInstance;
+ @Autowired
+ @Lazy
+ private TelemetrySubscriptionService tsSubService;
+
private final List<DiscoveryServiceListener> listeners = new CopyOnWriteArrayList<>();
private CuratorFramework client;
@@ -113,7 +120,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
log.info("[{}:{}] Creating ZK node for current instance", self.getHost(), self.getPort());
nodePath = client.create()
.creatingParentsIfNeeded()
- .withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(zkNodesDir + "/", self.getServerInfo().toByteArray());
+ .withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(zkNodesDir + "/", SerializationUtils.serialize(self.getServerAddress()));
log.info("[{}:{}] Created ZK node for current instance: {}", self.getHost(), self.getPort(), nodePath);
} catch (Exception e) {
log.error("Failed to create ZK node", e);
@@ -144,8 +151,8 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
.filter(cd -> !cd.getPath().equals(nodePath))
.map(cd -> {
try {
- return new ServerInstance(ServerInfo.parseFrom(cd.getData()));
- } catch (InvalidProtocolBufferException e) {
+ return new ServerInstance( (ServerAddress) SerializationUtils.deserialize(cd.getData()));
+ } catch (NoSuchElementException e) {
log.error("Failed to decode ZK node", e);
throw new RuntimeException(e);
}
@@ -186,20 +193,23 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
}
ServerInstance instance;
try {
- instance = new ServerInstance(ServerInfo.parseFrom(data.getData()));
- } catch (IOException e) {
+ ServerAddress serverAddress = SerializationUtils.deserialize(data.getData());
+ instance = new ServerInstance(serverAddress);
+ } catch (SerializationException e) {
log.error("Failed to decode server instance for node {}", data.getPath(), e);
throw e;
}
log.info("Processing [{}] event for [{}:{}]", pathChildrenCacheEvent.getType(), instance.getHost(), instance.getPort());
switch (pathChildrenCacheEvent.getType()) {
case CHILD_ADDED:
+ tsSubService.onClusterUpdate();
listeners.forEach(listener -> listener.onServerAdded(instance));
break;
case CHILD_UPDATED:
listeners.forEach(listener -> listener.onServerUpdated(instance));
break;
case CHILD_REMOVED:
+ tsSubService.onClusterUpdate();
listeners.forEach(listener -> listener.onServerRemoved(instance));
break;
default:
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java
index b29668c..272073d 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java
@@ -16,9 +16,7 @@
package org.thingsboard.server.service.cluster.routing;
import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.service.cluster.discovery.ServerInstance;
import java.util.Optional;
import java.util.UUID;
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
index 4067797..5087b5c 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
@@ -23,7 +23,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
import org.thingsboard.server.service.cluster.discovery.DiscoveryServiceListener;
@@ -107,7 +106,7 @@ public class ConsistentClusterRoutingService implements ClusterRoutingService, D
@Override
public void onServerAdded(ServerInstance server) {
- log.debug("On server added event: {}", server);
+ log.info("On server added event: {}", server);
addNode(server);
logCircle();
}
@@ -119,7 +118,7 @@ public class ConsistentClusterRoutingService implements ClusterRoutingService, D
@Override
public void onServerRemoved(ServerInstance server) {
- log.debug("On server removed event: {}", server);
+ log.info("On server removed event: {}", server);
removeNode(server);
logCircle();
}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterGrpcService.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterGrpcService.java
index 5a6b307..19715b8 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterGrpcService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterGrpcService.java
@@ -19,38 +19,24 @@ import com.google.protobuf.ByteString;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
-import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
-import org.springframework.util.SerializationUtils;
import org.thingsboard.server.actors.rpc.RpcBroadcastMsg;
import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg;
-import org.thingsboard.server.actors.rpc.RpcSessionTellMsg;
-import org.thingsboard.server.actors.service.ActorService;
-import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg;
-import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
import org.thingsboard.server.gen.cluster.ClusterRpcServiceGrpc;
-import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
import org.thingsboard.server.service.cluster.discovery.ServerInstance;
import org.thingsboard.server.service.cluster.discovery.ServerInstanceService;
-import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
+import org.thingsboard.server.service.encoding.DataDecodingEncodingService;
-import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.IOException;
-import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -64,13 +50,17 @@ public class ClusterGrpcService extends ClusterRpcServiceGrpc.ClusterRpcServiceI
@Autowired
private ServerInstanceService instanceService;
+ @Autowired
+ private DataDecodingEncodingService encodingService;
+
private RpcMsgListener listener;
private Server server;
private ServerInstance instance;
- private ConcurrentMap<UUID, RpcSessionCreationFuture> pendingSessionMap = new ConcurrentHashMap<>();
+ private ConcurrentMap<UUID, BlockingQueue<StreamObserver<ClusterAPIProtos.ClusterMessage>>> pendingSessionMap =
+ new ConcurrentHashMap<>();
public void init(RpcMsgListener listener) {
this.listener = listener;
@@ -88,11 +78,11 @@ public class ClusterGrpcService extends ClusterRpcServiceGrpc.ClusterRpcServiceI
}
@Override
- public void onSessionCreated(UUID msgUid, StreamObserver<ClusterAPIProtos.ToRpcServerMessage> msg) {
- RpcSessionCreationFuture future = pendingSessionMap.remove(msgUid);
- if (future != null) {
+ public void onSessionCreated(UUID msgUid, StreamObserver<ClusterAPIProtos.ClusterMessage> inputStream) {
+ BlockingQueue<StreamObserver<ClusterAPIProtos.ClusterMessage>> queue = pendingSessionMap.remove(msgUid);
+ if (queue != null) {
try {
- future.onMsg(msg);
+ queue.put(inputStream);
} catch (InterruptedException e) {
log.warn("Failed to report created session!");
Thread.currentThread().interrupt();
@@ -103,11 +93,13 @@ public class ClusterGrpcService extends ClusterRpcServiceGrpc.ClusterRpcServiceI
}
@Override
- public StreamObserver<ClusterAPIProtos.ToRpcServerMessage> handlePluginMsgs(StreamObserver<ClusterAPIProtos.ToRpcServerMessage> responseObserver) {
+ public StreamObserver<ClusterAPIProtos.ClusterMessage> handleMsgs(
+ StreamObserver<ClusterAPIProtos.ClusterMessage> responseObserver) {
log.info("Processing new session.");
return createSession(new RpcSessionCreateRequestMsg(UUID.randomUUID(), null, responseObserver));
}
+
@PreDestroy
public void stop() {
if (server != null) {
@@ -123,65 +115,18 @@ public class ClusterGrpcService extends ClusterRpcServiceGrpc.ClusterRpcServiceI
}
}
- @Override
- public void tell(ServerAddress serverAddress, ToDeviceActorMsg toForward) {
- ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
- .setToDeviceActorRpcMsg(toProtoMsg(toForward)).build();
- tell(serverAddress, msg);
- }
-
- @Override
- public void tell(ServerAddress serverAddress, ToDeviceActorNotificationMsg toForward) {
- ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
- .setToDeviceActorNotificationRpcMsg(toProtoMsg(toForward)).build();
- tell(serverAddress, msg);
- }
-
- @Override
- public void tell(ServerAddress serverAddress, ToDeviceRpcRequestPluginMsg toForward) {
- ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
- .setToDeviceRpcRequestRpcMsg(toProtoMsg(toForward)).build();
- tell(serverAddress, msg);
- }
@Override
- public void tell(ServerAddress serverAddress, ToPluginRpcResponseDeviceMsg toForward) {
- ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
- .setToPluginRpcResponseRpcMsg(toProtoMsg(toForward)).build();
- tell(serverAddress, msg);
+ public void broadcast(RpcBroadcastMsg msg) {
+ listener.onBroadcastMsg(msg);
}
- @Override
- public void tell(ServerAddress serverAddress, ToDeviceSessionActorMsg toForward) {
- ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
- .setToDeviceSessionActorRpcMsg(toProtoMsg(toForward)).build();
- tell(serverAddress, msg);
- }
-
- @Override
- public void tell(PluginRpcMsg toForward) {
- ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
- .setToPluginRpcMsg(toProtoMsg(toForward)).build();
- tell(toForward.getRpcMsg().getServerAddress(), msg);
- }
-
- @Override
- public void broadcast(ToAllNodesMsg toForward) {
- ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
- .setToAllNodesRpcMsg(toProtoMsg(toForward)).build();
- listener.onMsg(new RpcBroadcastMsg(msg));
- }
-
- private void tell(ServerAddress serverAddress, ClusterAPIProtos.ToRpcServerMessage msg) {
- listener.onMsg(new RpcSessionTellMsg(serverAddress, msg));
- }
-
- private StreamObserver<ClusterAPIProtos.ToRpcServerMessage> createSession(RpcSessionCreateRequestMsg msg) {
- RpcSessionCreationFuture future = new RpcSessionCreationFuture();
- pendingSessionMap.put(msg.getMsgUid(), future);
- listener.onMsg(msg);
+ private StreamObserver<ClusterAPIProtos.ClusterMessage> createSession(RpcSessionCreateRequestMsg msg) {
+ BlockingQueue<StreamObserver<ClusterAPIProtos.ClusterMessage>> queue = new ArrayBlockingQueue<>(1);
+ pendingSessionMap.put(msg.getMsgUid(), queue);
+ listener.onRpcSessionCreateRequestMsg(msg);
try {
- StreamObserver<ClusterAPIProtos.ToRpcServerMessage> observer = future.get();
+ StreamObserver<ClusterAPIProtos.ClusterMessage> observer = queue.take();
log.info("Processed new session.");
return observer;
} catch (Exception e) {
@@ -190,86 +135,27 @@ public class ClusterGrpcService extends ClusterRpcServiceGrpc.ClusterRpcServiceI
}
}
- private static ClusterAPIProtos.ToDeviceActorRpcMessage toProtoMsg(ToDeviceActorMsg msg) {
- return ClusterAPIProtos.ToDeviceActorRpcMessage.newBuilder().setData(
- ByteString.copyFrom(SerializationUtils.serialize(msg))
- ).build();
- }
-
- private static ClusterAPIProtos.ToDeviceActorNotificationRpcMessage toProtoMsg(ToDeviceActorNotificationMsg msg) {
- return ClusterAPIProtos.ToDeviceActorNotificationRpcMessage.newBuilder().setData(
- ByteString.copyFrom(SerializationUtils.serialize(msg))
- ).build();
- }
-
- private static ClusterAPIProtos.ToDeviceRpcRequestRpcMessage toProtoMsg(ToDeviceRpcRequestPluginMsg msg) {
- ClusterAPIProtos.ToDeviceRpcRequestRpcMessage.Builder builder = ClusterAPIProtos.ToDeviceRpcRequestRpcMessage.newBuilder();
- ToDeviceRpcRequest request = msg.getMsg();
-
- builder.setAddress(ClusterAPIProtos.PluginAddress.newBuilder()
- .setTenantId(toUid(msg.getPluginTenantId().getId()))
- .setPluginId(toUid(msg.getPluginId().getId()))
- .build());
-
- builder.setDeviceTenantId(toUid(msg.getTenantId()));
- builder.setDeviceId(toUid(msg.getDeviceId()));
-
- builder.setMsgId(toUid(request.getId()));
- builder.setOneway(request.isOneway());
- builder.setExpTime(request.getExpirationTime());
- builder.setMethod(request.getBody().getMethod());
- builder.setParams(request.getBody().getParams());
-
- return builder.build();
- }
-
- private static ClusterAPIProtos.ToPluginRpcResponseRpcMessage toProtoMsg(ToPluginRpcResponseDeviceMsg msg) {
- ClusterAPIProtos.ToPluginRpcResponseRpcMessage.Builder builder = ClusterAPIProtos.ToPluginRpcResponseRpcMessage.newBuilder();
- FromDeviceRpcResponse request = msg.getResponse();
-
- builder.setAddress(ClusterAPIProtos.PluginAddress.newBuilder()
- .setTenantId(toUid(msg.getPluginTenantId().getId()))
- .setPluginId(toUid(msg.getPluginId().getId()))
- .build());
-
- builder.setMsgId(toUid(request.getId()));
- request.getResponse().ifPresent(builder::setResponse);
- request.getError().ifPresent(e -> builder.setError(e.name()));
-
- return builder.build();
- }
-
- private ClusterAPIProtos.ToAllNodesRpcMessage toProtoMsg(ToAllNodesMsg msg) {
- return ClusterAPIProtos.ToAllNodesRpcMessage.newBuilder().setData(
- ByteString.copyFrom(SerializationUtils.serialize(msg))
- ).build();
- }
-
-
- private ClusterAPIProtos.ToPluginRpcMessage toProtoMsg(PluginRpcMsg msg) {
- return ClusterAPIProtos.ToPluginRpcMessage.newBuilder()
- .setClazz(msg.getRpcMsg().getMsgClazz())
- .setData(ByteString.copyFrom(msg.getRpcMsg().getMsgData()))
- .setAddress(ClusterAPIProtos.PluginAddress.newBuilder()
- .setTenantId(toUid(msg.getPluginTenantId().getId()))
- .setPluginId(toUid(msg.getPluginId().getId()))
- .build()
- ).build();
- }
-
- private static ClusterAPIProtos.Uid toUid(EntityId uuid) {
- return toUid(uuid.getId());
+ @Override
+ public void tell(ClusterAPIProtos.ClusterMessage message) {
+ listener.onSendMsg(message);
}
- private static ClusterAPIProtos.Uid toUid(UUID uuid) {
- return ClusterAPIProtos.Uid.newBuilder().setPluginUuidMsb(uuid.getMostSignificantBits()).setPluginUuidLsb(
- uuid.getLeastSignificantBits()).build();
+ @Override
+ public void tell(ServerAddress serverAddress, TbActorMsg actorMsg) {
+ listener.onSendMsg(encodingService.convertToProtoDataMessage(serverAddress, actorMsg));
}
- private static ClusterAPIProtos.ToDeviceSessionActorRpcMessage toProtoMsg(ToDeviceSessionActorMsg msg) {
- return ClusterAPIProtos.ToDeviceSessionActorRpcMessage.newBuilder().setData(
- ByteString.copyFrom(SerializationUtils.serialize(msg))
- ).build();
+ @Override
+ public void tell(ServerAddress serverAddress, ClusterAPIProtos.MessageType msgType, byte[] data) {
+ ClusterAPIProtos.ClusterMessage msg = ClusterAPIProtos.ClusterMessage
+ .newBuilder()
+ .setServerAddress(ClusterAPIProtos.ServerAddress
+ .newBuilder()
+ .setHost(serverAddress.getHost())
+ .setPort(serverAddress.getPort())
+ .build())
+ .setMessageType(msgType)
+ .setPayload(ByteString.copyFrom(data)).build();
+ listener.onSendMsg(msg);
}
-
}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterRpcService.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterRpcService.java
index 8c50bb7..de29b89 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterRpcService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterRpcService.java
@@ -16,14 +16,9 @@
package org.thingsboard.server.service.cluster.rpc;
import io.grpc.stub.StreamObserver;
+import org.thingsboard.server.actors.rpc.RpcBroadcastMsg;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg;
-import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
import java.util.UUID;
@@ -35,19 +30,13 @@ public interface ClusterRpcService {
void init(RpcMsgListener listener);
- void tell(ServerAddress serverAddress, ToDeviceActorMsg toForward);
+ void broadcast(RpcBroadcastMsg msg);
- void tell(ServerAddress serverAddress, ToDeviceSessionActorMsg toForward);
+ void onSessionCreated(UUID msgUid, StreamObserver<ClusterAPIProtos.ClusterMessage> inputStream);
- void tell(ServerAddress serverAddress, ToDeviceActorNotificationMsg toForward);
+ void tell(ClusterAPIProtos.ClusterMessage message);
- void tell(ServerAddress serverAddress, ToDeviceRpcRequestPluginMsg toForward);
+ void tell(ServerAddress serverAddress, TbActorMsg actorMsg);
- void tell(ServerAddress serverAddress, ToPluginRpcResponseDeviceMsg toForward);
-
- void tell(PluginRpcMsg toForward);
-
- void broadcast(ToAllNodesMsg msg);
-
- void onSessionCreated(UUID msgUid, StreamObserver<ClusterAPIProtos.ToRpcServerMessage> inputStream);
+ void tell(ServerAddress serverAddress, ClusterAPIProtos.MessageType msgType, byte[] data);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java
index c403895..7216c43 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java
@@ -33,8 +33,8 @@ public final class GrpcSession implements Closeable {
private final UUID sessionId;
private final boolean client;
private final GrpcSessionListener listener;
- private StreamObserver<ClusterAPIProtos.ToRpcServerMessage> inputStream;
- private StreamObserver<ClusterAPIProtos.ToRpcServerMessage> outputStream;
+ private StreamObserver<ClusterAPIProtos.ClusterMessage> inputStream;
+ private StreamObserver<ClusterAPIProtos.ClusterMessage> outputStream;
private boolean connected;
private ServerAddress remoteServer;
@@ -56,17 +56,17 @@ public final class GrpcSession implements Closeable {
}
public void initInputStream() {
- this.inputStream = new StreamObserver<ClusterAPIProtos.ToRpcServerMessage>() {
+ this.inputStream = new StreamObserver<ClusterAPIProtos.ClusterMessage>() {
@Override
- public void onNext(ClusterAPIProtos.ToRpcServerMessage msg) {
- if (!connected && msg.hasConnectMsg()) {
+ public void onNext(ClusterAPIProtos.ClusterMessage clusterMessage) {
+ if (!connected && clusterMessage.getMessageType() == ClusterAPIProtos.MessageType.CONNECT_RPC_MESSAGE) {
connected = true;
- ClusterAPIProtos.ServerAddress rpcAddress = msg.getConnectMsg().getServerAddress();
+ ServerAddress rpcAddress = new ServerAddress(clusterMessage.getServerAddress().getHost(), clusterMessage.getServerAddress().getPort());
remoteServer = new ServerAddress(rpcAddress.getHost(), rpcAddress.getPort());
listener.onConnected(GrpcSession.this);
}
if (connected) {
- handleToRpcServerMessage(msg);
+ listener.onReceiveClusterGrpcMsg(GrpcSession.this, clusterMessage);
}
}
@@ -83,37 +83,13 @@ public final class GrpcSession implements Closeable {
};
}
- private void handleToRpcServerMessage(ClusterAPIProtos.ToRpcServerMessage msg) {
- if (msg.hasToPluginRpcMsg()) {
- listener.onToPluginRpcMsg(GrpcSession.this, msg.getToPluginRpcMsg());
- }
- if (msg.hasToDeviceActorRpcMsg()) {
- listener.onToDeviceActorRpcMsg(GrpcSession.this, msg.getToDeviceActorRpcMsg());
- }
- if (msg.hasToDeviceSessionActorRpcMsg()) {
- listener.onToDeviceSessionActorRpcMsg(GrpcSession.this, msg.getToDeviceSessionActorRpcMsg());
- }
- if (msg.hasToDeviceActorNotificationRpcMsg()) {
- listener.onToDeviceActorNotificationRpcMsg(GrpcSession.this, msg.getToDeviceActorNotificationRpcMsg());
- }
- if (msg.hasToDeviceRpcRequestRpcMsg()) {
- listener.onToDeviceRpcRequestRpcMsg(GrpcSession.this, msg.getToDeviceRpcRequestRpcMsg());
- }
- if (msg.hasToPluginRpcResponseRpcMsg()) {
- listener.onFromDeviceRpcResponseRpcMsg(GrpcSession.this, msg.getToPluginRpcResponseRpcMsg());
- }
- if (msg.hasToAllNodesRpcMsg()) {
- listener.onToAllNodesRpcMessage(GrpcSession.this, msg.getToAllNodesRpcMsg());
- }
- }
-
public void initOutputStream() {
if (client) {
listener.onConnected(GrpcSession.this);
}
}
- public void sendMsg(ClusterAPIProtos.ToRpcServerMessage msg) {
+ public void sendMsg(ClusterAPIProtos.ClusterMessage msg) {
outputStream.onNext(msg);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSessionListener.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSessionListener.java
index 44e0693..266b1f5 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSessionListener.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSessionListener.java
@@ -26,20 +26,7 @@ public interface GrpcSessionListener {
void onDisconnected(GrpcSession session);
- void onToPluginRpcMsg(GrpcSession session, ClusterAPIProtos.ToPluginRpcMessage msg);
-
- void onToDeviceActorRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceActorRpcMessage msg);
-
- void onToDeviceActorNotificationRpcMsg(GrpcSession grpcSession, ClusterAPIProtos.ToDeviceActorNotificationRpcMessage msg);
-
- void onToDeviceSessionActorRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceSessionActorRpcMessage msg);
-
- void onToAllNodesRpcMessage(GrpcSession grpcSession, ClusterAPIProtos.ToAllNodesRpcMessage toAllNodesRpcMessage);
-
- void onToDeviceRpcRequestRpcMsg(GrpcSession grpcSession, ClusterAPIProtos.ToDeviceRpcRequestRpcMessage toDeviceRpcRequestRpcMsg);
-
- void onFromDeviceRpcResponseRpcMsg(GrpcSession grpcSession, ClusterAPIProtos.ToPluginRpcResponseRpcMessage toPluginRpcResponseRpcMsg);
+ void onReceiveClusterGrpcMsg(GrpcSession session, ClusterAPIProtos.ClusterMessage clusterMessage);
void onError(GrpcSession session, Throwable t);
-
}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcMsgListener.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcMsgListener.java
index a5c3151..33f3847 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcMsgListener.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcMsgListener.java
@@ -17,34 +17,16 @@ package org.thingsboard.server.service.cluster.rpc;
import org.thingsboard.server.actors.rpc.RpcBroadcastMsg;
import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg;
-import org.thingsboard.server.actors.rpc.RpcSessionTellMsg;
-import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
-import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
/**
* @author Andrew Shvayka
*/
-public interface RpcMsgListener {
-
- void onMsg(ToDeviceActorMsg msg);
-
- void onMsg(ToDeviceActorNotificationMsg msg);
-
- void onMsg(ToDeviceSessionActorMsg msg);
-
- void onMsg(ToAllNodesMsg nodeMsg);
-
- void onMsg(ToPluginActorMsg msg);
-
- void onMsg(RpcSessionCreateRequestMsg msg);
-
- void onMsg(RpcSessionTellMsg rpcSessionTellMsg);
-
- void onMsg(RpcBroadcastMsg rpcBroadcastMsg);
+public interface RpcMsgListener {
+ void onReceivedMsg(ServerAddress remoteServer, ClusterAPIProtos.ClusterMessage msg);
+ void onSendMsg(ClusterAPIProtos.ClusterMessage msg);
+ void onRpcSessionCreateRequestMsg(RpcSessionCreateRequestMsg msg);
+ void onBroadcastMsg(RpcBroadcastMsg msg);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
index 0a6081d..f3ceed2 100644
--- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
@@ -15,9 +15,9 @@
*/
package org.thingsboard.server.service.component;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.google.common.base.Charsets;
-import com.google.common.io.Resources;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -26,15 +26,27 @@ import org.springframework.context.annotation.ClassPathScanningCandidateComponen
import org.springframework.core.env.Environment;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.stereotype.Service;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.rule.engine.api.NodeDefinition;
+import org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbRelationTypes;
+import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.dao.component.ComponentDescriptorService;
-import org.thingsboard.server.extensions.api.component.*;
import javax.annotation.PostConstruct;
import java.lang.annotation.Annotation;
-import java.util.*;
-import java.util.stream.Collectors;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
@Service
@Slf4j
@@ -66,6 +78,24 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
}
}
+ private void registerRuleNodeComponents() {
+ Set<BeanDefinition> ruleNodeBeanDefinitions = getBeanDefinitions(RuleNode.class);
+ for (BeanDefinition def : ruleNodeBeanDefinitions) {
+ try {
+ String clazzName = def.getBeanClassName();
+ Class<?> clazz = Class.forName(clazzName);
+ RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
+ ComponentType type = ruleNodeAnnotation.type();
+ ComponentDescriptor component = scanAndPersistComponent(def, type);
+ components.put(component.getClazz(), component);
+ componentsMap.computeIfAbsent(type, k -> new ArrayList<>()).add(component);
+ } catch (Exception e) {
+ log.error("Can't initialize component {}, due to {}", def.getBeanClassName(), e.getMessage(), e);
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
private void registerComponents(ComponentType type, Class<? extends Annotation> annotation) {
List<ComponentDescriptor> components = persist(getBeanDefinitions(annotation), type);
componentsMap.put(type, components);
@@ -79,8 +109,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
private List<ComponentDescriptor> persist(Set<BeanDefinition> filterDefs, ComponentType type) {
List<ComponentDescriptor> result = new ArrayList<>();
for (BeanDefinition def : filterDefs) {
- ComponentDescriptor scannedComponent = scanAndPersistComponent(def, type);
- result.add(scannedComponent);
+ result.add(scanAndPersistComponent(def, type));
}
return result;
}
@@ -91,49 +120,24 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
try {
scannedComponent.setType(type);
Class<?> clazz = Class.forName(clazzName);
- String descriptorResourceName;
switch (type) {
+ case ENRICHMENT:
case FILTER:
- Filter filterAnnotation = clazz.getAnnotation(Filter.class);
- scannedComponent.setName(filterAnnotation.name());
- scannedComponent.setScope(filterAnnotation.scope());
- descriptorResourceName = filterAnnotation.descriptor();
- break;
- case PROCESSOR:
- Processor processorAnnotation = clazz.getAnnotation(Processor.class);
- scannedComponent.setName(processorAnnotation.name());
- scannedComponent.setScope(processorAnnotation.scope());
- descriptorResourceName = processorAnnotation.descriptor();
- break;
+ case TRANSFORMATION:
case ACTION:
- Action actionAnnotation = clazz.getAnnotation(Action.class);
- scannedComponent.setName(actionAnnotation.name());
- scannedComponent.setScope(actionAnnotation.scope());
- descriptorResourceName = actionAnnotation.descriptor();
- break;
- case PLUGIN:
- Plugin pluginAnnotation = clazz.getAnnotation(Plugin.class);
- scannedComponent.setName(pluginAnnotation.name());
- scannedComponent.setScope(pluginAnnotation.scope());
- descriptorResourceName = pluginAnnotation.descriptor();
- for (Class<?> actionClazz : pluginAnnotation.actions()) {
- ComponentDescriptor actionComponent = getComponent(actionClazz.getName())
- .orElseThrow(() -> {
- log.error("Can't initialize plugin {}, due to missing action {}!", def.getBeanClassName(), actionClazz.getName());
- return new ClassNotFoundException("Action: " + actionClazz.getName() + "is missing!");
- });
- if (actionComponent.getType() != ComponentType.ACTION) {
- log.error("Plugin {} action {} has wrong component type!", def.getBeanClassName(), actionClazz.getName(), actionComponent.getType());
- throw new RuntimeException("Plugin " + def.getBeanClassName() + "action " + actionClazz.getName() + " has wrong component type!");
- }
- }
- scannedComponent.setActions(Arrays.stream(pluginAnnotation.actions()).map(action -> action.getName()).collect(Collectors.joining(",")));
+ case EXTERNAL:
+ RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
+ scannedComponent.setName(ruleNodeAnnotation.name());
+ scannedComponent.setScope(ruleNodeAnnotation.scope());
+ NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation);
+ ObjectNode configurationDescriptor = mapper.createObjectNode();
+ JsonNode node = mapper.valueToTree(nodeDefinition);
+ configurationDescriptor.set("nodeDefinition", node);
+ scannedComponent.setConfigurationDescriptor(configurationDescriptor);
break;
default:
throw new RuntimeException(type + " is not supported yet!");
}
- scannedComponent.setConfigurationDescriptor(mapper.readTree(
- Resources.toString(Resources.getResource(descriptorResourceName), Charsets.UTF_8)));
scannedComponent.setClazz(clazzName);
log.info("Processing scanned component: {}", scannedComponent);
} catch (Exception e) {
@@ -156,6 +160,34 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
return scannedComponent;
}
+ private NodeDefinition prepareNodeDefinition(RuleNode nodeAnnotation) throws Exception {
+ NodeDefinition nodeDefinition = new NodeDefinition();
+ nodeDefinition.setDetails(nodeAnnotation.nodeDetails());
+ nodeDefinition.setDescription(nodeAnnotation.nodeDescription());
+ nodeDefinition.setInEnabled(nodeAnnotation.inEnabled());
+ nodeDefinition.setOutEnabled(nodeAnnotation.outEnabled());
+ nodeDefinition.setRelationTypes(getRelationTypesWithFailureRelation(nodeAnnotation));
+ nodeDefinition.setCustomRelations(nodeAnnotation.customRelations());
+ Class<? extends NodeConfiguration> configClazz = nodeAnnotation.configClazz();
+ NodeConfiguration config = configClazz.newInstance();
+ NodeConfiguration defaultConfiguration = config.defaultConfiguration();
+ nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration));
+ nodeDefinition.setUiResources(nodeAnnotation.uiResources());
+ nodeDefinition.setConfigDirective(nodeAnnotation.configDirective());
+ nodeDefinition.setIcon(nodeAnnotation.icon());
+ nodeDefinition.setIconUrl(nodeAnnotation.iconUrl());
+ nodeDefinition.setDocUrl(nodeAnnotation.docUrl());
+ return nodeDefinition;
+ }
+
+ private String[] getRelationTypesWithFailureRelation(RuleNode nodeAnnotation) {
+ List<String> relationTypes = new ArrayList<>(Arrays.asList(nodeAnnotation.relationTypes()));
+ if (!relationTypes.contains(TbRelationTypes.FAILURE)) {
+ relationTypes.add(TbRelationTypes.FAILURE);
+ }
+ return relationTypes.toArray(new String[relationTypes.size()]);
+ }
+
private Set<BeanDefinition> getBeanDefinitions(Class<? extends Annotation> componentType) {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(componentType));
@@ -168,42 +200,30 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
@Override
public void discoverComponents() {
- registerComponents(ComponentType.FILTER, Filter.class);
-
- registerComponents(ComponentType.PROCESSOR, Processor.class);
-
- registerComponents(ComponentType.ACTION, Action.class);
-
- registerComponents(ComponentType.PLUGIN, Plugin.class);
-
+ registerRuleNodeComponents();
log.info("Found following definitions: {}", components.values());
}
@Override
public List<ComponentDescriptor> getComponents(ComponentType type) {
- return Collections.unmodifiableList(componentsMap.get(type));
+ if (componentsMap.containsKey(type)) {
+ return Collections.unmodifiableList(componentsMap.get(type));
+ } else {
+ return Collections.emptyList();
+ }
}
@Override
- public Optional<ComponentDescriptor> getComponent(String clazz) {
- return Optional.ofNullable(components.get(clazz));
+ public List<ComponentDescriptor> getComponents(Set<ComponentType> types) {
+ List<ComponentDescriptor> result = new ArrayList<>();
+ types.stream().filter(type -> componentsMap.containsKey(type)).forEach(type -> {
+ result.addAll(componentsMap.get(type));
+ });
+ return Collections.unmodifiableList(result);
}
@Override
- public List<ComponentDescriptor> getPluginActions(String pluginClazz) {
- Optional<ComponentDescriptor> pluginOpt = getComponent(pluginClazz);
- if (pluginOpt.isPresent()) {
- ComponentDescriptor plugin = pluginOpt.get();
- if (ComponentType.PLUGIN != plugin.getType()) {
- throw new IllegalArgumentException(pluginClazz + " is not a plugin!");
- }
- List<ComponentDescriptor> result = new ArrayList<>();
- for (String action : plugin.getActions().split(",")) {
- getComponent(action).ifPresent(v -> result.add(v));
- }
- return result;
- } else {
- throw new IllegalArgumentException(pluginClazz + " is not a component!");
- }
+ public Optional<ComponentDescriptor> getComponent(String clazz) {
+ return Optional.ofNullable(components.get(clazz));
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
index ea27e60..3ee3251 100644
--- a/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
@@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
/**
* @author Andrew Shvayka
@@ -30,8 +31,8 @@ public interface ComponentDiscoveryService {
List<ComponentDescriptor> getComponents(ComponentType type);
- Optional<ComponentDescriptor> getComponent(String clazz);
+ List<ComponentDescriptor> getComponents(Set<ComponentType> types);
- List<ComponentDescriptor> getPluginActions(String pluginClazz);
+ Optional<ComponentDescriptor> getComponent(String clazz);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/encoding/ProtoWithJavaSerializationDecodingEncodingService.java b/application/src/main/java/org/thingsboard/server/service/encoding/ProtoWithJavaSerializationDecodingEncodingService.java
new file mode 100644
index 0000000..2cf9299
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/encoding/ProtoWithJavaSerializationDecodingEncodingService.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.encoding;
+
+import com.google.protobuf.ByteString;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.util.SerializationUtils;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+
+import java.util.Optional;
+
+import static org.thingsboard.server.gen.cluster.ClusterAPIProtos.MessageType.CLUSTER_ACTOR_MESSAGE;
+
+
+@Slf4j
+@Service
+public class ProtoWithJavaSerializationDecodingEncodingService implements DataDecodingEncodingService {
+
+
+ @Override
+ public Optional<TbActorMsg> decode(byte[] byteArray) {
+ try {
+ TbActorMsg msg = (TbActorMsg) SerializationUtils.deserialize(byteArray);
+ return Optional.of(msg);
+
+ } catch (IllegalArgumentException e) {
+ log.error("Error during deserialization message, [{}]", e.getMessage());
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public byte[] encode(TbActorMsg msq) {
+ return SerializationUtils.serialize(msq);
+ }
+
+ @Override
+ public ClusterAPIProtos.ClusterMessage convertToProtoDataMessage(ServerAddress serverAddress,
+ TbActorMsg msg) {
+ return ClusterAPIProtos.ClusterMessage
+ .newBuilder()
+ .setServerAddress(ClusterAPIProtos.ServerAddress
+ .newBuilder()
+ .setHost(serverAddress.getHost())
+ .setPort(serverAddress.getPort())
+ .build())
+ .setMessageType(CLUSTER_ACTOR_MESSAGE)
+ .setPayload(ByteString.copyFrom(encode(msg))).build();
+
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java b/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java
new file mode 100644
index 0000000..210d9da
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.executors;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import org.thingsboard.rule.engine.api.ListeningExecutor;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+
+/**
+ * Created by igor on 4/13/18.
+ */
+public abstract class AbstractListeningExecutor implements ListeningExecutor {
+
+ private ListeningExecutorService service;
+
+ @PostConstruct
+ public void init() {
+ this.service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(getThreadPollSize()));
+ }
+
+ @PreDestroy
+ public void destroy() {
+ if (this.service != null) {
+ this.service.shutdown();
+ }
+ }
+
+ @Override
+ public <T> ListenableFuture<T> executeAsync(Callable<T> task) {
+ return service.submit(task);
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ service.execute(command);
+ }
+
+ public ListeningExecutorService executor() {
+ return service;
+ }
+
+ protected abstract int getThreadPollSize();
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseSchemaService.java
index 5bdc0a7..dd76b21 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseSchemaService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseSchemaService.java
@@ -17,7 +17,6 @@ package org.thingsboard.server.service.install;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.cassandra.CassandraInstallCluster;
@@ -37,17 +36,16 @@ public class CassandraDatabaseSchemaService implements DatabaseSchemaService {
private static final String CASSANDRA_DIR = "cassandra";
private static final String SCHEMA_CQL = "schema.cql";
- @Value("${install.data_dir}")
- private String dataDir;
-
@Autowired
private CassandraInstallCluster cluster;
+ @Autowired
+ private InstallScripts installScripts;
+
@Override
public void createDatabaseSchema() throws Exception {
log.info("Installing Cassandra DataBase schema...");
-
- Path schemaFile = Paths.get(this.dataDir, CASSANDRA_DIR, SCHEMA_CQL);
+ Path schemaFile = Paths.get(installScripts.getDataDir(), CASSANDRA_DIR, SCHEMA_CQL);
loadCql(schemaFile);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
index e6826ec..4d2adea 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
@@ -18,7 +18,6 @@ package org.thingsboard.server.service.install;
import com.datastax.driver.core.KeyspaceMetadata;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
@@ -33,7 +32,17 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
-import static org.thingsboard.server.service.install.DatabaseHelper.*;
+import static org.thingsboard.server.service.install.DatabaseHelper.ADDITIONAL_INFO;
+import static org.thingsboard.server.service.install.DatabaseHelper.ASSET;
+import static org.thingsboard.server.service.install.DatabaseHelper.ASSIGNED_CUSTOMERS;
+import static org.thingsboard.server.service.install.DatabaseHelper.CONFIGURATION;
+import static org.thingsboard.server.service.install.DatabaseHelper.CUSTOMER_ID;
+import static org.thingsboard.server.service.install.DatabaseHelper.DASHBOARD;
+import static org.thingsboard.server.service.install.DatabaseHelper.DEVICE;
+import static org.thingsboard.server.service.install.DatabaseHelper.ID;
+import static org.thingsboard.server.service.install.DatabaseHelper.SEARCH_TEXT;
+import static org.thingsboard.server.service.install.DatabaseHelper.TENANT_ID;
+import static org.thingsboard.server.service.install.DatabaseHelper.TITLE;
@Service
@NoSqlDao
@@ -43,9 +52,6 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
private static final String SCHEMA_UPDATE_CQL = "schema_update.cql";
- @Value("${install.data_dir}")
- private String dataDir;
-
@Autowired
private CassandraCluster cluster;
@@ -55,6 +61,9 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
@Autowired
private DashboardService dashboardService;
+ @Autowired
+ private InstallScripts installScripts;
+
@Override
public void upgradeDatabase(String fromVersion) throws Exception {
@@ -91,7 +100,7 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
log.info("Relations dumped.");
log.info("Updating schema ...");
- Path schemaUpdateFile = Paths.get(this.dataDir, "upgrade", "1.3.0", SCHEMA_UPDATE_CQL);
+ Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "1.3.0", SCHEMA_UPDATE_CQL);
loadCql(schemaUpdateFile);
log.info("Schema updated.");
@@ -173,7 +182,7 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
log.info("Updating schema ...");
- schemaUpdateFile = Paths.get(this.dataDir, "upgrade", "1.4.0", SCHEMA_UPDATE_CQL);
+ schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "1.4.0", SCHEMA_UPDATE_CQL);
loadCql(schemaUpdateFile);
log.info("Schema updated.");
@@ -186,6 +195,14 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
}
log.info("Dashboards restored.");
break;
+ case "1.4.0":
+
+ log.info("Updating schema ...");
+ schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.0.0", SCHEMA_UPDATE_CQL);
+ loadCql(schemaUpdateFile);
+ log.info("Schema updated.");
+
+ break;
default:
throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java
index c13d70e..bdf980f 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java
@@ -15,7 +15,16 @@
*/
package org.thingsboard.server.service.install.cql;
-import com.datastax.driver.core.*;
+import com.datastax.driver.core.BoundStatement;
+import com.datastax.driver.core.DataType;
+import com.datastax.driver.core.KeyspaceMetadata;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.Row;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.SimpleStatement;
+import com.datastax.driver.core.Statement;
+import com.datastax.driver.core.TableMetadata;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVPrinter;
@@ -25,7 +34,11 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
import static org.thingsboard.server.service.install.DatabaseHelper.CSV_DUMP_FORMAT;
diff --git a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java
index 4a21412..2f9b4a5 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java
@@ -30,7 +30,11 @@ import org.thingsboard.server.dao.dashboard.DashboardService;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
/**
* Created by igor on 2/27/18.
diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDataUpdateService.java
new file mode 100644
index 0000000..b372368
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDataUpdateService.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.install;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.IdBased;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.tenant.TenantService;
+
+import java.util.List;
+import java.util.UUID;
+
+@Service
+@Profile("install")
+@Slf4j
+public class DefaultDataUpdateService implements DataUpdateService {
+
+ @Autowired
+ private TenantService tenantService;
+
+ @Autowired
+ private RuleChainService ruleChainService;
+
+ @Autowired
+ private InstallScripts installScripts;
+
+ @Override
+ public void updateData(String fromVersion) throws Exception {
+ switch (fromVersion) {
+ case "1.4.0":
+ log.info("Updating data from version 1.4.0 to 2.0.0 ...");
+ tenantsDefaultRuleChainUpdater.updateEntities(null);
+ break;
+ default:
+ throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion);
+ }
+ }
+
+ private PaginatedUpdater<String, Tenant> tenantsDefaultRuleChainUpdater =
+ new PaginatedUpdater<String, Tenant>() {
+
+ @Override
+ protected List<Tenant> findEntities(String region, TextPageLink pageLink) {
+ return tenantService.findTenants(pageLink).getData();
+ }
+
+ @Override
+ protected void updateEntity(Tenant tenant) {
+ try {
+ RuleChain ruleChain = ruleChainService.getRootTenantRuleChain(tenant.getId());
+ if (ruleChain == null) {
+ installScripts.createDefaultRuleChains(tenant.getId());
+ }
+ } catch (Exception e) {
+ log.error("Unable to update Tenant", e);
+ }
+ }
+ };
+
+ public abstract class PaginatedUpdater<I, D extends IdBased<?>> {
+
+ private static final int DEFAULT_LIMIT = 100;
+
+ public void updateEntities(I id) {
+ TextPageLink pageLink = new TextPageLink(DEFAULT_LIMIT);
+ boolean hasNext = true;
+ while (hasNext) {
+ List<D> entities = findEntities(id, pageLink);
+ for (D entity : entities) {
+ updateEntity(entity);
+ }
+ hasNext = entities.size() == pageLink.getLimit();
+ if (hasNext) {
+ int index = entities.size() - 1;
+ UUID idOffset = entities.get(index).getUuidId();
+ pageLink.setIdOffset(idOffset);
+ }
+ }
+ }
+
+ protected abstract List<D> findEntities(I id, TextPageLink pageLink);
+
+ protected abstract void updateEntity(D entity);
+
+ }
+
+}
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
index 1ef805f..aa8c60b 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
@@ -15,66 +15,45 @@
*/
package org.thingsboard.server.service.install;
-import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
-import org.thingsboard.server.common.data.*;
+import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
-import org.thingsboard.server.common.data.plugin.PluginMetaData;
-import org.thingsboard.server.common.data.rule.RuleMetaData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.UserCredentials;
-import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.customer.CustomerService;
-import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.dao.plugin.PluginService;
-import org.thingsboard.server.dao.rule.RuleService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
-import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
-import java.io.IOException;
-import java.nio.file.DirectoryStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
@Service
@Profile("install")
@Slf4j
public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
- private static final String JSON_DIR = "json";
- private static final String SYSTEM_DIR = "system";
- private static final String DEMO_DIR = "demo";
- private static final String WIDGET_BUNDLES_DIR = "widget_bundles";
- private static final String PLUGINS_DIR = "plugins";
- private static final String RULES_DIR = "rules";
- private static final String DASHBOARDS_DIR = "dashboards";
-
private static final ObjectMapper objectMapper = new ObjectMapper();
- public static final String JSON_EXT = ".json";
public static final String CUSTOMER_CRED = "customer";
public static final String DEFAULT_DEVICE_TYPE = "default";
- @Value("${install.data_dir}")
- private String dataDir;
+ @Autowired
+ private InstallScripts installScripts;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@@ -89,15 +68,6 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
private WidgetsBundleService widgetsBundleService;
@Autowired
- private WidgetTypeService widgetTypeService;
-
- @Autowired
- private PluginService pluginService;
-
- @Autowired
- private RuleService ruleService;
-
- @Autowired
private TenantService tenantService;
@Autowired
@@ -109,9 +79,6 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
@Autowired
private DeviceCredentialsService deviceCredentialsService;
- @Autowired
- private DashboardService dashboardService;
-
@Bean
protected BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
@@ -147,55 +114,12 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
}
@Override
- public void loadSystemWidgets() throws Exception {
- Path widgetBundlesDir = Paths.get(dataDir, JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
- try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) {
- dirStream.forEach(
- path -> {
- try {
- JsonNode widgetsBundleDescriptorJson = objectMapper.readTree(path.toFile());
- JsonNode widgetsBundleJson = widgetsBundleDescriptorJson.get("widgetsBundle");
- WidgetsBundle widgetsBundle = objectMapper.treeToValue(widgetsBundleJson, WidgetsBundle.class);
- WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
- JsonNode widgetTypesArrayJson = widgetsBundleDescriptorJson.get("widgetTypes");
- widgetTypesArrayJson.forEach(
- widgetTypeJson -> {
- try {
- WidgetType widgetType = objectMapper.treeToValue(widgetTypeJson, WidgetType.class);
- widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
- widgetTypeService.saveWidgetType(widgetType);
- } catch (Exception e) {
- log.error("Unable to load widget type from json: [{}]", path.toString());
- throw new RuntimeException("Unable to load widget type from json", e);
- }
- }
- );
- } catch (Exception e) {
- log.error("Unable to load widgets bundle from json: [{}]", path.toString());
- throw new RuntimeException("Unable to load widgets bundle from json", e);
- }
- }
- );
- }
- }
-
- @Override
- public void loadSystemPlugins() throws Exception {
- loadPlugins(Paths.get(dataDir, JSON_DIR, SYSTEM_DIR, PLUGINS_DIR), null);
- }
-
-
- @Override
- public void loadSystemRules() throws Exception {
- loadRules(Paths.get(dataDir, JSON_DIR, SYSTEM_DIR, RULES_DIR), null);
- }
-
- @Override
public void loadDemoData() throws Exception {
Tenant demoTenant = new Tenant();
demoTenant.setRegion("Global");
demoTenant.setTitle("Tenant");
demoTenant = tenantService.saveTenant(demoTenant);
+ installScripts.createDefaultRuleChains(demoTenant.getId());
createUser(Authority.TENANT_ADMIN, demoTenant.getId(), null, "tenant@thingsboard.org", "tenant");
Customer customerA = new Customer();
@@ -227,9 +151,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
createDevice(demoTenant.getId(), null, DEFAULT_DEVICE_TYPE, "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", "Demo device that is used in " +
"Raspberry Pi GPIO control sample application");
- loadPlugins(Paths.get(dataDir, JSON_DIR, DEMO_DIR, PLUGINS_DIR), demoTenant.getId());
- loadRules(Paths.get(dataDir, JSON_DIR, DEMO_DIR, RULES_DIR), demoTenant.getId());
- loadDashboards(Paths.get(dataDir, JSON_DIR, DEMO_DIR, DASHBOARDS_DIR), demoTenant.getId(), null);
+ installScripts.loadDashboards(demoTenant.getId(), null);
}
@Override
@@ -240,6 +162,11 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
}
}
+ @Override
+ public void loadSystemWidgets() throws Exception {
+ installScripts.loadSystemWidgets();
+ }
+
private User createUser(Authority authority,
TenantId tenantId,
CustomerId customerId,
@@ -282,72 +209,4 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
return device;
}
- private void loadPlugins(Path pluginsDir, TenantId tenantId) throws Exception{
- try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(pluginsDir, path -> path.toString().endsWith(JSON_EXT))) {
- dirStream.forEach(
- path -> {
- try {
- JsonNode pluginJson = objectMapper.readTree(path.toFile());
- PluginMetaData plugin = objectMapper.treeToValue(pluginJson, PluginMetaData.class);
- plugin.setTenantId(tenantId);
- if (plugin.getState() == ComponentLifecycleState.ACTIVE) {
- plugin.setState(ComponentLifecycleState.SUSPENDED);
- PluginMetaData savedPlugin = pluginService.savePlugin(plugin);
- pluginService.activatePluginById(savedPlugin.getId());
- } else {
- pluginService.savePlugin(plugin);
- }
- } catch (Exception e) {
- log.error("Unable to load plugin from json: [{}]", path.toString());
- throw new RuntimeException("Unable to load plugin from json", e);
- }
- }
- );
- }
- }
-
- private void loadRules(Path rulesDir, TenantId tenantId) throws Exception {
- try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(rulesDir, path -> path.toString().endsWith(JSON_EXT))) {
- dirStream.forEach(
- path -> {
- try {
- JsonNode ruleJson = objectMapper.readTree(path.toFile());
- RuleMetaData rule = objectMapper.treeToValue(ruleJson, RuleMetaData.class);
- rule.setTenantId(tenantId);
- if (rule.getState() == ComponentLifecycleState.ACTIVE) {
- rule.setState(ComponentLifecycleState.SUSPENDED);
- RuleMetaData savedRule = ruleService.saveRule(rule);
- ruleService.activateRuleById(savedRule.getId());
- } else {
- ruleService.saveRule(rule);
- }
- } catch (Exception e) {
- log.error("Unable to load rule from json: [{}]", path.toString());
- throw new RuntimeException("Unable to load rule from json", e);
- }
- }
- );
- }
- }
-
- private void loadDashboards(Path dashboardsDir, TenantId tenantId, CustomerId customerId) throws Exception {
- try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dashboardsDir, path -> path.toString().endsWith(JSON_EXT))) {
- dirStream.forEach(
- path -> {
- try {
- JsonNode dashboardJson = objectMapper.readTree(path.toFile());
- Dashboard dashboard = objectMapper.treeToValue(dashboardJson, Dashboard.class);
- dashboard.setTenantId(tenantId);
- Dashboard savedDashboard = dashboardService.saveDashboard(dashboard);
- if (customerId != null && !customerId.isNullUid()) {
- dashboardService.assignDashboardToCustomer(savedDashboard.getId(), customerId);
- }
- } catch (Exception e) {
- log.error("Unable to load dashboard from json: [{}]", path.toString());
- throw new RuntimeException("Unable to load dashboard from json", e);
- }
- }
- );
- }
- }
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
new file mode 100644
index 0000000..0ad0ede
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
@@ -0,0 +1,184 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.install;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.widget.WidgetType;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.dao.dashboard.DashboardService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.widget.WidgetTypeService;
+import org.thingsboard.server.dao.widget.WidgetsBundleService;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper;
+
+/**
+ * Created by ashvayka on 18.04.18.
+ */
+@Component
+@Slf4j
+public class InstallScripts {
+
+ public static final String APP_DIR = "application";
+ public static final String SRC_DIR = "src";
+ public static final String MAIN_DIR = "main";
+ public static final String DATA_DIR = "data";
+ public static final String JSON_DIR = "json";
+ public static final String SYSTEM_DIR = "system";
+ public static final String TENANT_DIR = "tenant";
+ public static final String DEMO_DIR = "demo";
+ public static final String RULE_CHAINS_DIR = "rule_chains";
+ public static final String WIDGET_BUNDLES_DIR = "widget_bundles";
+ public static final String DASHBOARDS_DIR = "dashboards";
+
+ public static final String JSON_EXT = ".json";
+
+ @Value("${install.data_dir:}")
+ private String dataDir;
+
+ @Autowired
+ private RuleChainService ruleChainService;
+
+ @Autowired
+ private DashboardService dashboardService;
+
+ @Autowired
+ private WidgetTypeService widgetTypeService;
+
+ @Autowired
+ private WidgetsBundleService widgetsBundleService;
+
+ public Path getTenantRuleChainsDir() {
+ return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR);
+ }
+
+ public String getDataDir() {
+ if (!StringUtils.isEmpty(dataDir)) {
+ if (!Paths.get(this.dataDir).toFile().isDirectory()) {
+ throw new RuntimeException("'install.data_dir' property value is not a valid directory!");
+ }
+ return dataDir;
+ } else {
+ String workDir = System.getProperty("user.dir");
+ if (workDir.endsWith("application")) {
+ return Paths.get(workDir, SRC_DIR, MAIN_DIR, DATA_DIR).toString();
+ } else {
+ Path dataDirPath = Paths.get(workDir, APP_DIR, SRC_DIR, MAIN_DIR, DATA_DIR);
+ if (Files.exists(dataDirPath)) {
+ return dataDirPath.toString();
+ } else {
+ throw new RuntimeException("Not valid working directory: " + workDir + ". Please use either root project directory, application module directory or specify valid \"install.data_dir\" ENV variable to avoid automatic data directory lookup!");
+ }
+ }
+ }
+ }
+
+ public void createDefaultRuleChains(TenantId tenantId) throws IOException {
+ Path tenantChainsDir = getTenantRuleChainsDir();
+ try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(tenantChainsDir, path -> path.toString().endsWith(InstallScripts.JSON_EXT))) {
+ dirStream.forEach(
+ path -> {
+ try {
+ JsonNode ruleChainJson = objectMapper.readTree(path.toFile());
+ RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class);
+ RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class);
+
+ ruleChain.setTenantId(tenantId);
+ ruleChain = ruleChainService.saveRuleChain(ruleChain);
+
+ ruleChainMetaData.setRuleChainId(ruleChain.getId());
+ ruleChainService.saveRuleChainMetaData(ruleChainMetaData);
+ } catch (Exception e) {
+ log.error("Unable to load rule chain from json: [{}]", path.toString());
+ throw new RuntimeException("Unable to load rule chain from json", e);
+ }
+ }
+ );
+ }
+ }
+
+ public void loadSystemWidgets() throws Exception {
+ Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
+ try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) {
+ dirStream.forEach(
+ path -> {
+ try {
+ JsonNode widgetsBundleDescriptorJson = objectMapper.readTree(path.toFile());
+ JsonNode widgetsBundleJson = widgetsBundleDescriptorJson.get("widgetsBundle");
+ WidgetsBundle widgetsBundle = objectMapper.treeToValue(widgetsBundleJson, WidgetsBundle.class);
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+ JsonNode widgetTypesArrayJson = widgetsBundleDescriptorJson.get("widgetTypes");
+ widgetTypesArrayJson.forEach(
+ widgetTypeJson -> {
+ try {
+ WidgetType widgetType = objectMapper.treeToValue(widgetTypeJson, WidgetType.class);
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetTypeService.saveWidgetType(widgetType);
+ } catch (Exception e) {
+ log.error("Unable to load widget type from json: [{}]", path.toString());
+ throw new RuntimeException("Unable to load widget type from json", e);
+ }
+ }
+ );
+ } catch (Exception e) {
+ log.error("Unable to load widgets bundle from json: [{}]", path.toString());
+ throw new RuntimeException("Unable to load widgets bundle from json", e);
+ }
+ }
+ );
+ }
+ }
+
+ public void loadDashboards(TenantId tenantId, CustomerId customerId) throws Exception {
+ Path dashboardsDir = Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR);
+ try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dashboardsDir, path -> path.toString().endsWith(JSON_EXT))) {
+ dirStream.forEach(
+ path -> {
+ try {
+ JsonNode dashboardJson = objectMapper.readTree(path.toFile());
+ Dashboard dashboard = objectMapper.treeToValue(dashboardJson, Dashboard.class);
+ dashboard.setTenantId(tenantId);
+ Dashboard savedDashboard = dashboardService.saveDashboard(dashboard);
+ if (customerId != null && !customerId.isNullUid()) {
+ dashboardService.assignDashboardToCustomer(savedDashboard.getId(), customerId);
+ }
+ } catch (Exception e) {
+ log.error("Unable to load dashboard from json: [{}]", path.toString());
+ throw new RuntimeException("Unable to load dashboard from json", e);
+ }
+ }
+ );
+ }
+ }
+
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java b/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java
index f6c4749..6e3d354 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java
@@ -23,7 +23,12 @@ import org.apache.commons.csv.CSVRecord;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.sql.*;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseSchemaService.java
index 443ec0c..1daf660 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseSchemaService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseSchemaService.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.service.install;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
@@ -27,7 +28,6 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
-import java.sql.PreparedStatement;
@Service
@Profile("install")
@@ -38,9 +38,6 @@ public class SqlDatabaseSchemaService implements DatabaseSchemaService {
private static final String SQL_DIR = "sql";
private static final String SCHEMA_SQL = "schema.sql";
- @Value("${install.data_dir}")
- private String dataDir;
-
@Value("${spring.datasource.url}")
private String dbUrl;
@@ -50,12 +47,15 @@ public class SqlDatabaseSchemaService implements DatabaseSchemaService {
@Value("${spring.datasource.password}")
private String dbPassword;
+ @Autowired
+ private InstallScripts installScripts;
+
@Override
public void createDatabaseSchema() throws Exception {
log.info("Installing SQL DataBase schema...");
- Path schemaFile = Paths.get(this.dataDir, SQL_DIR, SCHEMA_SQL);
+ Path schemaFile = Paths.get(installScripts.getDataDir(), SQL_DIR, SCHEMA_SQL);
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
String sql = new String(Files.readAllBytes(schemaFile), Charset.forName("UTF-8"));
conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to load initial thingsboard database schema
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
index cdd3103..29d5c65 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
@@ -22,7 +22,6 @@ import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.util.SqlDao;
-import org.thingsboard.server.service.install.cql.CassandraDbHelper;
import org.thingsboard.server.service.install.sql.SqlDbHelper;
import java.nio.charset.Charset;
@@ -30,11 +29,16 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
-import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
-import static org.thingsboard.server.service.install.DatabaseHelper.*;
+import static org.thingsboard.server.service.install.DatabaseHelper.ASSIGNED_CUSTOMERS;
import static org.thingsboard.server.service.install.DatabaseHelper.CONFIGURATION;
+import static org.thingsboard.server.service.install.DatabaseHelper.CUSTOMER_ID;
+import static org.thingsboard.server.service.install.DatabaseHelper.DASHBOARD;
+import static org.thingsboard.server.service.install.DatabaseHelper.ID;
+import static org.thingsboard.server.service.install.DatabaseHelper.SEARCH_TEXT;
+import static org.thingsboard.server.service.install.DatabaseHelper.TENANT_ID;
+import static org.thingsboard.server.service.install.DatabaseHelper.TITLE;
@Service
@Profile("install")
@@ -44,9 +48,6 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService {
private static final String SCHEMA_UPDATE_SQL = "schema_update.sql";
- @Value("${install.data_dir}")
- private String dataDir;
-
@Value("${spring.datasource.url}")
private String dbUrl;
@@ -59,12 +60,15 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService {
@Autowired
private DashboardService dashboardService;
+ @Autowired
+ private InstallScripts installScripts;
+
@Override
public void upgradeDatabase(String fromVersion) throws Exception {
switch (fromVersion) {
case "1.3.0":
log.info("Updating schema ...");
- Path schemaUpdateFile = Paths.get(this.dataDir, "upgrade", "1.3.1", SCHEMA_UPDATE_SQL);
+ Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "1.3.1", SCHEMA_UPDATE_SQL);
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
String sql = new String(Files.readAllBytes(schemaUpdateFile), Charset.forName("UTF-8"));
conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
@@ -82,7 +86,7 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService {
log.info("Dashboards dumped.");
log.info("Updating schema ...");
- schemaUpdateFile = Paths.get(this.dataDir, "upgrade", "1.4.0", SCHEMA_UPDATE_SQL);
+ schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "1.4.0", SCHEMA_UPDATE_SQL);
String sql = new String(Files.readAllBytes(schemaUpdateFile), Charset.forName("UTF-8"));
conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
log.info("Schema updated.");
@@ -97,6 +101,15 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService {
log.info("Dashboards restored.");
}
break;
+ case "1.4.0":
+ try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
+ log.info("Updating schema ...");
+ schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.0.0", SCHEMA_UPDATE_SQL);
+ String sql = new String(Files.readAllBytes(schemaUpdateFile), Charset.forName("UTF-8"));
+ conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
+ log.info("Schema updated.");
+ }
+ break;
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
index a3dcb68..f3a6af4 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
@@ -23,10 +23,6 @@ public interface SystemDataLoaderService {
void loadSystemWidgets() throws Exception;
- void loadSystemPlugins() throws Exception;
-
- void loadSystemRules() throws Exception;
-
void loadDemoData() throws Exception;
void deleteSystemWidgetBundle(String bundleAlias) throws Exception;
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
index 25b911c..818c935 100644
--- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
+++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
@@ -27,13 +27,15 @@ import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.ui.velocity.VelocityEngineUtils;
+import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.settings.AdminSettingsService;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
import javax.annotation.PostConstruct;
+import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.util.HashMap;
import java.util.Locale;
@@ -49,18 +51,18 @@ public class DefaultMailService implements MailService {
public static final String UTF_8 = "UTF-8";
@Autowired
private MessageSource messages;
-
+
@Autowired
@Qualifier("velocityEngine")
private VelocityEngine engine;
-
+
private JavaMailSenderImpl mailSender;
-
+
private String mailFrom;
-
+
@Autowired
- private AdminSettingsService adminSettingsService;
-
+ private AdminSettingsService adminSettingsService;
+
@PostConstruct
private void init() {
updateMailConfiguration();
@@ -77,7 +79,7 @@ public class DefaultMailService implements MailService {
throw new IncorrectParameterException("Failed to date mail configuration. Settings not found!");
}
}
-
+
private JavaMailSenderImpl createMailSender(JsonNode jsonConfig) {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(jsonConfig.get("smtpHost").asText());
@@ -99,7 +101,7 @@ public class DefaultMailService implements MailService {
javaMailProperties.put(MAIL_PROP + protocol + ".starttls.enable", jsonConfig.has("enableTls") ? jsonConfig.get("enableTls").asText() : "false");
return javaMailProperties;
}
-
+
private int parsePort(String strPort) {
try {
return Integer.valueOf(strPort);
@@ -112,86 +114,102 @@ public class DefaultMailService implements MailService {
public void sendEmail(String email, String subject, String message) throws ThingsboardException {
sendMail(mailSender, mailFrom, email, subject, message);
}
-
+
@Override
public void sendTestMail(JsonNode jsonConfig, String email) throws ThingsboardException {
JavaMailSenderImpl testMailSender = createMailSender(jsonConfig);
String mailFrom = jsonConfig.get("mailFrom").asText();
String subject = messages.getMessage("test.message.subject", null, Locale.US);
-
+
Map<String, Object> model = new HashMap<String, Object>();
model.put(TARGET_EMAIL, email);
-
+
String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
"test.vm", UTF_8, model);
-
- sendMail(testMailSender, mailFrom, email, subject, message);
+
+ sendMail(testMailSender, mailFrom, email, subject, message);
}
@Override
public void sendActivationEmail(String activationLink, String email) throws ThingsboardException {
-
+
String subject = messages.getMessage("activation.subject", null, Locale.US);
-
+
Map<String, Object> model = new HashMap<String, Object>();
model.put("activationLink", activationLink);
model.put(TARGET_EMAIL, email);
-
+
String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
"activation.vm", UTF_8, model);
-
- sendMail(mailSender, mailFrom, email, subject, message);
+
+ sendMail(mailSender, mailFrom, email, subject, message);
}
-
+
@Override
public void sendAccountActivatedEmail(String loginLink, String email) throws ThingsboardException {
-
+
String subject = messages.getMessage("account.activated.subject", null, Locale.US);
-
+
Map<String, Object> model = new HashMap<String, Object>();
model.put("loginLink", loginLink);
model.put(TARGET_EMAIL, email);
-
+
String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
"account.activated.vm", UTF_8, model);
-
- sendMail(mailSender, mailFrom, email, subject, message);
+
+ sendMail(mailSender, mailFrom, email, subject, message);
}
@Override
public void sendResetPasswordEmail(String passwordResetLink, String email) throws ThingsboardException {
-
+
String subject = messages.getMessage("reset.password.subject", null, Locale.US);
-
+
Map<String, Object> model = new HashMap<String, Object>();
model.put("passwordResetLink", passwordResetLink);
model.put(TARGET_EMAIL, email);
-
+
String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
"reset.password.vm", UTF_8, model);
-
- sendMail(mailSender, mailFrom, email, subject, message);
+
+ sendMail(mailSender, mailFrom, email, subject, message);
}
-
+
@Override
public void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException {
-
+
String subject = messages.getMessage("password.was.reset.subject", null, Locale.US);
-
+
Map<String, Object> model = new HashMap<String, Object>();
model.put("loginLink", loginLink);
model.put(TARGET_EMAIL, email);
-
+
String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
"password.was.reset.vm", UTF_8, model);
-
- sendMail(mailSender, mailFrom, email, subject, message);
+
+ sendMail(mailSender, mailFrom, email, subject, message);
}
+ @Override
+ public void send(String from, String to, String cc, String bcc, String subject, String body) throws MessagingException {
+ MimeMessage mailMsg = mailSender.createMimeMessage();
+ MimeMessageHelper helper = new MimeMessageHelper(mailMsg, "UTF-8");
+ helper.setFrom(StringUtils.isBlank(from) ? mailFrom : from);
+ helper.setTo(to.split("\\s*,\\s*"));
+ if (!StringUtils.isBlank(cc)) {
+ helper.setCc(cc.split("\\s*,\\s*"));
+ }
+ if (!StringUtils.isBlank(bcc)) {
+ helper.setBcc(bcc.split("\\s*,\\s*"));
+ }
+ helper.setSubject(subject);
+ helper.setText(body);
+ mailSender.send(helper.getMimeMessage());
+ }
- private void sendMail(JavaMailSenderImpl mailSender,
- String mailFrom, String email,
- String subject, String message) throws ThingsboardException {
+ private void sendMail(JavaMailSenderImpl mailSender,
+ String mailFrom, String email,
+ String subject, String message) throws ThingsboardException {
try {
MimeMessage mimeMsg = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMsg, UTF_8);
@@ -208,7 +226,7 @@ public class DefaultMailService implements MailService {
protected ThingsboardException handleException(Exception exception) {
String message;
if (exception instanceof NestedRuntimeException) {
- message = ((NestedRuntimeException)exception).getMostSpecificCause().getMessage();
+ message = ((NestedRuntimeException) exception).getMostSpecificCause().getMessage();
} else {
message = exception.getMessage();
}
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultMsgQueueService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultMsgQueueService.java
new file mode 100644
index 0000000..9275847
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultMsgQueueService.java
@@ -0,0 +1,111 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.queue;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.transport.quota.tenant.TenantQuotaService;
+import org.thingsboard.server.dao.queue.MsgQueue;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+@Service
+@Slf4j
+public class DefaultMsgQueueService implements MsgQueueService {
+
+ @Value("${actors.rule.queue.max_size}")
+ private long queueMaxSize;
+
+ @Value("${actors.rule.queue.cleanup_period}")
+ private long queueCleanUpPeriod;
+
+ @Autowired
+ private MsgQueue msgQueue;
+
+ @Autowired
+ private TenantQuotaService quotaService;
+
+ private ScheduledExecutorService cleanupExecutor;
+
+ private Map<TenantId, AtomicLong> pendingCountPerTenant = new ConcurrentHashMap<>();
+
+ @PostConstruct
+ public void init() {
+ if (queueCleanUpPeriod > 0) {
+ cleanupExecutor = Executors.newSingleThreadScheduledExecutor();
+ cleanupExecutor.scheduleAtFixedRate(() -> cleanup(),
+ queueCleanUpPeriod, queueCleanUpPeriod, TimeUnit.SECONDS);
+ }
+ }
+
+ @PreDestroy
+ public void stop() {
+ if (cleanupExecutor != null) {
+ cleanupExecutor.shutdownNow();
+ }
+ }
+
+ @Override
+ public ListenableFuture<Void> put(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition) {
+ if(quotaService.isQuotaExceeded(tenantId.getId().toString())) {
+ log.warn("Tenant TbMsg Quota exceeded for [{}:{}] . Reject", tenantId.getId());
+ return Futures.immediateFailedFuture(new RuntimeException("Tenant TbMsg Quota exceeded"));
+ }
+
+ AtomicLong pendingMsgCount = pendingCountPerTenant.computeIfAbsent(tenantId, key -> new AtomicLong());
+ if (pendingMsgCount.incrementAndGet() < queueMaxSize) {
+ return msgQueue.put(tenantId, msg, nodeId, clusterPartition);
+ } else {
+ pendingMsgCount.decrementAndGet();
+ return Futures.immediateFailedFuture(new RuntimeException("Message queue is full!"));
+ }
+ }
+
+ @Override
+ public ListenableFuture<Void> ack(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition) {
+ ListenableFuture<Void> result = msgQueue.ack(tenantId, msg, nodeId, clusterPartition);
+ AtomicLong pendingMsgCount = pendingCountPerTenant.computeIfAbsent(tenantId, key -> new AtomicLong());
+ pendingMsgCount.decrementAndGet();
+ return result;
+ }
+
+ @Override
+ public Iterable<TbMsg> findUnprocessed(TenantId tenantId, UUID nodeId, long clusterPartition) {
+ return msgQueue.findUnprocessed(tenantId, nodeId, clusterPartition);
+ }
+
+ private void cleanup() {
+ pendingCountPerTenant.forEach((tenantId, pendingMsgCount) -> {
+ pendingMsgCount.set(0);
+ msgQueue.cleanUp(tenantId);
+ });
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultDeviceRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultDeviceRpcService.java
new file mode 100644
index 0000000..678495b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultDeviceRpcService.java
@@ -0,0 +1,152 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.rpc;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.rule.engine.api.RpcError;
+import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.core.ToServerRpcResponseMsg;
+import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
+import org.thingsboard.server.dao.audit.AuditLogService;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
+import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
+import org.thingsboard.server.service.encoding.DataDecodingEncodingService;
+import org.thingsboard.server.service.telemetry.sub.Subscription;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+@Service
+@Slf4j
+public class DefaultDeviceRpcService implements DeviceRpcService {
+
+ @Autowired
+ private ClusterRoutingService routingService;
+
+ @Autowired
+ private ClusterRpcService rpcService;
+
+ @Autowired
+ private ActorService actorService;
+
+ private ScheduledExecutorService rpcCallBackExecutor;
+
+ private final ConcurrentMap<UUID, Consumer<FromDeviceRpcResponse>> localRpcRequests = new ConcurrentHashMap<>();
+
+ @PostConstruct
+ public void initExecutor() {
+ rpcCallBackExecutor = Executors.newSingleThreadScheduledExecutor();
+ }
+
+ @PreDestroy
+ public void shutdownExecutor() {
+ if (rpcCallBackExecutor != null) {
+ rpcCallBackExecutor.shutdownNow();
+ }
+ }
+
+ @Override
+ public void processRpcRequestToDevice(ToDeviceRpcRequest request, Consumer<FromDeviceRpcResponse> responseConsumer) {
+ log.trace("[{}] Processing local rpc call for device [{}]", request.getTenantId(), request.getDeviceId());
+ sendRpcRequest(request);
+ UUID requestId = request.getId();
+ localRpcRequests.put(requestId, responseConsumer);
+ long timeout = Math.max(0, request.getExpirationTime() - System.currentTimeMillis());
+ log.error("[{}] processing the request: [{}]", this.hashCode(), requestId);
+ rpcCallBackExecutor.schedule(() -> {
+ log.error("[{}] timeout the request: [{}]", this.hashCode(), requestId);
+ Consumer<FromDeviceRpcResponse> consumer = localRpcRequests.remove(requestId);
+ if (consumer != null) {
+ consumer.accept(new FromDeviceRpcResponse(requestId, null, null, RpcError.TIMEOUT));
+ }
+ }, timeout, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void processRpcResponseFromDevice(FromDeviceRpcResponse response) {
+ log.error("[{}] response to request: [{}]", this.hashCode(), response.getId());
+ if (routingService.getCurrentServer().equals(response.getServerAddress())) {
+ UUID requestId = response.getId();
+ Consumer<FromDeviceRpcResponse> consumer = localRpcRequests.remove(requestId);
+ if (consumer != null) {
+ consumer.accept(response);
+ } else {
+ log.trace("[{}] Unknown or stale rpc response received [{}]", requestId, response);
+ }
+ } else {
+ ClusterAPIProtos.FromDeviceRPCResponseProto.Builder builder = ClusterAPIProtos.FromDeviceRPCResponseProto.newBuilder();
+ builder.setRequestIdMSB(response.getId().getMostSignificantBits());
+ builder.setRequestIdLSB(response.getId().getLeastSignificantBits());
+ response.getResponse().ifPresent(builder::setResponse);
+ if (response.getError().isPresent()) {
+ builder.setError(response.getError().get().ordinal());
+ } else {
+ builder.setError(-1);
+ }
+ rpcService.tell(response.getServerAddress(), ClusterAPIProtos.MessageType.CLUSTER_RPC_FROM_DEVICE_RESPONSE_MESSAGE, builder.build().toByteArray());
+ }
+ }
+
+ @Override
+ public void processRemoteResponseFromDevice(ServerAddress serverAddress, byte[] data) {
+ ClusterAPIProtos.FromDeviceRPCResponseProto proto;
+ try {
+ proto = ClusterAPIProtos.FromDeviceRPCResponseProto.parseFrom(data);
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ RpcError error = proto.getError() > 0 ? RpcError.values()[proto.getError()] : null;
+ FromDeviceRpcResponse response = new FromDeviceRpcResponse(new UUID(proto.getRequestIdMSB(), proto.getRequestIdLSB()), routingService.getCurrentServer(),
+ proto.getResponse(), error);
+ processRpcResponseFromDevice(response);
+ }
+
+ @Override
+ public void sendRpcReplyToDevice(TenantId tenantId, DeviceId deviceId, int requestId, String body) {
+ ToServerRpcResponseActorMsg rpcMsg = new ToServerRpcResponseActorMsg(tenantId, deviceId, new ToServerRpcResponseMsg(requestId, body));
+ forward(deviceId, rpcMsg);
+ }
+
+ private void sendRpcRequest(ToDeviceRpcRequest msg) {
+ ToDeviceRpcRequestActorMsg rpcMsg = new ToDeviceRpcRequestActorMsg(routingService.getCurrentServer(), msg);
+ log.trace("[{}] Forwarding msg {} to device actor!", msg.getDeviceId(), msg);
+ forward(msg.getDeviceId(), rpcMsg);
+ }
+
+ private <T extends ToDeviceActorNotificationMsg> void forward(DeviceId deviceId, T msg) {
+ actorService.onMsg(new SendToClusterMsg(deviceId, msg));
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java b/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java
new file mode 100644
index 0000000..7f274ec
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java
@@ -0,0 +1,159 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.thingsboard.server.service.script;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import delight.nashornsandbox.NashornSandbox;
+import delight.nashornsandbox.NashornSandboxes;
+import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.script.Invocable;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Slf4j
+public abstract class AbstractNashornJsSandboxService implements JsSandboxService {
+
+ private NashornSandbox sandbox;
+ private ScriptEngine engine;
+ private ExecutorService monitorExecutorService;
+
+ private Map<UUID, String> functionsMap = new ConcurrentHashMap<>();
+
+ private Map<UUID,AtomicInteger> blackListedFunctions = new ConcurrentHashMap<>();
+
+ @PostConstruct
+ public void init() {
+ if (useJsSandbox()) {
+ sandbox = NashornSandboxes.create();
+ monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize());
+ sandbox.setExecutor(monitorExecutorService);
+ sandbox.setMaxCPUTime(getMaxCpuTime());
+ sandbox.allowNoBraces(false);
+ sandbox.setMaxPreparedStatements(30);
+ } else {
+ NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
+ engine = factory.getScriptEngine(new String[]{"--no-java"});
+ }
+ }
+
+ @PreDestroy
+ public void stop() {
+ if (monitorExecutorService != null) {
+ monitorExecutorService.shutdownNow();
+ }
+ }
+
+ protected abstract boolean useJsSandbox();
+
+ protected abstract int getMonitorThreadPoolSize();
+
+ protected abstract long getMaxCpuTime();
+
+ protected abstract int getMaxErrors();
+
+ @Override
+ public ListenableFuture<UUID> eval(JsScriptType scriptType, String scriptBody, String... argNames) {
+ UUID scriptId = UUID.randomUUID();
+ String functionName = "invokeInternal_" + scriptId.toString().replace('-','_');
+ String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames);
+ try {
+ if (useJsSandbox()) {
+ sandbox.eval(jsScript);
+ } else {
+ engine.eval(jsScript);
+ }
+ functionsMap.put(scriptId, functionName);
+ } catch (Exception e) {
+ log.warn("Failed to compile JS script: {}", e.getMessage(), e);
+ return Futures.immediateFailedFuture(e);
+ }
+ return Futures.immediateFuture(scriptId);
+ }
+
+ @Override
+ public ListenableFuture<Object> invokeFunction(UUID scriptId, Object... args) {
+ String functionName = functionsMap.get(scriptId);
+ if (functionName == null) {
+ return Futures.immediateFailedFuture(new RuntimeException("No compiled script found for scriptId: [" + scriptId + "]!"));
+ }
+ if (!isBlackListed(scriptId)) {
+ try {
+ Object result;
+ if (useJsSandbox()) {
+ result = sandbox.getSandboxedInvocable().invokeFunction(functionName, args);
+ } else {
+ result = ((Invocable)engine).invokeFunction(functionName, args);
+ }
+ return Futures.immediateFuture(result);
+ } catch (Exception e) {
+ blackListedFunctions.computeIfAbsent(scriptId, key -> new AtomicInteger(0)).incrementAndGet();
+ return Futures.immediateFailedFuture(e);
+ }
+ } else {
+ return Futures.immediateFailedFuture(
+ new RuntimeException("Script is blacklisted due to maximum error count " + getMaxErrors() + "!"));
+ }
+ }
+
+ @Override
+ public ListenableFuture<Void> release(UUID scriptId) {
+ String functionName = functionsMap.get(scriptId);
+ if (functionName != null) {
+ try {
+ if (useJsSandbox()) {
+ sandbox.eval(functionName + " = undefined;");
+ } else {
+ engine.eval(functionName + " = undefined;");
+ }
+ functionsMap.remove(scriptId);
+ blackListedFunctions.remove(scriptId);
+ } catch (ScriptException e) {
+ return Futures.immediateFailedFuture(e);
+ }
+ }
+ return Futures.immediateFuture(null);
+ }
+
+ private boolean isBlackListed(UUID scriptId) {
+ if (blackListedFunctions.containsKey(scriptId)) {
+ AtomicInteger errorCount = blackListedFunctions.get(scriptId);
+ return errorCount.get() >= getMaxErrors();
+ } else {
+ return false;
+ }
+ }
+
+ private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) {
+ switch (scriptType) {
+ case RULE_NODE_SCRIPT:
+ return RuleNodeScriptFactory.generateRuleNodeScript(functionName, scriptBody, argNames);
+ default:
+ throw new RuntimeException("No script factory implemented for scriptType: " + scriptType);
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/script/NashornJsSandboxService.java b/application/src/main/java/org/thingsboard/server/service/script/NashornJsSandboxService.java
new file mode 100644
index 0000000..3e8b4e9
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/script/NashornJsSandboxService.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.thingsboard.server.service.script;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+public class NashornJsSandboxService extends AbstractNashornJsSandboxService {
+
+ @Value("${actors.rule.js_sandbox.use_js_sandbox}")
+ private boolean useJsSandbox;
+
+ @Value("${actors.rule.js_sandbox.monitor_thread_pool_size}")
+ private int monitorThreadPoolSize;
+
+ @Value("${actors.rule.js_sandbox.max_cpu_time}")
+ private long maxCpuTime;
+
+ @Value("${actors.rule.js_sandbox.max_errors}")
+ private int maxErrors;
+
+ @Override
+ protected boolean useJsSandbox() {
+ return useJsSandbox;
+ }
+
+ @Override
+ protected int getMonitorThreadPoolSize() {
+ return monitorThreadPoolSize;
+ }
+
+ @Override
+ protected long getMaxCpuTime() {
+ return maxCpuTime;
+ }
+
+ @Override
+ protected int getMaxErrors() {
+ return maxErrors;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java
new file mode 100644
index 0000000..98b0e77
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java
@@ -0,0 +1,181 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.script;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Sets;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+
+
+@Slf4j
+public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.ScriptEngine {
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+ private final JsSandboxService sandboxService;
+
+ private final UUID scriptId;
+
+ public RuleNodeJsScriptEngine(JsSandboxService sandboxService, String script, String... argNames) {
+ this.sandboxService = sandboxService;
+ try {
+ this.scriptId = this.sandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, script, argNames).get();
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Can't compile script: " + e.getMessage());
+ }
+ }
+
+ private static String[] prepareArgs(TbMsg msg) {
+ try {
+ String[] args = new String[3];
+ if (msg.getData() != null) {
+ args[0] = msg.getData();
+ } else {
+ args[0] = "";
+ }
+ args[1] = mapper.writeValueAsString(msg.getMetaData().getData());
+ args[2] = msg.getType();
+ return args;
+ } catch (Throwable th) {
+ throw new IllegalArgumentException("Cannot bind js args", th);
+ }
+ }
+
+ private static TbMsg unbindMsg(JsonNode msgData, TbMsg msg) {
+ try {
+ String data = null;
+ Map<String, String> metadata = null;
+ String messageType = null;
+ if (msgData.has(RuleNodeScriptFactory.MSG)) {
+ JsonNode msgPayload = msgData.get(RuleNodeScriptFactory.MSG);
+ data = mapper.writeValueAsString(msgPayload);
+ }
+ if (msgData.has(RuleNodeScriptFactory.METADATA)) {
+ JsonNode msgMetadata = msgData.get(RuleNodeScriptFactory.METADATA);
+ metadata = mapper.convertValue(msgMetadata, new TypeReference<Map<String, String>>() {
+ });
+ }
+ if (msgData.has(RuleNodeScriptFactory.MSG_TYPE)) {
+ messageType = msgData.get(RuleNodeScriptFactory.MSG_TYPE).asText();
+ }
+ String newData = data != null ? data : msg.getData();
+ TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData().copy();
+ String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType();
+ return new TbMsg(msg.getId(), newMessageType, msg.getOriginator(), newMetadata, newData, msg.getRuleChainId(), msg.getRuleNodeId(), msg.getClusterPartition());
+ } catch (Throwable th) {
+ th.printStackTrace();
+ throw new RuntimeException("Failed to unbind message data from javascript result", th);
+ }
+ }
+
+ @Override
+ public TbMsg executeUpdate(TbMsg msg) throws ScriptException {
+ JsonNode result = executeScript(msg);
+ if (!result.isObject()) {
+ log.warn("Wrong result type: {}", result.getNodeType());
+ throw new ScriptException("Wrong result type: " + result.getNodeType());
+ }
+ return unbindMsg(result, msg);
+ }
+
+ @Override
+ public TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException {
+ JsonNode result = executeScript(prevMsg);
+ if (!result.isObject()) {
+ log.warn("Wrong result type: {}", result.getNodeType());
+ throw new ScriptException("Wrong result type: " + result.getNodeType());
+ }
+ return unbindMsg(result, prevMsg);
+ }
+
+ @Override
+ public JsonNode executeJson(TbMsg msg) throws ScriptException {
+ return executeScript(msg);
+ }
+
+ @Override
+ public String executeToString(TbMsg msg) throws ScriptException {
+ JsonNode result = executeScript(msg);
+ if (!result.isTextual()) {
+ log.warn("Wrong result type: {}", result.getNodeType());
+ throw new ScriptException("Wrong result type: " + result.getNodeType());
+ }
+ return result.asText();
+ }
+
+ @Override
+ public boolean executeFilter(TbMsg msg) throws ScriptException {
+ JsonNode result = executeScript(msg);
+ if (!result.isBoolean()) {
+ log.warn("Wrong result type: {}", result.getNodeType());
+ throw new ScriptException("Wrong result type: " + result.getNodeType());
+ }
+ return result.asBoolean();
+ }
+
+ @Override
+ public Set<String> executeSwitch(TbMsg msg) throws ScriptException {
+ JsonNode result = executeScript(msg);
+ if (result.isTextual()) {
+ return Collections.singleton(result.asText());
+ } else if (result.isArray()) {
+ Set<String> nextStates = Sets.newHashSet();
+ for (JsonNode val : result) {
+ if (!val.isTextual()) {
+ log.warn("Wrong result type: {}", val.getNodeType());
+ throw new ScriptException("Wrong result type: " + val.getNodeType());
+ } else {
+ nextStates.add(val.asText());
+ }
+ }
+ return nextStates;
+ } else {
+ log.warn("Wrong result type: {}", result.getNodeType());
+ throw new ScriptException("Wrong result type: " + result.getNodeType());
+ }
+ }
+
+ private JsonNode executeScript(TbMsg msg) throws ScriptException {
+ try {
+ String[] inArgs = prepareArgs(msg);
+ String eval = sandboxService.invokeFunction(this.scriptId, inArgs[0], inArgs[1], inArgs[2]).get().toString();
+ return mapper.readTree(eval);
+ } catch (ExecutionException e) {
+ if (e.getCause() instanceof ScriptException) {
+ throw (ScriptException)e.getCause();
+ } else {
+ throw new ScriptException("Failed to execute js script: " + e.getMessage());
+ }
+ } catch (Exception e) {
+ throw new ScriptException("Failed to execute js script: " + e.getMessage());
+ }
+ }
+
+ public void destroy() {
+ sandboxService.release(this.scriptId);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptFactory.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptFactory.java
new file mode 100644
index 0000000..5cc9c55
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptFactory.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.thingsboard.server.service.script;
+
+public class RuleNodeScriptFactory {
+
+ public static final String MSG = "msg";
+ public static final String METADATA = "metadata";
+ public static final String MSG_TYPE = "msgType";
+ public static final String RULE_NODE_FUNCTION_NAME = "ruleNodeFunc";
+
+ private static final String JS_WRAPPER_PREFIX_TEMPLATE = "function %s(msgStr, metadataStr, msgType) { " +
+ " var msg = JSON.parse(msgStr); " +
+ " var metadata = JSON.parse(metadataStr); " +
+ " return JSON.stringify(%s(msg, metadata, msgType));" +
+ " function %s(%s, %s, %s) {";
+ private static final String JS_WRAPPER_SUFFIX = "}" +
+ "\n}";
+
+
+ public static String generateRuleNodeScript(String functionName, String scriptBody, String... argNames) {
+ String msgArg;
+ String metadataArg;
+ String msgTypeArg;
+ if (argNames != null && argNames.length == 3) {
+ msgArg = argNames[0];
+ metadataArg = argNames[1];
+ msgTypeArg = argNames[2];
+ } else {
+ msgArg = MSG;
+ metadataArg = METADATA;
+ msgTypeArg = MSG_TYPE;
+ }
+ String jsWrapperPrefix = String.format(JS_WRAPPER_PREFIX_TEMPLATE, functionName,
+ RULE_NODE_FUNCTION_NAME, RULE_NODE_FUNCTION_NAME, msgArg, metadataArg, msgTypeArg);
+ return jsWrapperPrefix + scriptBody + JS_WRAPPER_SUFFIX;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
new file mode 100644
index 0000000..600820e
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
@@ -0,0 +1,321 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security;
+
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.controller.HttpValidationCallback;
+import org.thingsboard.server.dao.alarm.AlarmService;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.dao.user.UserService;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.telemetry.exception.ToErrorResponseEntity;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.BiConsumer;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+@Component
+public class AccessValidator {
+
+ public static final String CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "Customer user is not allowed to perform this operation!";
+ public static final String SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "System administrator is not allowed to perform this operation!";
+ public static final String DEVICE_WITH_REQUESTED_ID_NOT_FOUND = "Device with requested id wasn't found!";
+
+ @Autowired
+ protected TenantService tenantService;
+
+ @Autowired
+ protected CustomerService customerService;
+
+ @Autowired
+ protected UserService userService;
+
+ @Autowired
+ protected DeviceService deviceService;
+
+ @Autowired
+ protected AssetService assetService;
+
+ @Autowired
+ protected AlarmService alarmService;
+
+ @Autowired
+ protected RuleChainService ruleChainService;
+
+ private ExecutorService executor;
+
+ @PostConstruct
+ public void initExecutor() {
+ executor = Executors.newSingleThreadExecutor();
+ }
+
+ @PreDestroy
+ public void shutdownExecutor() {
+ if (executor != null) {
+ executor.shutdownNow();
+ }
+ }
+
+ public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, String entityType, String entityIdStr,
+ BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess) throws ThingsboardException {
+ return validateEntityAndCallback(currentUser, entityType, entityIdStr, onSuccess, (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+
+ public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, String entityType, String entityIdStr,
+ BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess,
+ BiConsumer<DeferredResult<ResponseEntity>, Throwable> onFailure) throws ThingsboardException {
+ return validateEntityAndCallback(currentUser, EntityIdFactory.getByTypeAndId(entityType, entityIdStr),
+ onSuccess, onFailure);
+ }
+
+ public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, EntityId entityId,
+ BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess) throws ThingsboardException {
+ return validateEntityAndCallback(currentUser, entityId, onSuccess, (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+
+ public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, EntityId entityId,
+ BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess,
+ BiConsumer<DeferredResult<ResponseEntity>, Throwable> onFailure) throws ThingsboardException {
+
+ final DeferredResult<ResponseEntity> response = new DeferredResult<>();
+
+ validate(currentUser, entityId, new HttpValidationCallback(response,
+ new FutureCallback<DeferredResult<ResponseEntity>>() {
+ @Override
+ public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) {
+ onSuccess.accept(response, entityId);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ onFailure.accept(response, t);
+ }
+ }));
+
+ return response;
+ }
+
+ public void validate(SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+ switch (entityId.getEntityType()) {
+ case DEVICE:
+ validateDevice(currentUser, entityId, callback);
+ return;
+ case ASSET:
+ validateAsset(currentUser, entityId, callback);
+ return;
+ case RULE_CHAIN:
+ validateRuleChain(currentUser, entityId, callback);
+ return;
+ case CUSTOMER:
+ validateCustomer(currentUser, entityId, callback);
+ return;
+ case TENANT:
+ validateTenant(currentUser, entityId, callback);
+ return;
+ default:
+ //TODO: add support of other entities
+ throw new IllegalStateException("Not Implemented!");
+ }
+ }
+
+ private void validateDevice(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+ if (currentUser.isSystemAdmin()) {
+ callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+ } else {
+ ListenableFuture<Device> deviceFuture = deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId()));
+ Futures.addCallback(deviceFuture, getCallback(callback, device -> {
+ if (device == null) {
+ return ValidationResult.entityNotFound(DEVICE_WITH_REQUESTED_ID_NOT_FOUND);
+ } else {
+ if (!device.getTenantId().equals(currentUser.getTenantId())) {
+ return ValidationResult.accessDenied("Device doesn't belong to the current Tenant!");
+ } else if (currentUser.isCustomerUser() && !device.getCustomerId().equals(currentUser.getCustomerId())) {
+ return ValidationResult.accessDenied("Device doesn't belong to the current Customer!");
+ } else {
+ return ValidationResult.ok(device);
+ }
+ }
+ }), executor);
+ }
+ }
+
+ private void validateAsset(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+ if (currentUser.isSystemAdmin()) {
+ callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+ } else {
+ ListenableFuture<Asset> assetFuture = assetService.findAssetByIdAsync(new AssetId(entityId.getId()));
+ Futures.addCallback(assetFuture, getCallback(callback, asset -> {
+ if (asset == null) {
+ return ValidationResult.entityNotFound("Asset with requested id wasn't found!");
+ } else {
+ if (!asset.getTenantId().equals(currentUser.getTenantId())) {
+ return ValidationResult.accessDenied("Asset doesn't belong to the current Tenant!");
+ } else if (currentUser.isCustomerUser() && !asset.getCustomerId().equals(currentUser.getCustomerId())) {
+ return ValidationResult.accessDenied("Asset doesn't belong to the current Customer!");
+ } else {
+ return ValidationResult.ok(asset);
+ }
+ }
+ }), executor);
+ }
+ }
+
+ private void validateRuleChain(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+ if (currentUser.isCustomerUser()) {
+ callback.onSuccess(ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+ } else {
+ ListenableFuture<RuleChain> ruleChainFuture = ruleChainService.findRuleChainByIdAsync(new RuleChainId(entityId.getId()));
+ Futures.addCallback(ruleChainFuture, getCallback(callback, ruleChain -> {
+ if (ruleChain == null) {
+ return ValidationResult.entityNotFound("Rule chain with requested id wasn't found!");
+ } else {
+ if (currentUser.isTenantAdmin() && !ruleChain.getTenantId().equals(currentUser.getTenantId())) {
+ return ValidationResult.accessDenied("Rule chain doesn't belong to the current Tenant!");
+ } else if (currentUser.isSystemAdmin() && !ruleChain.getTenantId().isNullUid()) {
+ return ValidationResult.accessDenied("Rule chain is not in system scope!");
+ } else {
+ return ValidationResult.ok(ruleChain);
+ }
+ }
+ }), executor);
+ }
+ }
+
+ private void validateRule(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+ if (currentUser.isCustomerUser()) {
+ callback.onSuccess(ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+ } else {
+ ListenableFuture<RuleNode> ruleNodeFuture = ruleChainService.findRuleNodeByIdAsync(new RuleNodeId(entityId.getId()));
+ Futures.addCallback(ruleNodeFuture, getCallback(callback, ruleNodeTmp -> {
+ RuleNode ruleNode = ruleNodeTmp;
+ if (ruleNode == null) {
+ return ValidationResult.entityNotFound("Rule node with requested id wasn't found!");
+ } else if (ruleNode.getRuleChainId() == null) {
+ return ValidationResult.entityNotFound("Rule chain with requested node id wasn't found!");
+ } else {
+ //TODO: make async
+ RuleChain ruleChain = ruleChainService.findRuleChainById(ruleNode.getRuleChainId());
+ if (currentUser.isTenantAdmin() && !ruleChain.getTenantId().equals(currentUser.getTenantId())) {
+ return ValidationResult.accessDenied("Rule chain doesn't belong to the current Tenant!");
+ } else if (currentUser.isSystemAdmin() && !ruleChain.getTenantId().isNullUid()) {
+ return ValidationResult.accessDenied("Rule chain is not in system scope!");
+ } else {
+ return ValidationResult.ok(ruleNode);
+ }
+ }
+ }), executor);
+ }
+ }
+
+ private void validateCustomer(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+ if (currentUser.isSystemAdmin()) {
+ callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+ } else {
+ ListenableFuture<Customer> customerFuture = customerService.findCustomerByIdAsync(new CustomerId(entityId.getId()));
+ Futures.addCallback(customerFuture, getCallback(callback, customer -> {
+ if (customer == null) {
+ return ValidationResult.entityNotFound("Customer with requested id wasn't found!");
+ } else {
+ if (!customer.getTenantId().equals(currentUser.getTenantId())) {
+ return ValidationResult.accessDenied("Customer doesn't belong to the current Tenant!");
+ } else if (currentUser.isCustomerUser() && !customer.getId().equals(currentUser.getCustomerId())) {
+ return ValidationResult.accessDenied("Customer doesn't relate to the currently authorized customer user!");
+ } else {
+ return ValidationResult.ok(customer);
+ }
+ }
+ }), executor);
+ }
+ }
+
+ private void validateTenant(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+ if (currentUser.isCustomerUser()) {
+ callback.onSuccess(ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+ } else if (currentUser.isSystemAdmin()) {
+ callback.onSuccess(ValidationResult.ok(null));
+ } else {
+ ListenableFuture<Tenant> tenantFuture = tenantService.findTenantByIdAsync(new TenantId(entityId.getId()));
+ Futures.addCallback(tenantFuture, getCallback(callback, tenant -> {
+ if (tenant == null) {
+ return ValidationResult.entityNotFound("Tenant with requested id wasn't found!");
+ } else if (!tenant.getId().equals(currentUser.getTenantId())) {
+ return ValidationResult.accessDenied("Tenant doesn't relate to the currently authorized user!");
+ } else {
+ return ValidationResult.ok(tenant);
+ }
+ }), executor);
+ }
+ }
+
+ private <T, V> FutureCallback<T> getCallback(FutureCallback<ValidationResult> callback, Function<T, ValidationResult<V>> transformer) {
+ return new FutureCallback<T>() {
+ @Override
+ public void onSuccess(@Nullable T result) {
+ callback.onSuccess(transformer.apply(result));
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ callback.onFailure(t);
+ }
+ };
+ }
+
+ public static void handleError(Throwable e, final DeferredResult<ResponseEntity> response, HttpStatus defaultErrorStatus) {
+ ResponseEntity responseEntity;
+ if (e != null && e instanceof ToErrorResponseEntity) {
+ responseEntity = ((ToErrorResponseEntity) e).toErrorResponseEntity();
+ } else if (e != null && e instanceof IllegalArgumentException) {
+ responseEntity = new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
+ } else {
+ responseEntity = new ResponseEntity<>(defaultErrorStatus);
+ }
+ response.setResult(responseEntity);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java
index a46fb48..527e2be 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java
@@ -15,24 +15,16 @@
*/
package org.thingsboard.server.service.security.auth.jwt;
-import io.jsonwebtoken.Claims;
-import io.jsonwebtoken.Jws;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
-import org.springframework.security.core.GrantedAuthority;
-import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
-import org.thingsboard.server.config.JwtSettings;
import org.thingsboard.server.service.security.auth.JwtAuthenticationToken;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
-import java.util.List;
-import java.util.stream.Collectors;
-
@Component
@SuppressWarnings("unchecked")
public class JwtAuthenticationProvider implements AuthenticationProvider {
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java
index f4da3a5..7008662 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java
@@ -23,7 +23,6 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.util.matcher.RequestMatcher;
-import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
import org.thingsboard.server.service.security.auth.JwtAuthenticationToken;
import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor;
import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
index 08de9ef..2e68b35 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
@@ -16,7 +16,10 @@
package org.thingsboard.server.service.security.auth.jwt;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.authentication.*;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.DisabledException;
+import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -26,7 +29,6 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java
index a016f3a..fff15f2 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java
@@ -18,8 +18,6 @@ package org.thingsboard.server.service.security.auth.jwt;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
index 36f2199..195208a 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
@@ -16,7 +16,11 @@
package org.thingsboard.server.service.security.auth.rest;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.authentication.*;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.DisabledException;
+import org.springframework.security.authentication.InsufficientAuthenticationException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -27,7 +31,6 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java
index 4f6a87c..03c1b34 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java
@@ -18,8 +18,6 @@ package org.thingsboard.server.service.security.auth.rest;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java
index 5f8488e..2bbeddf 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java
@@ -18,8 +18,6 @@ package org.thingsboard.server.service.security.auth.rest;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java
index db6a336..544b6fd 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java
@@ -15,7 +15,13 @@
*/
package org.thingsboard.server.service.security.model.token;
-import io.jsonwebtoken.*;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.MalformedJwtException;
+import io.jsonwebtoken.SignatureException;
+import io.jsonwebtoken.UnsupportedJwtException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.BadCredentialsException;
diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
new file mode 100644
index 0000000..45b9696
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
@@ -0,0 +1,386 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.state;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Function;
+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.ListeningScheduledExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgDataType;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import static org.thingsboard.server.common.data.DataConstants.ACTIVITY_EVENT;
+import static org.thingsboard.server.common.data.DataConstants.CONNECT_EVENT;
+import static org.thingsboard.server.common.data.DataConstants.DISCONNECT_EVENT;
+import static org.thingsboard.server.common.data.DataConstants.INACTIVITY_EVENT;
+
+/**
+ * Created by ashvayka on 01.05.18.
+ */
+@Service
+@Slf4j
+//TODO: refactor to use page links as cursor and not fetch all
+public class DefaultDeviceStateService implements DeviceStateService {
+
+ private static final ObjectMapper json = new ObjectMapper();
+ public static final String ACTIVITY_STATE = "active";
+ public static final String LAST_CONNECT_TIME = "lastConnectTime";
+ public static final String LAST_DISCONNECT_TIME = "lastDisconnectTime";
+ public static final String LAST_ACTIVITY_TIME = "lastActivityTime";
+ public static final String INACTIVITY_ALARM_TIME = "inactivityAlarmTime";
+ public static final String INACTIVITY_TIMEOUT = "inactivityTimeout";
+
+ public static final List<String> PERSISTENT_ATTRIBUTES = Arrays.asList(ACTIVITY_STATE, LAST_CONNECT_TIME, LAST_DISCONNECT_TIME, LAST_ACTIVITY_TIME, INACTIVITY_ALARM_TIME, INACTIVITY_TIMEOUT);
+
+ @Autowired
+ private TenantService tenantService;
+
+ @Autowired
+ private DeviceService deviceService;
+
+ @Autowired
+ private AttributesService attributesService;
+
+ @Autowired
+ private ActorService actorService;
+
+ @Autowired
+ private TelemetrySubscriptionService tsSubService;
+
+ @Value("${state.defaultInactivityTimeoutInSec}")
+ @Getter
+ private long defaultInactivityTimeoutInSec;
+
+ @Value("${state.defaultStateCheckIntervalInSec}")
+ @Getter
+ private long defaultStateCheckIntervalInSec;
+
+// TODO in v2.1
+// @Value("${state.defaultStatePersistenceIntervalInSec}")
+// @Getter
+// private long defaultStatePersistenceIntervalInSec;
+//
+// @Value("${state.defaultStatePersistencePack}")
+// @Getter
+// private long defaultStatePersistencePack;
+
+ private ListeningScheduledExecutorService queueExecutor;
+
+ private ConcurrentMap<TenantId, Set<DeviceId>> tenantDevices = new ConcurrentHashMap<>();
+ private ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>();
+
+ @PostConstruct
+ public void init() {
+ // Should be always single threaded due to absence of locks.
+ queueExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadScheduledExecutor());
+ queueExecutor.submit(this::initStateFromDB);
+ queueExecutor.scheduleAtFixedRate(this::updateState, defaultStateCheckIntervalInSec, defaultStateCheckIntervalInSec, TimeUnit.SECONDS);
+ //TODO: schedule persistence in v2.1;
+ }
+
+ @PreDestroy
+ public void stop() {
+ if (queueExecutor != null) {
+ queueExecutor.shutdownNow();
+ }
+ }
+
+ @Override
+ public void onDeviceAdded(Device device) {
+ queueExecutor.submit(() -> onDeviceAddedSync(device));
+ }
+
+ @Override
+ public void onDeviceUpdated(Device device) {
+ queueExecutor.submit(() -> onDeviceUpdatedSync(device));
+ }
+
+ @Override
+ public void onDeviceConnect(DeviceId deviceId) {
+ queueExecutor.submit(() -> onDeviceConnectSync(deviceId));
+ }
+
+ @Override
+ public void onDeviceActivity(DeviceId deviceId) {
+ queueExecutor.submit(() -> onDeviceActivitySync(deviceId));
+ }
+
+ @Override
+ public void onDeviceDisconnect(DeviceId deviceId) {
+ queueExecutor.submit(() -> onDeviceDisconnectSync(deviceId));
+ }
+
+ @Override
+ public void onDeviceDeleted(Device device) {
+ queueExecutor.submit(() -> onDeviceDeleted(device.getTenantId(), device.getId()));
+ }
+
+ @Override
+ public void onDeviceInactivityTimeoutUpdate(DeviceId deviceId, long inactivityTimeout) {
+ queueExecutor.submit(() -> onInactivityTimeoutUpdate(deviceId, inactivityTimeout));
+ }
+
+ @Override
+ public Optional<DeviceState> getDeviceState(DeviceId deviceId) {
+ DeviceStateData state = deviceStates.get(deviceId);
+ if (state != null) {
+ return Optional.of(state.getState());
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ private void initStateFromDB() {
+ List<Tenant> tenants = tenantService.findTenants(new TextPageLink(Integer.MAX_VALUE)).getData();
+ for (Tenant tenant : tenants) {
+ List<ListenableFuture<DeviceStateData>> fetchFutures = new ArrayList<>();
+ List<Device> devices = deviceService.findDevicesByTenantId(tenant.getId(), new TextPageLink(Integer.MAX_VALUE)).getData();
+ for (Device device : devices) {
+ fetchFutures.add(fetchDeviceState(device));
+ }
+ try {
+ Futures.successfulAsList(fetchFutures).get().forEach(this::addDeviceUsingState);
+ } catch (InterruptedException | ExecutionException e) {
+ log.warn("Failed to init device state service from DB", e);
+ }
+ }
+ }
+
+ private void addDeviceUsingState(DeviceStateData state) {
+ tenantDevices.computeIfAbsent(state.getTenantId(), id -> ConcurrentHashMap.newKeySet()).add(state.getDeviceId());
+ deviceStates.put(state.getDeviceId(), state);
+ }
+
+ private void updateState() {
+ long ts = System.currentTimeMillis();
+ Set<DeviceId> deviceIds = new HashSet<>(deviceStates.keySet());
+ for (DeviceId deviceId : deviceIds) {
+ DeviceStateData stateData = deviceStates.get(deviceId);
+ DeviceState state = stateData.getState();
+ state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout());
+ if (!state.isActive() && state.getLastInactivityAlarmTime() < state.getLastActivityTime()) {
+ state.setLastInactivityAlarmTime(ts);
+ pushRuleEngineMessage(stateData, INACTIVITY_EVENT);
+ saveAttribute(deviceId, INACTIVITY_ALARM_TIME, ts);
+ saveAttribute(deviceId, ACTIVITY_STATE, state.isActive());
+ }
+ }
+ }
+
+ private void onDeviceConnectSync(DeviceId deviceId) {
+ DeviceStateData stateData = deviceStates.get(deviceId);
+ if (stateData != null) {
+ long ts = System.currentTimeMillis();
+ stateData.getState().setLastConnectTime(ts);
+ pushRuleEngineMessage(stateData, CONNECT_EVENT);
+ saveAttribute(deviceId, LAST_CONNECT_TIME, ts);
+ }
+ }
+
+ private void onDeviceDisconnectSync(DeviceId deviceId) {
+ DeviceStateData stateData = deviceStates.get(deviceId);
+ if (stateData != null) {
+ long ts = System.currentTimeMillis();
+ stateData.getState().setLastDisconnectTime(ts);
+ pushRuleEngineMessage(stateData, DISCONNECT_EVENT);
+ saveAttribute(deviceId, LAST_DISCONNECT_TIME, ts);
+ }
+ }
+
+ private void onDeviceActivitySync(DeviceId deviceId) {
+ DeviceStateData stateData = deviceStates.get(deviceId);
+ if (stateData != null) {
+ DeviceState state = stateData.getState();
+ long ts = System.currentTimeMillis();
+ state.setActive(true);
+ stateData.getState().setLastActivityTime(ts);
+ pushRuleEngineMessage(stateData, ACTIVITY_EVENT);
+ saveAttribute(deviceId, LAST_ACTIVITY_TIME, ts);
+ saveAttribute(deviceId, ACTIVITY_STATE, state.isActive());
+ }
+ }
+
+ private void onInactivityTimeoutUpdate(DeviceId deviceId, long inactivityTimeout) {
+ if (inactivityTimeout == 0L) {
+ return;
+ }
+ DeviceStateData stateData = deviceStates.get(deviceId);
+ if (stateData != null) {
+ long ts = System.currentTimeMillis();
+ DeviceState state = stateData.getState();
+ state.setInactivityTimeout(inactivityTimeout);
+ boolean oldActive = state.isActive();
+ state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout());
+ if (!oldActive && state.isActive() || oldActive && !state.isActive()) {
+ saveAttribute(deviceId, ACTIVITY_STATE, state.isActive());
+ }
+ }
+ }
+
+ private void onDeviceAddedSync(Device device) {
+ Futures.addCallback(fetchDeviceState(device), new FutureCallback<DeviceStateData>() {
+ @Override
+ public void onSuccess(@Nullable DeviceStateData state) {
+ addDeviceUsingState(state);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ log.warn("Failed to register device to the state service", t);
+ }
+ });
+ }
+
+ private void onDeviceUpdatedSync(Device device) {
+ DeviceStateData stateData = deviceStates.get(device.getId());
+ if (stateData != null) {
+ TbMsgMetaData md = new TbMsgMetaData();
+ md.putValue("deviceName", device.getName());
+ md.putValue("deviceType", device.getType());
+ stateData.setMetaData(md);
+ }
+ }
+
+ private void onDeviceDeleted(TenantId tenantId, DeviceId deviceId) {
+ deviceStates.remove(deviceId);
+ Set<DeviceId> deviceIds = tenantDevices.get(tenantId);
+ if (deviceIds != null) {
+ deviceIds.remove(deviceId);
+ if (deviceIds.isEmpty()) {
+ tenantDevices.remove(tenantId);
+ }
+ }
+ }
+
+ private ListenableFuture<DeviceStateData> fetchDeviceState(Device device) {
+ ListenableFuture<List<AttributeKvEntry>> attributes = attributesService.find(device.getId(), DataConstants.SERVER_SCOPE, PERSISTENT_ATTRIBUTES);
+ return Futures.transform(attributes, new Function<List<AttributeKvEntry>, DeviceStateData>() {
+ @Nullable
+ @Override
+ public DeviceStateData apply(@Nullable List<AttributeKvEntry> attributes) {
+ long lastActivityTime = getAttributeValue(attributes, LAST_ACTIVITY_TIME, 0L);
+ long inactivityAlarmTime = getAttributeValue(attributes, INACTIVITY_ALARM_TIME, 0L);
+ long inactivityTimeout = getAttributeValue(attributes, INACTIVITY_TIMEOUT, TimeUnit.SECONDS.toMillis(defaultInactivityTimeoutInSec));
+ boolean active = System.currentTimeMillis() < lastActivityTime + inactivityTimeout;
+ DeviceState deviceState = DeviceState.builder()
+ .active(active)
+ .lastConnectTime(getAttributeValue(attributes, LAST_CONNECT_TIME, 0L))
+ .lastDisconnectTime(getAttributeValue(attributes, LAST_DISCONNECT_TIME, 0L))
+ .lastActivityTime(lastActivityTime)
+ .lastInactivityAlarmTime(inactivityAlarmTime)
+ .inactivityTimeout(inactivityTimeout)
+ .build();
+ TbMsgMetaData md = new TbMsgMetaData();
+ md.putValue("deviceName", device.getName());
+ md.putValue("deviceType", device.getType());
+ return DeviceStateData.builder()
+ .tenantId(device.getTenantId())
+ .deviceId(device.getId())
+ .metaData(md)
+ .state(deviceState).build();
+ }
+ });
+ }
+
+ private long getAttributeValue(List<AttributeKvEntry> attributes, String attributeName, long defaultValue) {
+ for (AttributeKvEntry attribute : attributes) {
+ if (attribute.getKey().equals(attributeName)) {
+ return attribute.getLongValue().orElse(defaultValue);
+ }
+ }
+ return defaultValue;
+ }
+
+ private void pushRuleEngineMessage(DeviceStateData stateData, String msgType) {
+ DeviceState state = stateData.getState();
+ try {
+ TbMsg tbMsg = new TbMsg(UUIDs.timeBased(), msgType, stateData.getDeviceId(), stateData.getMetaData().copy(), TbMsgDataType.JSON
+ , json.writeValueAsString(state)
+ , null, null, 0L);
+ actorService.onMsg(new ServiceToRuleEngineMsg(stateData.getTenantId(), tbMsg));
+ } catch (Exception e) {
+ log.warn("[{}] Failed to push inactivity alarm: {}", stateData.getDeviceId(), state, e);
+ }
+ }
+
+ private void saveAttribute(DeviceId deviceId, String key, long value) {
+ tsSubService.saveAttrAndNotify(deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value));
+ }
+
+ private void saveAttribute(DeviceId deviceId, String key, boolean value) {
+ tsSubService.saveAttrAndNotify(deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value));
+ }
+
+ private class AttributeSaveCallback implements FutureCallback<Void> {
+ private final DeviceId deviceId;
+ private final String key;
+ private final Object value;
+
+ AttributeSaveCallback(DeviceId deviceId, String key, Object value) {
+ this.deviceId = deviceId;
+ this.key = key;
+ this.value = value;
+ }
+
+ @Override
+ public void onSuccess(@Nullable Void result) {
+ log.trace("[{}] Successfully updated attribute [{}] with value [{}]", deviceId, key, value);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ log.warn("[{}] Failed to update attribute [{}] with value [{}]", deviceId, key, value, t);
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/GetHistoryCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/GetHistoryCmd.java
new file mode 100644
index 0000000..9fc28f6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/GetHistoryCmd.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry.cmd;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author Andrew Shvayka
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+public class GetHistoryCmd implements TelemetryPluginCmd {
+
+ private int cmdId;
+ private String entityType;
+ private String entityId;
+ private String keys;
+ private long startTs;
+ private long endTs;
+ private long interval;
+ private int limit;
+ private String agg;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/SubscriptionCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/SubscriptionCmd.java
new file mode 100644
index 0000000..15701ca
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/SubscriptionCmd.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry.cmd;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.service.telemetry.TelemetryFeature;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+public abstract class SubscriptionCmd implements TelemetryPluginCmd {
+
+ private int cmdId;
+ private String entityType;
+ private String entityId;
+ private String keys;
+ private String scope;
+ private boolean unsubscribe;
+
+ public abstract TelemetryFeature getType();
+
+ @Override
+ public String toString() {
+ return "SubscriptionCmd [entityType=" + entityType + ", entityId=" + entityId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]";
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java
new file mode 100644
index 0000000..2f5c037
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry.cmd;
+
+import java.util.List;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class TelemetryPluginCmdsWrapper {
+
+ private List<AttributesSubscriptionCmd> attrSubCmds;
+
+ private List<TimeseriesSubscriptionCmd> tsSubCmds;
+
+ private List<GetHistoryCmd> historyCmds;
+
+ public TelemetryPluginCmdsWrapper() {
+ super();
+ }
+
+ public List<AttributesSubscriptionCmd> getAttrSubCmds() {
+ return attrSubCmds;
+ }
+
+ public void setAttrSubCmds(List<AttributesSubscriptionCmd> attrSubCmds) {
+ this.attrSubCmds = attrSubCmds;
+ }
+
+ public List<TimeseriesSubscriptionCmd> getTsSubCmds() {
+ return tsSubCmds;
+ }
+
+ public void setTsSubCmds(List<TimeseriesSubscriptionCmd> tsSubCmds) {
+ this.tsSubCmds = tsSubCmds;
+ }
+
+ public List<GetHistoryCmd> getHistoryCmds() {
+ return historyCmds;
+ }
+
+ public void setHistoryCmds(List<GetHistoryCmd> historyCmds) {
+ this.historyCmds = historyCmds;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
new file mode 100644
index 0000000..0942b40
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
@@ -0,0 +1,710 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.protobuf.InvalidProtocolBufferException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.api.util.DonAsynchron;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+import org.thingsboard.server.common.data.kv.DataType;
+import org.thingsboard.server.common.data.kv.DoubleDataEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
+import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
+import org.thingsboard.server.service.state.DefaultDeviceStateService;
+import org.thingsboard.server.service.state.DeviceStateService;
+import org.thingsboard.server.service.telemetry.sub.Subscription;
+import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode;
+import org.thingsboard.server.service.telemetry.sub.SubscriptionState;
+import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+@Service
+@Slf4j
+public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptionService {
+
+ @Autowired
+ private TelemetryWebSocketService wsService;
+
+ @Autowired
+ private AttributesService attrService;
+
+ @Autowired
+ private TimeseriesService tsService;
+
+ @Autowired
+ private ClusterRoutingService routingService;
+
+ @Autowired
+ private ClusterRpcService rpcService;
+
+ @Autowired
+ @Lazy
+ private DeviceStateService stateService;
+
+ private ExecutorService tsCallBackExecutor;
+ private ExecutorService wsCallBackExecutor;
+
+ @PostConstruct
+ public void initExecutor() {
+ tsCallBackExecutor = Executors.newSingleThreadExecutor();
+ wsCallBackExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ @PreDestroy
+ public void shutdownExecutor() {
+ if (tsCallBackExecutor != null) {
+ tsCallBackExecutor.shutdownNow();
+ }
+ if (wsCallBackExecutor != null) {
+ wsCallBackExecutor.shutdownNow();
+ }
+ }
+
+ private final Map<EntityId, Set<Subscription>> subscriptionsByEntityId = new HashMap<>();
+ private final Map<String, Map<Integer, Subscription>> subscriptionsByWsSessionId = new HashMap<>();
+
+ @Override
+ public void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub) {
+ Optional<ServerAddress> server = routingService.resolveById(entityId);
+ Subscription subscription;
+ if (server.isPresent()) {
+ ServerAddress address = server.get();
+ log.trace("[{}] Forwarding subscription [{}] for device [{}] to [{}]", sessionId, sub.getSubscriptionId(), entityId, address);
+ subscription = new Subscription(sub, true, address);
+ tellNewSubscription(address, sessionId, subscription);
+ } else {
+ log.trace("[{}] Registering local subscription [{}] for device [{}]", sessionId, sub.getSubscriptionId(), entityId);
+ subscription = new Subscription(sub, true);
+ }
+ registerSubscription(sessionId, entityId, subscription);
+ }
+
+ @Override
+ public void cleanupLocalWsSessionSubscriptions(TelemetryWebSocketSessionRef sessionRef, String sessionId) {
+ cleanupLocalWsSessionSubscriptions(sessionId);
+ }
+
+ @Override
+ public void removeSubscription(String sessionId, int subscriptionId) {
+ log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId);
+ Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId);
+ if (sessionSubscriptions != null) {
+ Subscription subscription = sessionSubscriptions.remove(subscriptionId);
+ if (subscription != null) {
+ processSubscriptionRemoval(sessionId, sessionSubscriptions, subscription);
+ } else {
+ log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId);
+ }
+ } else {
+ log.debug("[{}] No session subscriptions found!", sessionId);
+ }
+ }
+
+ @Override
+ public void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback) {
+ saveAndNotify(entityId, ts, 0L, callback);
+ }
+
+ @Override
+ public void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, long ttl, FutureCallback<Void> callback) {
+ ListenableFuture<List<Void>> saveFuture = tsService.save(entityId, ts, ttl);
+ addMainCallback(saveFuture, callback);
+ addWsCallback(saveFuture, success -> onTimeseriesUpdate(entityId, ts));
+ }
+
+ @Override
+ public void saveAndNotify(EntityId entityId, String scope, List<AttributeKvEntry> attributes, FutureCallback<Void> callback) {
+ ListenableFuture<List<Void>> saveFuture = attrService.save(entityId, scope, attributes);
+ addMainCallback(saveFuture, callback);
+ addWsCallback(saveFuture, success -> onAttributesUpdate(entityId, scope, attributes));
+ }
+
+ @Override
+ public void saveAttrAndNotify(EntityId entityId, String scope, String key, long value, FutureCallback<Void> callback) {
+ saveAndNotify(entityId, scope, Collections.singletonList(new BaseAttributeKvEntry(new LongDataEntry(key, value)
+ , System.currentTimeMillis())), callback);
+ }
+
+ @Override
+ public void saveAttrAndNotify(EntityId entityId, String scope, String key, String value, FutureCallback<Void> callback) {
+ saveAndNotify(entityId, scope, Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(key, value)
+ , System.currentTimeMillis())), callback);
+ }
+
+ @Override
+ public void saveAttrAndNotify(EntityId entityId, String scope, String key, double value, FutureCallback<Void> callback) {
+ saveAndNotify(entityId, scope, Collections.singletonList(new BaseAttributeKvEntry(new DoubleDataEntry(key, value)
+ , System.currentTimeMillis())), callback);
+ }
+
+ @Override
+ public void saveAttrAndNotify(EntityId entityId, String scope, String key, boolean value, FutureCallback<Void> callback) {
+ saveAndNotify(entityId, scope, Collections.singletonList(new BaseAttributeKvEntry(new BooleanDataEntry(key, value)
+ , System.currentTimeMillis())), callback);
+ }
+
+ @Override
+ public void onNewRemoteSubscription(ServerAddress serverAddress, byte[] data) {
+ ClusterAPIProtos.SubscriptionProto proto;
+ try {
+ proto = ClusterAPIProtos.SubscriptionProto.parseFrom(data);
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ Map<String, Long> statesMap = proto.getKeyStatesList().stream().collect(
+ Collectors.toMap(ClusterAPIProtos.SubscriptionKetStateProto::getKey, ClusterAPIProtos.SubscriptionKetStateProto::getTs));
+ Subscription subscription = new Subscription(
+ new SubscriptionState(proto.getSessionId(), proto.getSubscriptionId(),
+ EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()),
+ TelemetryFeature.valueOf(proto.getType()), proto.getAllKeys(), statesMap, proto.getScope()),
+ false, new ServerAddress(serverAddress.getHost(), serverAddress.getPort()));
+
+ addRemoteWsSubscription(serverAddress, proto.getSessionId(), subscription);
+ }
+
+ @Override
+ public void onRemoteSubscriptionUpdate(ServerAddress serverAddress, byte[] data) {
+ ClusterAPIProtos.SubscriptionUpdateProto proto;
+ try {
+ proto = ClusterAPIProtos.SubscriptionUpdateProto.parseFrom(data);
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ SubscriptionUpdate update = convert(proto);
+ String sessionId = proto.getSessionId();
+ log.trace("[{}] Processing remote subscription onUpdate [{}]", sessionId, update);
+ Optional<Subscription> subOpt = getSubscription(sessionId, update.getSubscriptionId());
+ if (subOpt.isPresent()) {
+ updateSubscriptionState(sessionId, subOpt.get(), update);
+ wsService.sendWsMsg(sessionId, update);
+ }
+ }
+
+ @Override
+ public void onRemoteSubscriptionClose(ServerAddress serverAddress, byte[] data) {
+ ClusterAPIProtos.SubscriptionCloseProto proto;
+ try {
+ proto = ClusterAPIProtos.SubscriptionCloseProto.parseFrom(data);
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ removeSubscription(proto.getSessionId(), proto.getSubscriptionId());
+ }
+
+ @Override
+ public void onRemoteSessionClose(ServerAddress serverAddress, byte[] data) {
+ ClusterAPIProtos.SessionCloseProto proto;
+ try {
+ proto = ClusterAPIProtos.SessionCloseProto.parseFrom(data);
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ cleanupRemoteWsSessionSubscriptions(proto.getSessionId());
+ }
+
+ @Override
+ public void onRemoteAttributesUpdate(ServerAddress serverAddress, byte[] data) {
+ ClusterAPIProtos.AttributeUpdateProto proto;
+ try {
+ proto = ClusterAPIProtos.AttributeUpdateProto.parseFrom(data);
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ onAttributesUpdate(EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()), proto.getScope(),
+ proto.getDataList().stream().map(this::toAttribute).collect(Collectors.toList()));
+ }
+
+ @Override
+ public void onRemoteTsUpdate(ServerAddress serverAddress, byte[] data) {
+ ClusterAPIProtos.TimeseriesUpdateProto proto;
+ try {
+ proto = ClusterAPIProtos.TimeseriesUpdateProto.parseFrom(data);
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ onTimeseriesUpdate(EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()),
+ proto.getDataList().stream().map(this::toTimeseries).collect(Collectors.toList()));
+ }
+
+ @Override
+ public void onClusterUpdate() {
+ log.trace("Processing cluster onUpdate msg!");
+ Iterator<Map.Entry<EntityId, Set<Subscription>>> deviceIterator = subscriptionsByEntityId.entrySet().iterator();
+ while (deviceIterator.hasNext()) {
+ Map.Entry<EntityId, Set<Subscription>> e = deviceIterator.next();
+ Set<Subscription> subscriptions = e.getValue();
+ Optional<ServerAddress> newAddressOptional = routingService.resolveById(e.getKey());
+ if (newAddressOptional.isPresent()) {
+ newAddressOptional.ifPresent(serverAddress -> checkSubsciptionsNewAddress(serverAddress, subscriptions));
+ } else {
+ checkSubsciptionsPrevAddress(subscriptions);
+ }
+ if (subscriptions.size() == 0) {
+ log.trace("[{}] No more subscriptions for this device on current server.", e.getKey());
+ deviceIterator.remove();
+ }
+ }
+ }
+
+ private void checkSubsciptionsNewAddress(ServerAddress newAddress, Set<Subscription> subscriptions) {
+ Iterator<Subscription> subscriptionIterator = subscriptions.iterator();
+ while (subscriptionIterator.hasNext()) {
+ Subscription s = subscriptionIterator.next();
+ if (s.isLocal()) {
+ if (!newAddress.equals(s.getServer())) {
+ log.trace("[{}] Local subscription is now handled on new server [{}]", s.getWsSessionId(), newAddress);
+ s.setServer(newAddress);
+ tellNewSubscription(newAddress, s.getWsSessionId(), s);
+ }
+ } else {
+ log.trace("[{}] Remote subscription is now handled on new server address: [{}]", s.getWsSessionId(), newAddress);
+ subscriptionIterator.remove();
+ //TODO: onUpdate state of subscription by WsSessionId and other maps.
+ }
+ }
+ }
+
+ private void checkSubsciptionsPrevAddress(Set<Subscription> subscriptions) {
+ for (Subscription s : subscriptions) {
+ if (s.isLocal() && s.getServer() != null) {
+ log.trace("[{}] Local subscription is no longer handled on remote server address [{}]", s.getWsSessionId(), s.getServer());
+ s.setServer(null);
+ } else {
+ log.trace("[{}] Remote subscription is on up to date server address.", s.getWsSessionId());
+ }
+ }
+ }
+
+ private void addRemoteWsSubscription(ServerAddress address, String sessionId, Subscription subscription) {
+ EntityId entityId = subscription.getEntityId();
+ log.trace("[{}] Registering remote subscription [{}] for device [{}] to [{}]", sessionId, subscription.getSubscriptionId(), entityId, address);
+ registerSubscription(sessionId, entityId, subscription);
+ if (subscription.getType() == TelemetryFeature.ATTRIBUTES) {
+ final Map<String, Long> keyStates = subscription.getKeyStates();
+ DonAsynchron.withCallback(attrService.find(entityId, DataConstants.CLIENT_SCOPE, keyStates.keySet()), values -> {
+ List<TsKvEntry> missedUpdates = new ArrayList<>();
+ values.forEach(latestEntry -> {
+ if (latestEntry.getLastUpdateTs() > keyStates.get(latestEntry.getKey())) {
+ missedUpdates.add(new BasicTsKvEntry(latestEntry.getLastUpdateTs(), latestEntry));
+ }
+ });
+ if (!missedUpdates.isEmpty()) {
+ tellRemoteSubUpdate(address, sessionId, new SubscriptionUpdate(subscription.getSubscriptionId(), missedUpdates));
+ }
+ },
+ e -> log.error("Failed to fetch missed updates.", e), tsCallBackExecutor);
+ } else if (subscription.getType() == TelemetryFeature.TIMESERIES) {
+ long curTs = System.currentTimeMillis();
+ List<TsKvQuery> queries = new ArrayList<>();
+ subscription.getKeyStates().entrySet().forEach(e -> {
+ queries.add(new BaseTsKvQuery(e.getKey(), e.getValue() + 1L, curTs));
+ });
+
+ DonAsynchron.withCallback(tsService.findAll(entityId, queries),
+ missedUpdates -> {
+ if (!missedUpdates.isEmpty()) {
+ tellRemoteSubUpdate(address, sessionId, new SubscriptionUpdate(subscription.getSubscriptionId(), missedUpdates));
+ }
+ },
+ e -> log.error("Failed to fetch missed updates.", e),
+ tsCallBackExecutor);
+ }
+ }
+
+ private void onAttributesUpdate(EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
+ Optional<ServerAddress> serverAddress = routingService.resolveById(entityId);
+ if (!serverAddress.isPresent()) {
+ onLocalAttributesUpdate(entityId, scope, attributes);
+ if (entityId.getEntityType() == EntityType.DEVICE && DataConstants.SERVER_SCOPE.equalsIgnoreCase(scope)) {
+ for (AttributeKvEntry attribute : attributes) {
+ if (attribute.getKey().equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT)) {
+ stateService.onDeviceInactivityTimeoutUpdate(new DeviceId(entityId.getId()), attribute.getLongValue().orElse(0L));
+ }
+ }
+ }
+ } else {
+ tellRemoteAttributesUpdate(serverAddress.get(), entityId, scope, attributes);
+ }
+ }
+
+ private void onTimeseriesUpdate(EntityId entityId, List<TsKvEntry> ts) {
+ Optional<ServerAddress> serverAddress = routingService.resolveById(entityId);
+ if (!serverAddress.isPresent()) {
+ onLocalTimeseriesUpdate(entityId, ts);
+ } else {
+ tellRemoteTimeseriesUpdate(serverAddress.get(), entityId, ts);
+ }
+ }
+
+ private void onLocalAttributesUpdate(EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
+ onLocalSubUpdate(entityId, s -> TelemetryFeature.ATTRIBUTES == s.getType() && (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope())), s -> {
+ List<TsKvEntry> subscriptionUpdate = null;
+ for (AttributeKvEntry kv : attributes) {
+ if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) {
+ if (subscriptionUpdate == null) {
+ subscriptionUpdate = new ArrayList<>();
+ }
+ subscriptionUpdate.add(new BasicTsKvEntry(kv.getLastUpdateTs(), kv));
+ }
+ }
+ return subscriptionUpdate;
+ });
+ }
+
+ private void onLocalTimeseriesUpdate(EntityId entityId, List<TsKvEntry> ts) {
+ onLocalSubUpdate(entityId, s -> TelemetryFeature.TIMESERIES == s.getType(), s -> {
+ List<TsKvEntry> subscriptionUpdate = null;
+ for (TsKvEntry kv : ts) {
+ if (s.isAllKeys() || s.getKeyStates().containsKey((kv.getKey()))) {
+ if (subscriptionUpdate == null) {
+ subscriptionUpdate = new ArrayList<>();
+ }
+ subscriptionUpdate.add(kv);
+ }
+ }
+ return subscriptionUpdate;
+ });
+ }
+
+ private void onLocalSubUpdate(EntityId entityId, Predicate<Subscription> filter, Function<Subscription, List<TsKvEntry>> f) {
+ Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId);
+ if (deviceSubscriptions != null) {
+ deviceSubscriptions.stream().filter(filter).forEach(s -> {
+ String sessionId = s.getWsSessionId();
+ List<TsKvEntry> subscriptionUpdate = f.apply(s);
+ if (subscriptionUpdate == null || !subscriptionUpdate.isEmpty()) {
+ SubscriptionUpdate update = new SubscriptionUpdate(s.getSubscriptionId(), subscriptionUpdate);
+ if (s.isLocal()) {
+ updateSubscriptionState(sessionId, s, update);
+ wsService.sendWsMsg(sessionId, update);
+ } else {
+ tellRemoteSubUpdate(s.getServer(), sessionId, update);
+ }
+ }
+ });
+ } else {
+ log.debug("[{}] No device subscriptions to process!", entityId);
+ }
+ }
+
+ private void updateSubscriptionState(String sessionId, Subscription subState, SubscriptionUpdate update) {
+ log.trace("[{}] updating subscription state {} using onUpdate {}", sessionId, subState, update);
+ update.getLatestValues().entrySet().forEach(e -> subState.setKeyState(e.getKey(), e.getValue()));
+ }
+
+ private void registerSubscription(String sessionId, EntityId entityId, Subscription subscription) {
+ Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.computeIfAbsent(entityId, k -> new HashSet<>());
+ deviceSubscriptions.add(subscription);
+ Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.computeIfAbsent(sessionId, k -> new HashMap<>());
+ sessionSubscriptions.put(subscription.getSubscriptionId(), subscription);
+ }
+
+ private void cleanupLocalWsSessionSubscriptions(String sessionId) {
+ cleanupWsSessionSubscriptions(sessionId, true);
+ }
+
+ private void cleanupRemoteWsSessionSubscriptions(String sessionId) {
+ cleanupWsSessionSubscriptions(sessionId, false);
+ }
+
+ private void cleanupWsSessionSubscriptions(String sessionId, boolean localSession) {
+ log.debug("[{}] Removing all subscriptions for particular session.", sessionId);
+ Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId);
+ if (sessionSubscriptions != null) {
+ int sessionSubscriptionSize = sessionSubscriptions.size();
+
+ for (Subscription subscription : sessionSubscriptions.values()) {
+ EntityId entityId = subscription.getEntityId();
+ Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId);
+ deviceSubscriptions.remove(subscription);
+ if (deviceSubscriptions.isEmpty()) {
+ subscriptionsByEntityId.remove(entityId);
+ }
+ }
+ subscriptionsByWsSessionId.remove(sessionId);
+ log.debug("[{}] Removed {} subscriptions for particular session.", sessionId, sessionSubscriptionSize);
+
+ if (localSession) {
+ notifyWsSubscriptionClosed(sessionId, sessionSubscriptions);
+ }
+ } else {
+ log.debug("[{}] No subscriptions found!", sessionId);
+ }
+ }
+
+ private void notifyWsSubscriptionClosed(String sessionId, Map<Integer, Subscription> sessionSubscriptions) {
+ Set<ServerAddress> affectedServers = new HashSet<>();
+ for (Subscription subscription : sessionSubscriptions.values()) {
+ if (subscription.getServer() != null) {
+ affectedServers.add(subscription.getServer());
+ }
+ }
+ for (ServerAddress address : affectedServers) {
+ log.debug("[{}] Going to onSubscriptionUpdate [{}] server about session close event", sessionId, address);
+ tellRemoteSessionClose(address, sessionId);
+ }
+ }
+
+ private void processSubscriptionRemoval(String sessionId, Map<Integer, Subscription> sessionSubscriptions, Subscription subscription) {
+ EntityId entityId = subscription.getEntityId();
+ if (subscription.isLocal() && subscription.getServer() != null) {
+ tellRemoteSubClose(subscription.getServer(), sessionId, subscription.getSubscriptionId());
+ }
+ if (sessionSubscriptions.isEmpty()) {
+ log.debug("[{}] Removed last subscription for particular session.", sessionId);
+ subscriptionsByWsSessionId.remove(sessionId);
+ } else {
+ log.debug("[{}] Removed session subscription.", sessionId);
+ }
+ Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId);
+ if (deviceSubscriptions != null) {
+ boolean result = deviceSubscriptions.remove(subscription);
+ if (result) {
+ if (deviceSubscriptions.size() == 0) {
+ log.debug("[{}] Removed last subscription for particular device.", sessionId);
+ subscriptionsByEntityId.remove(entityId);
+ } else {
+ log.debug("[{}] Removed device subscription.", sessionId);
+ }
+ } else {
+ log.debug("[{}] Subscription not found!", sessionId);
+ }
+ } else {
+ log.debug("[{}] No device subscriptions found!", sessionId);
+ }
+ }
+
+ private void addMainCallback(ListenableFuture<List<Void>> saveFuture, final FutureCallback<Void> callback) {
+ Futures.addCallback(saveFuture, new FutureCallback<List<Void>>() {
+ @Override
+ public void onSuccess(@Nullable List<Void> result) {
+ callback.onSuccess(null);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ callback.onFailure(t);
+ }
+ }, tsCallBackExecutor);
+ }
+
+ private void addWsCallback(ListenableFuture<List<Void>> saveFuture, Consumer<Void> callback) {
+ Futures.addCallback(saveFuture, new FutureCallback<List<Void>>() {
+ @Override
+ public void onSuccess(@Nullable List<Void> result) {
+ callback.accept(null);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ }
+ }, wsCallBackExecutor);
+ }
+
+ private void tellNewSubscription(ServerAddress address, String sessionId, Subscription sub) {
+ ClusterAPIProtos.SubscriptionProto.Builder builder = ClusterAPIProtos.SubscriptionProto.newBuilder();
+ builder.setSessionId(sessionId);
+ builder.setSubscriptionId(sub.getSubscriptionId());
+ builder.setEntityType(sub.getEntityId().getEntityType().name());
+ builder.setEntityId(sub.getEntityId().getId().toString());
+ builder.setType(sub.getType().name());
+ builder.setAllKeys(sub.isAllKeys());
+ builder.setScope(sub.getScope());
+ sub.getKeyStates().entrySet().forEach(e -> builder.addKeyStates(
+ ClusterAPIProtos.SubscriptionKetStateProto.newBuilder().setKey(e.getKey()).setTs(e.getValue()).build()));
+ rpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TELEMETRY_SUBSCRIPTION_CREATE_MESSAGE, builder.build().toByteArray());
+ }
+
+ private void tellRemoteSubUpdate(ServerAddress address, String sessionId, SubscriptionUpdate update) {
+ ClusterAPIProtos.SubscriptionUpdateProto.Builder builder = ClusterAPIProtos.SubscriptionUpdateProto.newBuilder();
+ builder.setSessionId(sessionId);
+ builder.setSubscriptionId(update.getSubscriptionId());
+ builder.setErrorCode(update.getErrorCode());
+ if (update.getErrorMsg() != null) {
+ builder.setErrorMsg(update.getErrorMsg());
+ }
+ update.getData().entrySet().forEach(
+ e -> {
+ ClusterAPIProtos.SubscriptionUpdateValueListProto.Builder dataBuilder = ClusterAPIProtos.SubscriptionUpdateValueListProto.newBuilder();
+
+ dataBuilder.setKey(e.getKey());
+ e.getValue().forEach(v -> {
+ Object[] array = (Object[]) v;
+ dataBuilder.addTs((long) array[0]);
+ dataBuilder.addValue((String) array[1]);
+ });
+
+ builder.addData(dataBuilder.build());
+ }
+ );
+ rpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TELEMETRY_SUBSCRIPTION_UPDATE_MESSAGE, builder.build().toByteArray());
+ }
+
+ private void tellRemoteAttributesUpdate(ServerAddress address, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
+ ClusterAPIProtos.AttributeUpdateProto.Builder builder = ClusterAPIProtos.AttributeUpdateProto.newBuilder();
+ builder.setEntityId(entityId.getId().toString());
+ builder.setEntityType(entityId.getEntityType().name());
+ builder.setScope(scope);
+ attributes.forEach(v -> builder.addData(toKeyValueProto(v.getLastUpdateTs(), v).build()));
+ rpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TELEMETRY_ATTR_UPDATE_MESSAGE, builder.build().toByteArray());
+ }
+
+ private void tellRemoteTimeseriesUpdate(ServerAddress address, EntityId entityId, List<TsKvEntry> ts) {
+ ClusterAPIProtos.TimeseriesUpdateProto.Builder builder = ClusterAPIProtos.TimeseriesUpdateProto.newBuilder();
+ builder.setEntityId(entityId.getId().toString());
+ builder.setEntityType(entityId.getEntityType().name());
+ ts.forEach(v -> builder.addData(toKeyValueProto(v.getTs(), v).build()));
+ rpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TELEMETRY_TS_UPDATE_MESSAGE, builder.build().toByteArray());
+ }
+
+ private void tellRemoteSessionClose(ServerAddress address, String sessionId) {
+ ClusterAPIProtos.SessionCloseProto proto = ClusterAPIProtos.SessionCloseProto.newBuilder().setSessionId(sessionId).build();
+ rpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TELEMETRY_SESSION_CLOSE_MESSAGE, proto.toByteArray());
+ }
+
+ private void tellRemoteSubClose(ServerAddress address, String sessionId, int subscriptionId) {
+ ClusterAPIProtos.SubscriptionCloseProto proto = ClusterAPIProtos.SubscriptionCloseProto.newBuilder().setSessionId(sessionId).setSubscriptionId(subscriptionId).build();
+ rpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TELEMETRY_SUBSCRIPTION_CLOSE_MESSAGE, proto.toByteArray());
+ }
+
+ private ClusterAPIProtos.KeyValueProto.Builder toKeyValueProto(long ts, KvEntry attr) {
+ ClusterAPIProtos.KeyValueProto.Builder dataBuilder = ClusterAPIProtos.KeyValueProto.newBuilder();
+ dataBuilder.setKey(attr.getKey());
+ dataBuilder.setTs(ts);
+ dataBuilder.setValueType(attr.getDataType().ordinal());
+ switch (attr.getDataType()) {
+ case BOOLEAN:
+ Optional<Boolean> booleanValue = attr.getBooleanValue();
+ booleanValue.ifPresent(dataBuilder::setBoolValue);
+ break;
+ case LONG:
+ Optional<Long> longValue = attr.getLongValue();
+ longValue.ifPresent(dataBuilder::setLongValue);
+ break;
+ case DOUBLE:
+ Optional<Double> doubleValue = attr.getDoubleValue();
+ doubleValue.ifPresent(dataBuilder::setDoubleValue);
+ break;
+ case STRING:
+ Optional<String> stringValue = attr.getStrValue();
+ stringValue.ifPresent(dataBuilder::setStrValue);
+ break;
+ }
+ return dataBuilder;
+ }
+
+ private AttributeKvEntry toAttribute(ClusterAPIProtos.KeyValueProto proto) {
+ return new BaseAttributeKvEntry(getKvEntry(proto), proto.getTs());
+ }
+
+ private TsKvEntry toTimeseries(ClusterAPIProtos.KeyValueProto proto) {
+ return new BasicTsKvEntry(proto.getTs(), getKvEntry(proto));
+ }
+
+ private KvEntry getKvEntry(ClusterAPIProtos.KeyValueProto proto) {
+ KvEntry entry = null;
+ DataType type = DataType.values()[proto.getValueType()];
+ switch (type) {
+ case BOOLEAN:
+ entry = new BooleanDataEntry(proto.getKey(), proto.getBoolValue());
+ break;
+ case LONG:
+ entry = new LongDataEntry(proto.getKey(), proto.getLongValue());
+ break;
+ case DOUBLE:
+ entry = new DoubleDataEntry(proto.getKey(), proto.getDoubleValue());
+ break;
+ case STRING:
+ entry = new StringDataEntry(proto.getKey(), proto.getStrValue());
+ break;
+ }
+ return entry;
+ }
+
+ private SubscriptionUpdate convert(ClusterAPIProtos.SubscriptionUpdateProto proto) {
+ if (proto.getErrorCode() > 0) {
+ return new SubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg());
+ } else {
+ Map<String, List<Object>> data = new TreeMap<>();
+ proto.getDataList().forEach(v -> {
+ List<Object> values = data.computeIfAbsent(v.getKey(), k -> new ArrayList<>());
+ for (int i = 0; i < v.getTsCount(); i++) {
+ Object[] value = new Object[2];
+ value[0] = v.getTs(i);
+ value[1] = v.getValue(i);
+ values.add(value);
+ }
+ });
+ return new SubscriptionUpdate(proto.getSubscriptionId(), data);
+ }
+ }
+
+ private Optional<Subscription> getSubscription(String sessionId, int subscriptionId) {
+ Subscription state = null;
+ Map<Integer, Subscription> subMap = subscriptionsByWsSessionId.get(sessionId);
+ if (subMap != null) {
+ state = subMap.get(subscriptionId);
+ }
+ return Optional.ofNullable(state);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
new file mode 100644
index 0000000..3b2d690
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
@@ -0,0 +1,560 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.service.security.AccessValidator;
+import org.thingsboard.server.service.security.ValidationResult;
+import org.thingsboard.server.service.telemetry.cmd.AttributesSubscriptionCmd;
+import org.thingsboard.server.service.telemetry.cmd.GetHistoryCmd;
+import org.thingsboard.server.service.telemetry.cmd.SubscriptionCmd;
+import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmd;
+import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper;
+import org.thingsboard.server.service.telemetry.cmd.TimeseriesSubscriptionCmd;
+import org.thingsboard.server.service.telemetry.exception.UnauthorizedException;
+import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode;
+import org.thingsboard.server.service.telemetry.sub.SubscriptionState;
+import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+@Service
+@Slf4j
+public class DefaultTelemetryWebSocketService implements TelemetryWebSocketService {
+
+ public static final int DEFAULT_LIMIT = 100;
+ public static final Aggregation DEFAULT_AGGREGATION = Aggregation.NONE;
+ private static final int UNKNOWN_SUBSCRIPTION_ID = 0;
+ private static final String PROCESSING_MSG = "[{}] Processing: {}";
+ private static final ObjectMapper jsonMapper = new ObjectMapper();
+ private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!";
+ private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!";
+ private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!";
+
+ private final ConcurrentMap<String, WsSessionMetaData> wsSessionsMap = new ConcurrentHashMap<>();
+
+ @Autowired
+ private TelemetrySubscriptionService subscriptionManager;
+
+ @Autowired
+ private TelemetryWebSocketMsgEndpoint msgEndpoint;
+
+ @Autowired
+ private AccessValidator accessValidator;
+
+ @Autowired
+ private AttributesService attributesService;
+
+ @Autowired
+ private TimeseriesService tsService;
+
+ private ExecutorService executor;
+
+ @PostConstruct
+ public void initExecutor() {
+ executor = Executors.newSingleThreadExecutor();
+ }
+
+ @PreDestroy
+ public void shutdownExecutor() {
+ if (executor != null) {
+ executor.shutdownNow();
+ }
+ }
+
+ @Override
+ public void handleWebSocketSessionEvent(TelemetryWebSocketSessionRef sessionRef, SessionEvent event) {
+ String sessionId = sessionRef.getSessionId();
+ log.debug(PROCESSING_MSG, sessionId, event);
+ switch (event.getEventType()) {
+ case ESTABLISHED:
+ wsSessionsMap.put(sessionId, new WsSessionMetaData(sessionRef));
+ break;
+ case ERROR:
+ log.debug("[{}] Unknown websocket session error: {}. ", sessionId, event.getError().orElse(null));
+ break;
+ case CLOSED:
+ wsSessionsMap.remove(sessionId);
+ subscriptionManager.cleanupLocalWsSessionSubscriptions(sessionRef, sessionId);
+ break;
+ }
+ }
+
+ @Override
+ public void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg) {
+ if (log.isTraceEnabled()) {
+ log.trace("[{}] Processing: {}", sessionRef.getSessionId(), msg);
+ }
+
+ try {
+ TelemetryPluginCmdsWrapper cmdsWrapper = jsonMapper.readValue(msg, TelemetryPluginCmdsWrapper.class);
+ if (cmdsWrapper != null) {
+ if (cmdsWrapper.getAttrSubCmds() != null) {
+ cmdsWrapper.getAttrSubCmds().forEach(cmd -> handleWsAttributesSubscriptionCmd(sessionRef, cmd));
+ }
+ if (cmdsWrapper.getTsSubCmds() != null) {
+ cmdsWrapper.getTsSubCmds().forEach(cmd -> handleWsTimeseriesSubscriptionCmd(sessionRef, cmd));
+ }
+ if (cmdsWrapper.getHistoryCmds() != null) {
+ cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(sessionRef, cmd));
+ }
+ }
+ } catch (IOException e) {
+ log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e);
+ SubscriptionUpdate update = new SubscriptionUpdate(UNKNOWN_SUBSCRIPTION_ID, SubscriptionErrorCode.INTERNAL_ERROR, SESSION_META_DATA_NOT_FOUND);
+ sendWsMsg(sessionRef, update);
+ }
+ }
+
+ @Override
+ public void sendWsMsg(String sessionId, SubscriptionUpdate update) {
+ WsSessionMetaData md = wsSessionsMap.get(sessionId);
+ if (md != null) {
+ sendWsMsg(md.getSessionRef(), update);
+ }
+ }
+
+ private void handleWsAttributesSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, AttributesSubscriptionCmd cmd) {
+ String sessionId = sessionRef.getSessionId();
+ log.debug("[{}] Processing: {}", sessionId, cmd);
+
+ if (validateSessionMetadata(sessionRef, cmd, sessionId)) {
+ if (cmd.isUnsubscribe()) {
+ unsubscribe(sessionRef, cmd, sessionId);
+ } else if (validateSubscriptionCmd(sessionRef, cmd)) {
+ EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
+ log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), entityId);
+ Optional<Set<String>> keysOptional = getKeys(cmd);
+ if (keysOptional.isPresent()) {
+ List<String> keys = new ArrayList<>(keysOptional.get());
+ handleWsAttributesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId, keys);
+ } else {
+ handleWsAttributesSubscription(sessionRef, cmd, sessionId, entityId);
+ }
+ }
+ }
+ }
+
+ private void handleWsAttributesSubscriptionByKeys(TelemetryWebSocketSessionRef sessionRef,
+ AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId,
+ List<String> keys) {
+ FutureCallback<List<AttributeKvEntry>> callback = new FutureCallback<List<AttributeKvEntry>>() {
+ @Override
+ public void onSuccess(List<AttributeKvEntry> data) {
+ List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
+ sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
+
+ Map<String, Long> subState = new HashMap<>(keys.size());
+ keys.forEach(key -> subState.put(key, 0L));
+ attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
+
+ SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.ATTRIBUTES, false, subState, cmd.getScope());
+ subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub);
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ log.error(FAILED_TO_FETCH_ATTRIBUTES, e);
+ SubscriptionUpdate update;
+ if (UnauthorizedException.class.isInstance(e)) {
+ update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
+ SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
+ } else {
+ update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+ FAILED_TO_FETCH_ATTRIBUTES);
+ }
+ sendWsMsg(sessionRef, update);
+ }
+ };
+
+ if (StringUtils.isEmpty(cmd.getScope())) {
+ accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, keys, callback));
+ } else {
+ accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, cmd.getScope(), keys, callback));
+ }
+ }
+
+ private void handleWsHistoryCmd(TelemetryWebSocketSessionRef sessionRef, GetHistoryCmd cmd) {
+ String sessionId = sessionRef.getSessionId();
+ WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
+ if (sessionMD == null) {
+ log.warn("[{}] Session meta data not found. ", sessionId);
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+ SESSION_META_DATA_NOT_FOUND);
+ sendWsMsg(sessionRef, update);
+ return;
+ }
+ if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty() || cmd.getEntityType() == null || cmd.getEntityType().isEmpty()) {
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+ "Device id is empty!");
+ sendWsMsg(sessionRef, update);
+ return;
+ }
+ if (cmd.getKeys() == null || cmd.getKeys().isEmpty()) {
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+ "Keys are empty!");
+ sendWsMsg(sessionRef, update);
+ return;
+ }
+ EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
+ List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
+ List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg())))
+ .collect(Collectors.toList());
+
+ FutureCallback<List<TsKvEntry>> callback = new FutureCallback<List<TsKvEntry>>() {
+ @Override
+ public void onSuccess(List<TsKvEntry> data) {
+ sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ SubscriptionUpdate update;
+ if (UnauthorizedException.class.isInstance(e)) {
+ update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
+ SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
+ } else {
+ update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+ FAILED_TO_FETCH_DATA);
+ }
+ sendWsMsg(sessionRef, update);
+ }
+ };
+ accessValidator.validate(sessionRef.getSecurityCtx(), entityId,
+ on(r -> Futures.addCallback(tsService.findAll(entityId, queries), callback, executor), callback::onFailure));
+ }
+
+ private void handleWsAttributesSubscription(TelemetryWebSocketSessionRef sessionRef,
+ AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId) {
+ FutureCallback<List<AttributeKvEntry>> callback = new FutureCallback<List<AttributeKvEntry>>() {
+ @Override
+ public void onSuccess(List<AttributeKvEntry> data) {
+ List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
+ sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
+
+ Map<String, Long> subState = new HashMap<>(attributesData.size());
+ attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
+
+ SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.ATTRIBUTES, true, subState, cmd.getScope());
+ subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub);
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ log.error(FAILED_TO_FETCH_ATTRIBUTES, e);
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+ FAILED_TO_FETCH_ATTRIBUTES);
+ sendWsMsg(sessionRef, update);
+ }
+ };
+
+
+ if (StringUtils.isEmpty(cmd.getScope())) {
+ accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, callback));
+ } else {
+ accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, cmd.getScope(), callback));
+ }
+ }
+
+ private void handleWsTimeseriesSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd) {
+ String sessionId = sessionRef.getSessionId();
+ log.debug("[{}] Processing: {}", sessionId, cmd);
+
+ if (validateSessionMetadata(sessionRef, cmd, sessionId)) {
+ if (cmd.isUnsubscribe()) {
+ unsubscribe(sessionRef, cmd, sessionId);
+ } else if (validateSubscriptionCmd(sessionRef, cmd)) {
+ EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
+ Optional<Set<String>> keysOptional = getKeys(cmd);
+
+ if (keysOptional.isPresent()) {
+ handleWsTimeseriesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId);
+ } else {
+ handleWsTimeseriesSubscription(sessionRef, cmd, sessionId, entityId);
+ }
+ }
+ }
+ }
+
+ private void handleWsTimeseriesSubscriptionByKeys(TelemetryWebSocketSessionRef sessionRef,
+ TimeseriesSubscriptionCmd cmd, String sessionId, EntityId entityId) {
+ long startTs;
+ if (cmd.getTimeWindow() > 0) {
+ List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
+ log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), entityId);
+ startTs = cmd.getStartTs();
+ long endTs = cmd.getStartTs() + cmd.getTimeWindow();
+ List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, cmd.getInterval(),
+ getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
+
+ final FutureCallback<List<TsKvEntry>> callback = getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys);
+ accessValidator.validate(sessionRef.getSecurityCtx(), entityId,
+ on(r -> Futures.addCallback(tsService.findAll(entityId, queries), callback, executor), callback::onFailure));
+ } else {
+ List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
+ startTs = System.currentTimeMillis();
+ log.debug("[{}] fetching latest timeseries data for keys: ({}) for device : {}", sessionId, cmd.getKeys(), entityId);
+ final FutureCallback<List<TsKvEntry>> callback = getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys);
+ accessValidator.validate(sessionRef.getSecurityCtx(), entityId,
+ on(r -> Futures.addCallback(tsService.findLatest(entityId, keys), callback, executor), callback::onFailure));
+ }
+ }
+
+ private void handleWsTimeseriesSubscription(TelemetryWebSocketSessionRef sessionRef,
+ TimeseriesSubscriptionCmd cmd, String sessionId, EntityId entityId) {
+ FutureCallback<List<TsKvEntry>> callback = new FutureCallback<List<TsKvEntry>>() {
+ @Override
+ public void onSuccess(List<TsKvEntry> data) {
+ sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+ Map<String, Long> subState = new HashMap<>(data.size());
+ data.forEach(v -> subState.put(v.getKey(), v.getTs()));
+ SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.TIMESERIES, true, subState, cmd.getScope());
+ subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub);
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ SubscriptionUpdate update;
+ if (UnauthorizedException.class.isInstance(e)) {
+ update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
+ SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
+ } else {
+ update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+ FAILED_TO_FETCH_DATA);
+ }
+ sendWsMsg(sessionRef, update);
+ }
+ };
+ accessValidator.validate(sessionRef.getSecurityCtx(), entityId,
+ on(r -> Futures.addCallback(tsService.findAllLatest(entityId), callback, executor), callback::onFailure));
+ }
+
+ private FutureCallback<List<TsKvEntry>> getSubscriptionCallback(final TelemetryWebSocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final EntityId entityId, final long startTs, final List<String> keys) {
+ return new FutureCallback<List<TsKvEntry>>() {
+ @Override
+ public void onSuccess(List<TsKvEntry> data) {
+ sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+
+ Map<String, Long> subState = new HashMap<>(keys.size());
+ keys.forEach(key -> subState.put(key, startTs));
+ data.forEach(v -> subState.put(v.getKey(), v.getTs()));
+ SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.TIMESERIES, false, subState, cmd.getScope());
+ subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub);
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ log.error(FAILED_TO_FETCH_DATA, e);
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+ FAILED_TO_FETCH_DATA);
+ sendWsMsg(sessionRef, update);
+ }
+ };
+ }
+
+ private void unsubscribe(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
+ if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
+ subscriptionManager.cleanupLocalWsSessionSubscriptions(sessionRef, sessionId);
+ } else {
+ subscriptionManager.removeSubscription(sessionId, cmd.getCmdId());
+ }
+ }
+
+ private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) {
+ if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+ "Device id is empty!");
+ sendWsMsg(sessionRef, update);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
+ WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
+ if (sessionMD == null) {
+ log.warn("[{}] Session meta data not found. ", sessionId);
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+ SESSION_META_DATA_NOT_FOUND);
+ sendWsMsg(sessionRef, update);
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, SubscriptionUpdate update) {
+ try {
+ msgEndpoint.send(sessionRef, jsonMapper.writeValueAsString(update));
+ } catch (JsonProcessingException e) {
+ log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e);
+ } catch (IOException e) {
+ log.warn("[{}] Failed to send reply: {}", sessionRef.getSessionId(), update, e);
+ }
+ }
+
+ private static Optional<Set<String>> getKeys(TelemetryPluginCmd cmd) {
+ if (!StringUtils.isEmpty(cmd.getKeys())) {
+ Set<String> keys = new HashSet<>();
+ Collections.addAll(keys, cmd.getKeys().split(","));
+ return Optional.of(keys);
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ private ListenableFuture<List<AttributeKvEntry>> mergeAllAttributesFutures(List<ListenableFuture<List<AttributeKvEntry>>> futures) {
+ return Futures.transform(Futures.successfulAsList(futures),
+ (Function<? super List<List<AttributeKvEntry>>, ? extends List<AttributeKvEntry>>) input -> {
+ List<AttributeKvEntry> tmp = new ArrayList<>();
+ if (input != null) {
+ input.forEach(tmp::addAll);
+ }
+ return tmp;
+ }, executor);
+ }
+
+ private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final List<String> keys, final FutureCallback<List<AttributeKvEntry>> callback) {
+ return new FutureCallback<ValidationResult>() {
+ @Override
+ public void onSuccess(@Nullable ValidationResult result) {
+ List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
+ for (String scope : DataConstants.allScopes()) {
+ futures.add(attributesService.find(entityId, scope, keys));
+ }
+
+ ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures);
+ Futures.addCallback(future, callback);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ callback.onFailure(t);
+ }
+ };
+ }
+
+ private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final String scope, final List<String> keys, final FutureCallback<List<AttributeKvEntry>> callback) {
+ return new FutureCallback<ValidationResult>() {
+ @Override
+ public void onSuccess(@Nullable ValidationResult result) {
+ Futures.addCallback(attributesService.find(entityId, scope, keys), callback);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ callback.onFailure(t);
+ }
+ };
+ }
+
+ private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final FutureCallback<List<AttributeKvEntry>> callback) {
+ return new FutureCallback<ValidationResult>() {
+ @Override
+ public void onSuccess(@Nullable ValidationResult result) {
+ List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
+ for (String scope : DataConstants.allScopes()) {
+ futures.add(attributesService.findAll(entityId, scope));
+ }
+
+ ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures);
+ Futures.addCallback(future, callback);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ callback.onFailure(t);
+ }
+ };
+ }
+
+ private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final String scope, final FutureCallback<List<AttributeKvEntry>> callback) {
+ return new FutureCallback<ValidationResult>() {
+ @Override
+ public void onSuccess(@Nullable ValidationResult result) {
+ Futures.addCallback(attributesService.findAll(entityId, scope), callback);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ callback.onFailure(t);
+ }
+ };
+ }
+
+ private FutureCallback<ValidationResult> on(Consumer<ValidationResult> success, Consumer<Throwable> failure) {
+ return new FutureCallback<ValidationResult>() {
+ @Override
+ public void onSuccess(@Nullable ValidationResult result) {
+ success.accept(result);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ failure.accept(t);
+ }
+ };
+ }
+
+
+ private static Aggregation getAggregation(String agg) {
+ return StringUtils.isEmpty(agg) ? DEFAULT_AGGREGATION : Aggregation.valueOf(agg);
+ }
+
+ private int getLimit(int limit) {
+ return limit == 0 ? DEFAULT_LIMIT : limit;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/Subscription.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/Subscription.java
new file mode 100644
index 0000000..811c055
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/Subscription.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry.sub;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.service.telemetry.TelemetryFeature;
+
+import java.util.Map;
+
+@Data
+@AllArgsConstructor
+public class Subscription {
+
+ private final SubscriptionState sub;
+ private final boolean local;
+ private ServerAddress server;
+
+ public Subscription(SubscriptionState sub, boolean local) {
+ this(sub, local, null);
+ }
+
+ public String getWsSessionId() {
+ return getSub().getWsSessionId();
+ }
+
+ public int getSubscriptionId() {
+ return getSub().getSubscriptionId();
+ }
+
+ public EntityId getEntityId() {
+ return getSub().getEntityId();
+ }
+
+ public TelemetryFeature getType() {
+ return getSub().getType();
+ }
+
+ public String getScope() {
+ return getSub().getScope();
+ }
+
+ public boolean isAllKeys() {
+ return getSub().isAllKeys();
+ }
+
+ public Map<String, Long> getKeyStates() {
+ return getSub().getKeyStates();
+ }
+
+ public void setKeyState(String key, long ts) {
+ getSub().getKeyStates().put(key, ts);
+ }
+
+ @Override
+ public String toString() {
+ return "Subscription{" +
+ "sub=" + sub +
+ ", local=" + local +
+ ", server=" + server +
+ '}';
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionErrorCode.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionErrorCode.java
new file mode 100644
index 0000000..9da74d9
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionErrorCode.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry.sub;
+
+public enum SubscriptionErrorCode {
+
+ NO_ERROR(0), INTERNAL_ERROR(1, "Internal Server error!"), BAD_REQUEST(2, "Bad request"), UNAUTHORIZED(3, "Unauthorized");
+
+ private final int code;
+ private final String defaultMsg;
+
+ private SubscriptionErrorCode(int code) {
+ this(code, null);
+ }
+
+ private SubscriptionErrorCode(int code, String defaultMsg) {
+ this.code = code;
+ this.defaultMsg = defaultMsg;
+ }
+
+ public static SubscriptionErrorCode forCode(int code) {
+ for (SubscriptionErrorCode errorCode : SubscriptionErrorCode.values()) {
+ if (errorCode.getCode() == code) {
+ return errorCode;
+ }
+ }
+ throw new IllegalArgumentException("Invalid error code: " + code);
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public String getDefaultMsg() {
+ return defaultMsg;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionState.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionState.java
new file mode 100644
index 0000000..a088fa9
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionState.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry.sub;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.service.telemetry.TelemetryFeature;
+
+import java.util.Map;
+
+/**
+ * @author Andrew Shvayka
+ */
+@AllArgsConstructor
+public class SubscriptionState {
+
+ @Getter private final String wsSessionId;
+ @Getter private final int subscriptionId;
+ @Getter private final EntityId entityId;
+ @Getter private final TelemetryFeature type;
+ @Getter private final boolean allKeys;
+ @Getter private final Map<String, Long> keyStates;
+ @Getter private final String scope;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ SubscriptionState that = (SubscriptionState) o;
+
+ if (subscriptionId != that.subscriptionId) return false;
+ if (wsSessionId != null ? !wsSessionId.equals(that.wsSessionId) : that.wsSessionId != null) return false;
+ if (entityId != null ? !entityId.equals(that.entityId) : that.entityId != null) return false;
+ return type == that.type;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = wsSessionId != null ? wsSessionId.hashCode() : 0;
+ result = 31 * result + subscriptionId;
+ result = 31 * result + (entityId != null ? entityId.hashCode() : 0);
+ result = 31 * result + (type != null ? type.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "SubscriptionState{" +
+ "type=" + type +
+ ", entityId=" + entityId +
+ ", subscriptionId=" + subscriptionId +
+ ", wsSessionId='" + wsSessionId + '\'' +
+ '}';
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionUpdate.java
new file mode 100644
index 0000000..bbabe7a
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionUpdate.java
@@ -0,0 +1,99 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry.sub;
+
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+public class SubscriptionUpdate {
+
+ private int subscriptionId;
+ private int errorCode;
+ private String errorMsg;
+ private Map<String, List<Object>> data;
+
+ public SubscriptionUpdate(int subscriptionId, List<TsKvEntry> data) {
+ super();
+ this.subscriptionId = subscriptionId;
+ this.data = new TreeMap<>();
+ if (data != null) {
+ for (TsKvEntry tsEntry : data) {
+ List<Object> values = this.data.computeIfAbsent(tsEntry.getKey(), k -> new ArrayList<>());
+ Object[] value = new Object[2];
+ value[0] = tsEntry.getTs();
+ value[1] = tsEntry.getValueAsString();
+ values.add(value);
+ }
+ }
+ }
+
+ public SubscriptionUpdate(int subscriptionId, Map<String, List<Object>> data) {
+ super();
+ this.subscriptionId = subscriptionId;
+ this.data = data;
+ }
+
+ public SubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode) {
+ this(subscriptionId, errorCode, null);
+ }
+
+ public SubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode, String errorMsg) {
+ super();
+ this.subscriptionId = subscriptionId;
+ this.errorCode = errorCode.getCode();
+ this.errorMsg = errorMsg != null ? errorMsg : errorCode.getDefaultMsg();
+ }
+
+ public int getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ public Map<String, List<Object>> getData() {
+ return data;
+ }
+
+ public Map<String, Long> getLatestValues() {
+ if (data == null) {
+ return Collections.emptyMap();
+ } else {
+ return data.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> {
+ List<Object> data = e.getValue();
+ Object[] latest = (Object[]) data.get(data.size() - 1);
+ return (long) latest[0];
+ }));
+ }
+ }
+
+ public int getErrorCode() {
+ return errorCode;
+ }
+
+ public String getErrorMsg() {
+ return errorMsg;
+ }
+
+ @Override
+ public String toString() {
+ return "SubscriptionUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", data="
+ + data + "]";
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java
new file mode 100644
index 0000000..f1802ac
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry;
+
+import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.service.telemetry.sub.SubscriptionState;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+public interface TelemetrySubscriptionService extends RuleEngineTelemetryService {
+
+ void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub);
+
+ void cleanupLocalWsSessionSubscriptions(TelemetryWebSocketSessionRef sessionRef, String sessionId);
+
+ void removeSubscription(String sessionId, int cmdId);
+
+ void onNewRemoteSubscription(ServerAddress serverAddress, byte[] data);
+
+ void onRemoteSubscriptionUpdate(ServerAddress serverAddress, byte[] bytes);
+
+ void onRemoteSubscriptionClose(ServerAddress serverAddress, byte[] bytes);
+
+ void onRemoteSessionClose(ServerAddress serverAddress, byte[] bytes);
+
+ void onRemoteAttributesUpdate(ServerAddress serverAddress, byte[] bytes);
+
+ void onRemoteTsUpdate(ServerAddress serverAddress, byte[] bytes);
+
+ void onClusterUpdate();
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java
new file mode 100644
index 0000000..53438c5
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry;
+
+import lombok.Getter;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+import java.net.InetSocketAddress;
+import java.util.Objects;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+public class TelemetryWebSocketSessionRef {
+
+ private static final long serialVersionUID = 1L;
+
+ @Getter
+ private final String sessionId;
+ @Getter
+ private final SecurityUser securityCtx;
+ @Getter
+ private final InetSocketAddress localAddress;
+ @Getter
+ private final InetSocketAddress remoteAddress;
+
+ public TelemetryWebSocketSessionRef(String sessionId, SecurityUser securityCtx, InetSocketAddress localAddress, InetSocketAddress remoteAddress) {
+ this.sessionId = sessionId;
+ this.securityCtx = securityCtx;
+ this.localAddress = localAddress;
+ this.remoteAddress = remoteAddress;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TelemetryWebSocketSessionRef that = (TelemetryWebSocketSessionRef) o;
+ return Objects.equals(sessionId, that.sessionId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(sessionId);
+ }
+
+ @Override
+ public String toString() {
+ return "TelemetryWebSocketSessionRef{" +
+ "sessionId='" + sessionId + '\'' +
+ ", localAddress=" + localAddress +
+ ", remoteAddress=" + remoteAddress +
+ '}';
+ }
+}
application/src/main/proto/cluster.proto 129(+81 -48)
diff --git a/application/src/main/proto/cluster.proto b/application/src/main/proto/cluster.proto
index e106d1b..ac96010 100644
--- a/application/src/main/proto/cluster.proto
+++ b/application/src/main/proto/cluster.proto
@@ -19,79 +19,112 @@ package cluster;
option java_package = "org.thingsboard.server.gen.cluster";
option java_outer_classname = "ClusterAPIProtos";
+service ClusterRpcService {
+ rpc handleMsgs(stream ClusterMessage) returns (stream ClusterMessage) {}
+}
+message ClusterMessage {
+ MessageType messageType = 1;
+ MessageMataInfo messageMetaInfo = 2;
+ ServerAddress serverAddress = 3;
+ bytes payload = 4;
+}
+
message ServerAddress {
string host = 1;
int32 port = 2;
}
-message Uid {
- sint64 pluginUuidMsb = 1;
- sint64 pluginUuidLsb = 2;
+message MessageMataInfo {
+ string payloadMetaInfo = 1;
+ repeated string tags = 2;
}
-message PluginAddress {
- Uid pluginId = 1;
- Uid tenantId = 2;
+enum MessageType {
+
+ //Cluster control messages
+ RPC_SESSION_CREATE_REQUEST_MSG = 0;
+ TO_ALL_NODES_MSG = 1;
+ RPC_SESSION_TELL_MSG = 2;
+ RPC_BROADCAST_MSG = 3;
+ CONNECT_RPC_MESSAGE =4;
+
+ CLUSTER_ACTOR_MESSAGE = 5;
+ // Messages related to TelemetrySubscriptionService
+ CLUSTER_TELEMETRY_SUBSCRIPTION_CREATE_MESSAGE = 6;
+ CLUSTER_TELEMETRY_SUBSCRIPTION_UPDATE_MESSAGE = 7;
+ CLUSTER_TELEMETRY_SUBSCRIPTION_CLOSE_MESSAGE = 8;
+ CLUSTER_TELEMETRY_SESSION_CLOSE_MESSAGE = 9;
+ CLUSTER_TELEMETRY_ATTR_UPDATE_MESSAGE = 10;
+ CLUSTER_TELEMETRY_TS_UPDATE_MESSAGE = 11;
+ CLUSTER_RPC_FROM_DEVICE_RESPONSE_MESSAGE = 12;
}
-message ToPluginRpcMessage {
- PluginAddress address = 1;
- int32 clazz = 2;
- bytes data = 3;
+// Messages related to CLUSTER_TELEMETRY_MESSAGE
+message SubscriptionProto {
+ string sessionId = 1;
+ int32 subscriptionId = 2;
+ string entityType = 3;
+ string entityId = 4;
+ string type = 5;
+ bool allKeys = 6;
+ repeated SubscriptionKetStateProto keyStates = 7;
+ string scope = 8;
}
-message ToDeviceActorRpcMessage {
- bytes data = 1;
+message SubscriptionUpdateProto {
+ string sessionId = 1;
+ int32 subscriptionId = 2;
+ int32 errorCode = 3;
+ string errorMsg = 4;
+ repeated SubscriptionUpdateValueListProto data = 5;
}
-message ToDeviceSessionActorRpcMessage {
- bytes data = 1;
+message AttributeUpdateProto {
+ string entityType = 1;
+ string entityId = 2;
+ string scope = 3;
+ repeated KeyValueProto data = 4;
}
-message ToDeviceActorNotificationRpcMessage {
- bytes data = 1;
+message TimeseriesUpdateProto {
+ string entityType = 1;
+ string entityId = 2;
+ repeated KeyValueProto data = 4;
}
-message ToAllNodesRpcMessage {
- bytes data = 1;
+message SessionCloseProto {
+ string sessionId = 1;
}
-message ConnectRpcMessage {
- ServerAddress serverAddress = 1;
+message SubscriptionCloseProto {
+ string sessionId = 1;
+ int32 subscriptionId = 2;
}
-message ToDeviceRpcRequestRpcMessage {
- PluginAddress address = 1;
- Uid deviceTenantId = 2;
- Uid deviceId = 3;
-
- Uid msgId = 4;
- bool oneway = 5;
- int64 expTime = 6;
- string method = 7;
- string params = 8;
+message SubscriptionKetStateProto {
+ string key = 1;
+ int64 ts = 2;
}
-message ToPluginRpcResponseRpcMessage {
- PluginAddress address = 1;
-
- Uid msgId = 2;
- string response = 3;
- string error = 4;
+message SubscriptionUpdateValueListProto {
+ string key = 1;
+ repeated int64 ts = 2;
+ repeated string value = 3;
}
-message ToRpcServerMessage {
- ConnectRpcMessage connectMsg = 1;
- ToPluginRpcMessage toPluginRpcMsg = 2;
- ToDeviceActorRpcMessage toDeviceActorRpcMsg = 3;
- ToDeviceSessionActorRpcMessage toDeviceSessionActorRpcMsg = 4;
- ToDeviceActorNotificationRpcMessage toDeviceActorNotificationRpcMsg = 5;
- ToAllNodesRpcMessage toAllNodesRpcMsg = 6;
- ToDeviceRpcRequestRpcMessage toDeviceRpcRequestRpcMsg = 7;
- ToPluginRpcResponseRpcMessage toPluginRpcResponseRpcMsg = 8;
+message KeyValueProto {
+ string key = 1;
+ int64 ts = 2;
+ int32 valueType = 3;
+ string strValue = 4;
+ int64 longValue = 5;
+ double doubleValue = 6;
+ bool boolValue = 7;
}
-service ClusterRpcService {
- rpc handlePluginMsgs(stream ToRpcServerMessage) returns (stream ToRpcServerMessage) {}
+message FromDeviceRPCResponseProto {
+ int64 requestIdMSB = 1;
+ int64 requestIdLSB = 2;
+ string response = 3;
+ int32 error = 4;
}
-
diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml
index 1779912..978a570 100644
--- a/application/src/main/resources/logback.xml
+++ b/application/src/main/resources/logback.xml
@@ -25,7 +25,7 @@
</encoder>
</appender>
- <logger name="org.thingsboard.server" level="TRACE" />
+ <logger name="org.thingsboard.server" level="INFO" />
<logger name="akka" level="INFO" />
<root level="INFO">
application/src/main/resources/thingsboard.yml 107(+85 -22)
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 6a45f42..5653193 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -58,11 +58,13 @@ cluster:
hash_function_name: "${CLUSTER_HASH_FUNCTION_NAME:murmur3_128}"
# Amount of virtual nodes in consistent hash ring.
vitrual_nodes_size: "${CLUSTER_VIRTUAL_NODES_SIZE:16}"
+ # Queue partition id for current node
+ partition_id: "${QUEUE_PARTITION_ID:0}"
# Plugins configuration parameters
plugins:
# Comma seperated package list used during classpath scanning for plugins
- scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions}"
+ scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions,org.thingsboard.rule.engine}"
# JWT Token parameters
security.jwt:
@@ -106,7 +108,7 @@ mqtt:
# CoAP server parameters
coap:
# Enable/disable coap transport protocol.
- enabled: "${COAP_ENABLED:true}"
+ enabled: "${COAP_ENABLED:false}"
bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}"
bind_port: "${COAP_BIND_PORT:5683}"
adaptor: "${COAP_ADAPTOR_NAME:JsonCoapAdaptor}"
@@ -129,9 +131,28 @@ quota:
whitelist: "${QUOTA_HOST_WHITELIST:localhost,127.0.0.1}"
# Array of blacklist hosts
blacklist: "${QUOTA_HOST_BLACKLIST:}"
- log:
- topSize: 10
- intervalMin: 2
+ log:
+ topSize: 10
+ intervalMin: 2
+ rule:
+ tenant:
+ # Max allowed number of API requests in interval for single tenant
+ limit: "${QUOTA_TENANT_LIMIT:100000}"
+ # Interval duration
+ intervalMs: "${QUOTA_TENANT_INTERVAL_MS:60000}"
+ # Maximum silence duration for tenant after which Tenant removed from QuotaService. Must be bigger than intervalMs
+ ttlMs: "${QUOTA_TENANT_TTL_MS:60000}"
+ # Interval for scheduled task that cleans expired records. TTL is used for expiring
+ cleanPeriodMs: "${QUOTA_TENANT_CLEAN_PERIOD_MS:300000}"
+ # Enable Host API Limits
+ enabled: "${QUOTA_TENANT_ENABLED:false}"
+ # Array of whitelist tenants
+ whitelist: "${QUOTA_TENANT_WHITELIST:}"
+ # Array of blacklist tenants
+ blacklist: "${QUOTA_HOST_BLACKLIST:}"
+ log:
+ topSize: 10
+ intervalMin: 2
database:
type: "${DATABASE_TYPE:sql}" # cassandra OR sql
@@ -182,11 +203,18 @@ cassandra:
default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
# Specify partitioning size for timestamp key-value storage. Example MINUTES, HOURS, DAYS, MONTHS
ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
+ ts_key_value_ttl: "${TS_KV_TTL:0}"
buffer_size: "${CASSANDRA_QUERY_BUFFER_SIZE:200000}"
concurrent_limit: "${CASSANDRA_QUERY_CONCURRENT_LIMIT:1000}"
permit_max_wait_time: "${PERMIT_MAX_WAIT_TIME:120000}"
rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:30000}"
+ queue:
+ msg.ttl: 604800 # 7 days
+ ack.ttl: 604800 # 7 days
+ partitions.ttl: 604800 # 7 days
+ partitioning: "HOURS"
+
# SQL configuration parameters
sql:
# Specify executor service type used to perform timeseries insert tasks: SINGLE FIXED CACHED
@@ -199,25 +227,54 @@ actors:
tenant:
create_components_on_init: true
session:
+ max_concurrent_sessions_per_device: "${ACTORS_MAX_CONCURRENT_SESSION_PER_DEVICE:1}"
sync:
# Default timeout for processing request using synchronous session (HTTP, CoAP) in milliseconds
timeout: "${ACTORS_SESSION_SYNC_TIMEOUT:10000}"
- plugin:
- # Default timeout for termination of the plugin actor after it is stopped
- termination.delay: "${ACTORS_PLUGIN_TERMINATION_DELAY:60000}"
- # Default timeout for processing of particular message by particular plugin
- processing.timeout: "${ACTORS_PLUGIN_TIMEOUT:60000}"
- # Errors for particular actor are persisted once per specified amount of milliseconds
- error_persist_frequency: "${ACTORS_PLUGIN_ERROR_FREQUENCY:3000}"
rule:
- # Default timeout for termination of the rule actor after it is stopped
- termination.delay: "${ACTORS_RULE_TERMINATION_DELAY:30000}"
- # Errors for particular actor are persisted once per specified amount of milliseconds
- error_persist_frequency: "${ACTORS_RULE_ERROR_FREQUENCY:3000}"
+ # Specify thread pool size for database request callbacks executor service
+ db_callback_thread_pool_size: "${ACTORS_RULE_DB_CALLBACK_THREAD_POOL_SIZE:1}"
+ # Specify thread pool size for javascript executor service
+ js_thread_pool_size: "${ACTORS_RULE_JS_THREAD_POOL_SIZE:10}"
+ # Specify thread pool size for mail sender executor service
+ mail_thread_pool_size: "${ACTORS_RULE_MAIL_THREAD_POOL_SIZE:10}"
+ # Whether to allow usage of system mail service for rules
+ allow_system_mail_service: "${ACTORS_RULE_ALLOW_SYSTEM_MAIL_SERVICE:true}"
+ # Specify thread pool size for external call service
+ external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:10}"
+ js_sandbox:
+ # Use Sandboxed (secured) JavaScript environment
+ use_js_sandbox: "${ACTORS_RULE_JS_SANDBOX_USE_JS_SANDBOX:true}"
+ # Specify thread pool size for JavaScript sandbox resource monitor
+ monitor_thread_pool_size: "${ACTORS_RULE_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}"
+ # Maximum CPU time in milliseconds allowed for script execution
+ max_cpu_time: "${ACTORS_RULE_JS_SANDBOX_MAX_CPU_TIME:100}"
+ # Maximum allowed JavaScript execution errors before JavaScript will be blacklisted
+ max_errors: "${ACTORS_RULE_JS_SANDBOX_MAX_ERRORS:3}"
+ chain:
+ # Errors for particular actor are persisted once per specified amount of milliseconds
+ error_persist_frequency: "${ACTORS_RULE_CHAIN_ERROR_FREQUENCY:3000}"
+ node:
+ # Errors for particular actor are persisted once per specified amount of milliseconds
+ error_persist_frequency: "${ACTORS_RULE_NODE_ERROR_FREQUENCY:3000}"
+ queue:
+ # Message queue type
+ type: "${ACTORS_RULE_QUEUE_TYPE:memory}"
+ # Message queue maximum size (per tenant)
+ max_size: "${ACTORS_RULE_QUEUE_MAX_SIZE:100}"
+ # Message queue cleanup period in seconds
+ cleanup_period: "${ACTORS_RULE_QUEUE_CLEANUP_PERIOD:3600}"
statistics:
# Enable/disable actor statistics
enabled: "${ACTORS_STATISTICS_ENABLED:true}"
persist_frequency: "${ACTORS_STATISTICS_PERSIST_FREQUENCY:3600000}"
+ queue:
+ # Enable/disable persistence of un-processed messages to the queue
+ enabled: "${ACTORS_QUEUE_ENABLED:true}"
+ # Maximum allowed timeout for persistence into the queue
+ timeout: "${ACTORS_QUEUE_PERSISTENCE_TIMEOUT:30000}"
+ client_side_rpc:
+ timeout: "${CLIENT_SIDE_RPC_TIMEOUT:60000}"
cache:
# caffeine or redis
@@ -283,17 +340,17 @@ spring:
database-platform: "${SPRING_JPA_DATABASE_PLATFORM:org.hibernate.dialect.HSQLDialect}"
datasource:
driverClassName: "${SPRING_DRIVER_CLASS_NAME:org.hsqldb.jdbc.JDBCDriver}"
- url: "${SPRING_DATASOURCE_URL:jdbc:hsqldb:file:${SQL_DATA_FOLDER:/tmp}/thingsboardDb;sql.enforce_size=false}"
+ url: "${SPRING_DATASOURCE_URL:jdbc:hsqldb:file:${SQL_DATA_FOLDER:/tmp}/thingsboardDb;sql.enforce_size=false;hsqldb.log_size=5}"
username: "${SPRING_DATASOURCE_USERNAME:sa}"
password: "${SPRING_DATASOURCE_PASSWORD:}"
# PostgreSQL DAO Configuration
#spring:
# data:
-# jpa:
+# sql:
# repositories:
# enabled: "true"
-# jpa:
+# sql:
# hibernate:
# ddl-auto: "validate"
# database-platform: "${SPRING_JPA_DATABASE_PLATFORM:org.hibernate.dialect.PostgreSQLDialect}"
@@ -320,8 +377,7 @@ audit_log:
"dashboard": "${AUDIT_LOG_MASK_DASHBOARD:W}"
"customer": "${AUDIT_LOG_MASK_CUSTOMER:W}"
"user": "${AUDIT_LOG_MASK_USER:W}"
- "rule": "${AUDIT_LOG_MASK_RULE:W}"
- "plugin": "${AUDIT_LOG_MASK_PLUGIN:W}"
+ "rule_chain": "${AUDIT_LOG_MASK_RULE_CHAIN:W}"
sink:
# Type of external sink. possible options: none, elasticsearch
type: "${AUDIT_LOG_SINK_TYPE:none}"
@@ -337,4 +393,11 @@ audit_log:
host: "${AUDIT_LOG_SINK_HOST:localhost}"
port: "${AUDIT_LOG_SINK_POST:9200}"
user_name: "${AUDIT_LOG_SINK_USER_NAME:}"
- password: "${AUDIT_LOG_SINK_PASSWORD:}"
\ No newline at end of file
+ password: "${AUDIT_LOG_SINK_PASSWORD:}"
+
+state:
+ defaultInactivityTimeoutInSec: 10
+ defaultStateCheckIntervalInSec: 10
+# TODO in v2.1
+# defaultStatePersistenceIntervalInSec: 60
+# defaultStatePersistencePack: 100
\ No newline at end of file
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
index b92e464..3ec4dc8 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
@@ -96,6 +96,8 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC
@Slf4j
public abstract class AbstractControllerTest {
+ protected ObjectMapper mapper = new ObjectMapper();
+
protected static final String TEST_TENANT_NAME = "TEST TENANT";
protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org";
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java
new file mode 100644
index 0000000..1b042a8
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.dao.queue.MsgQueue;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.service.queue.MsgQueueService;
+
+import java.io.IOException;
+import java.util.function.Predicate;
+
+/**
+ * Created by ashvayka on 20.03.18.
+ */
+public class AbstractRuleEngineControllerTest extends AbstractControllerTest {
+
+ @Autowired
+ protected RuleChainService ruleChainService;
+
+ @Autowired
+ protected MsgQueueService msgQueueService;
+
+ protected RuleChain saveRuleChain(RuleChain ruleChain) throws Exception {
+ return doPost("/api/ruleChain", ruleChain, RuleChain.class);
+ }
+
+ protected RuleChain getRuleChain(RuleChainId ruleChainId) throws Exception {
+ return doGet("/api/ruleChain/" + ruleChainId.getId().toString(), RuleChain.class);
+ }
+
+ protected RuleChainMetaData saveRuleChainMetaData(RuleChainMetaData ruleChainMD) throws Exception {
+ return doPost("/api/ruleChain/metadata", ruleChainMD, RuleChainMetaData.class);
+ }
+
+ protected RuleChainMetaData getRuleChainMetaData(RuleChainId ruleChainId) throws Exception {
+ return doGet("/api/ruleChain/metadata/" + ruleChainId.getId().toString(), RuleChainMetaData.class);
+ }
+
+ protected TimePageData<Event> getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception {
+ TimePageLink pageLink = new TimePageLink(limit);
+ return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&",
+ new TypeReference<TimePageData<Event>>() {
+ }, pageLink, entityId.getEntityType(), entityId.getId(), DataConstants.DEBUG_RULE_NODE, tenantId.getId());
+ }
+
+ protected JsonNode getMetadata(Event outEvent) {
+ String metaDataStr = outEvent.getBody().get("metadata").asText();
+ try {
+ return mapper.readTree(metaDataStr);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected Predicate<Event> filterByCustomEvent() {
+ return event -> event.getBody().get("msgType").textValue().equals("CUSTOM");
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseComponentDescriptorControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseComponentDescriptorControllerTest.java
index 4346538..62f7562 100644
--- a/application/src/test/java/org/thingsboard/server/controller/BaseComponentDescriptorControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/BaseComponentDescriptorControllerTest.java
@@ -20,14 +20,13 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
+import org.thingsboard.rule.engine.filter.TbJsFilterNode;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentScope;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.security.Authority;
-import org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction;
-import org.thingsboard.server.extensions.core.plugin.telemetry.TelemetryStoragePlugin;
import java.util.List;
@@ -35,7 +34,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
public abstract class BaseComponentDescriptorControllerTest extends AbstractControllerTest {
- private static final int AMOUNT_OF_DEFAULT_PLUGINS_DESCRIPTORS = 5;
+ private static final int AMOUNT_OF_DEFAULT_FILTER_NODES = 4;
private Tenant savedTenant;
private User tenantAdmin;
@@ -69,38 +68,28 @@ public abstract class BaseComponentDescriptorControllerTest extends AbstractCont
@Test
public void testGetByClazz() throws Exception {
ComponentDescriptor descriptor =
- doGet("/api/component/" + TelemetryStoragePlugin.class.getName(), ComponentDescriptor.class);
+ doGet("/api/component/" + TbJsFilterNode.class.getName(), ComponentDescriptor.class);
Assert.assertNotNull(descriptor);
Assert.assertNotNull(descriptor.getId());
Assert.assertNotNull(descriptor.getName());
Assert.assertEquals(ComponentScope.TENANT, descriptor.getScope());
- Assert.assertEquals(ComponentType.PLUGIN, descriptor.getType());
+ Assert.assertEquals(ComponentType.FILTER, descriptor.getType());
Assert.assertEquals(descriptor.getClazz(), descriptor.getClazz());
}
@Test
public void testGetByType() throws Exception {
List<ComponentDescriptor> descriptors = readResponse(
- doGet("/api/components/" + ComponentType.PLUGIN).andExpect(status().isOk()), new TypeReference<List<ComponentDescriptor>>() {
+ doGet("/api/components/" + ComponentType.FILTER).andExpect(status().isOk()), new TypeReference<List<ComponentDescriptor>>() {
});
Assert.assertNotNull(descriptors);
- Assert.assertEquals(AMOUNT_OF_DEFAULT_PLUGINS_DESCRIPTORS, descriptors.size());
+ Assert.assertTrue(descriptors.size() >= AMOUNT_OF_DEFAULT_FILTER_NODES);
for (ComponentType type : ComponentType.values()) {
doGet("/api/components/" + type).andExpect(status().isOk());
}
}
- @Test
- public void testGetActionsByType() throws Exception {
- List<ComponentDescriptor> descriptors = readResponse(
- doGet("/api/components/actions/" + TelemetryStoragePlugin.class.getName()).andExpect(status().isOk()), new TypeReference<List<ComponentDescriptor>>() {
- });
-
- Assert.assertNotNull(descriptors);
- Assert.assertEquals(1, descriptors.size());
- Assert.assertEquals(TelemetryPluginAction.class.getName(), descriptors.get(0).getClazz());
- }
}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java
index b732e66..f3e29dc 100644
--- a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java
@@ -15,23 +15,27 @@
*/
package org.thingsboard.server.mqtt.rpc;
-import java.util.Arrays;
-
import com.datastax.driver.core.utils.UUIDs;
-import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
-import org.eclipse.paho.client.mqttv3.*;
-import org.junit.*;
-import org.thingsboard.server.actors.plugin.PluginProcessingContext;
+import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
+import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
+import org.eclipse.paho.client.mqttv3.MqttCallback;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
-import org.thingsboard.server.common.data.page.TextPageData;
-import org.thingsboard.server.common.data.plugin.PluginMetaData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.controller.AbstractControllerTest;
+import org.thingsboard.server.service.security.AccessValidator;
+
+import java.util.Arrays;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -55,7 +59,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
public void beforeTest() throws Exception {
loginSysAdmin();
- asyncContextTimeoutToUseRpcPlugin = getAsyncContextTimeoutToUseRpcPlugin();
+ asyncContextTimeoutToUseRpcPlugin = 10000L;
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
@@ -131,7 +135,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
String result = doPostAsync("/api/plugins/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class,
status().isNotFound());
- Assert.assertEquals(PluginProcessingContext.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
+ Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
}
@Test
@@ -186,7 +190,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
String result = doPostAsync("/api/plugins/rpc/twoway/" + nonExistentDeviceId, setGpioRequest, String.class,
status().isNotFound());
- Assert.assertEquals(PluginProcessingContext.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
+ Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
}
private Device getSavedDevice(Device device) throws Exception {
@@ -197,13 +201,6 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
return doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
}
- private Long getAsyncContextTimeoutToUseRpcPlugin() throws Exception {
- TextPageData<PluginMetaData> plugins = doGetTyped("/api/plugin/system?limit=1&textSearch=system rpc plugin",
- new TypeReference<TextPageData<PluginMetaData>>(){});
- Long systemRpcPluginTimeout = plugins.getData().iterator().next().getConfiguration().get("defaultTimeout").asLong();
- return systemRpcPluginTimeout + TIME_TO_HANDLE_REQUEST;
- }
-
private static class TestMqttCallback implements MqttCallback {
private final MqttAsyncClient client;
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java
index 7c9c058..f3d69ba 100644
--- a/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java
@@ -15,7 +15,6 @@
*/
package org.thingsboard.server.mqtt.rpc.sql;
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcIntegrationTest;
diff --git a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
new file mode 100644
index 0000000..c86d496
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
@@ -0,0 +1,322 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.rules.flow;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.collect.Lists;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.*;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import org.thingsboard.server.controller.AbstractRuleEngineControllerTest;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author Valerii Sosliuk
+ */
+@Slf4j
+public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRuleEngineControllerTest {
+
+ protected Tenant savedTenant;
+ protected User tenantAdmin;
+
+ @Autowired
+ protected ActorService actorService;
+
+ @Autowired
+ protected AttributesService attributesService;
+
+ @Before
+ public void beforeTest() throws Exception {
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+ ruleChainService.deleteRuleChainsByTenantId(savedTenant.getId());
+
+ tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(savedTenant.getId());
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ createUserAndLogin(tenantAdmin, "testPassword1");
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ loginSysAdmin();
+ if (savedTenant != null) {
+ doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk());
+ }
+ }
+
+ @Test
+ public void testRuleChainWithTwoRules() throws Exception {
+ // Creating Rule Chain
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setName("Simple Rule Chain");
+ ruleChain.setTenantId(savedTenant.getId());
+ ruleChain.setRoot(true);
+ ruleChain.setDebugMode(true);
+ ruleChain = saveRuleChain(ruleChain);
+ Assert.assertNull(ruleChain.getFirstRuleNodeId());
+
+ RuleChainMetaData metaData = new RuleChainMetaData();
+ metaData.setRuleChainId(ruleChain.getId());
+
+ RuleNode ruleNode1 = new RuleNode();
+ ruleNode1.setName("Simple Rule Node 1");
+ ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+ ruleNode1.setDebugMode(true);
+ TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
+ configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
+ ruleNode1.setConfiguration(mapper.valueToTree(configuration1));
+
+ RuleNode ruleNode2 = new RuleNode();
+ ruleNode2.setName("Simple Rule Node 2");
+ ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+ ruleNode2.setDebugMode(true);
+ TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
+ configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
+ ruleNode2.setConfiguration(mapper.valueToTree(configuration2));
+
+
+ metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2));
+ metaData.setFirstNodeIndex(0);
+ metaData.addConnectionInfo(0, 1, "Success");
+ metaData = saveRuleChainMetaData(metaData);
+ Assert.assertNotNull(metaData);
+
+ ruleChain = getRuleChain(ruleChain.getId());
+ Assert.assertNotNull(ruleChain.getFirstRuleNodeId());
+
+ // Saving the device
+ Device device = new Device();
+ device.setName("My device");
+ device.setType("default");
+ device = doPost("/api/device", device, Device.class);
+
+ attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+ Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey1", "serverAttributeValue1"), System.currentTimeMillis())));
+ attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+ Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey2", "serverAttributeValue2"), System.currentTimeMillis())));
+
+
+ Thread.sleep(1000);
+
+ // Pushing Message to the system
+ TbMsg tbMsg = new TbMsg(UUIDs.timeBased(),
+ "CUSTOM",
+ device.getId(),
+ new TbMsgMetaData(),
+ "{}", null, null, 0L);
+ actorService.onMsg(new ServiceToRuleEngineMsg(savedTenant.getId(), tbMsg));
+
+ Thread.sleep(3000);
+
+ TimePageData<Event> eventsPage = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000);
+ List<Event> events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList());
+ Assert.assertEquals(2, events.size());
+
+ Event inEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+ Assert.assertEquals(ruleChain.getFirstRuleNodeId(), inEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+ Event outEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+ Assert.assertEquals(ruleChain.getFirstRuleNodeId(), outEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+ Assert.assertEquals("serverAttributeValue1", getMetadata(outEvent).get("ss_serverAttributeKey1").asText());
+
+ RuleChain finalRuleChain = ruleChain;
+ RuleNode lastRuleNode = metaData.getNodes().stream().filter(node -> !node.getId().equals(finalRuleChain.getFirstRuleNodeId())).findFirst().get();
+
+ eventsPage = getDebugEvents(savedTenant.getId(), lastRuleNode.getId(), 1000);
+ events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList());
+
+ Assert.assertEquals(2, events.size());
+
+ inEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+ Assert.assertEquals(lastRuleNode.getId(), inEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+ outEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+ Assert.assertEquals(lastRuleNode.getId(), outEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+ Assert.assertEquals("serverAttributeValue1", getMetadata(outEvent).get("ss_serverAttributeKey1").asText());
+ Assert.assertEquals("serverAttributeValue2", getMetadata(outEvent).get("ss_serverAttributeKey2").asText());
+
+ List<TbMsg> unAckMsgList = Lists.newArrayList(msgQueueService.findUnprocessed(savedTenant.getId(), ruleChain.getId().getId(), 0L));
+ Assert.assertEquals(0, unAckMsgList.size());
+ }
+
+ @Test
+ public void testTwoRuleChainsWithTwoRules() throws Exception {
+ // Creating Rule Chain
+ RuleChain rootRuleChain = new RuleChain();
+ rootRuleChain.setName("Root Rule Chain");
+ rootRuleChain.setTenantId(savedTenant.getId());
+ rootRuleChain.setRoot(true);
+ rootRuleChain.setDebugMode(true);
+ rootRuleChain = saveRuleChain(rootRuleChain);
+ Assert.assertNull(rootRuleChain.getFirstRuleNodeId());
+
+ // Creating Rule Chain
+ RuleChain secondaryRuleChain = new RuleChain();
+ secondaryRuleChain.setName("Secondary Rule Chain");
+ secondaryRuleChain.setTenantId(savedTenant.getId());
+ secondaryRuleChain.setRoot(false);
+ secondaryRuleChain.setDebugMode(true);
+ secondaryRuleChain = saveRuleChain(secondaryRuleChain);
+ Assert.assertNull(secondaryRuleChain.getFirstRuleNodeId());
+
+ RuleChainMetaData rootMetaData = new RuleChainMetaData();
+ rootMetaData.setRuleChainId(rootRuleChain.getId());
+
+ RuleNode ruleNode1 = new RuleNode();
+ ruleNode1.setName("Simple Rule Node 1");
+ ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+ ruleNode1.setDebugMode(true);
+ TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
+ configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
+ ruleNode1.setConfiguration(mapper.valueToTree(configuration1));
+
+ rootMetaData.setNodes(Collections.singletonList(ruleNode1));
+ rootMetaData.setFirstNodeIndex(0);
+ rootMetaData.addRuleChainConnectionInfo(0, secondaryRuleChain.getId(), "Success", mapper.createObjectNode());
+ rootMetaData = saveRuleChainMetaData(rootMetaData);
+ Assert.assertNotNull(rootMetaData);
+
+ rootRuleChain = getRuleChain(rootRuleChain.getId());
+ Assert.assertNotNull(rootRuleChain.getFirstRuleNodeId());
+
+
+ RuleChainMetaData secondaryMetaData = new RuleChainMetaData();
+ secondaryMetaData.setRuleChainId(secondaryRuleChain.getId());
+
+ RuleNode ruleNode2 = new RuleNode();
+ ruleNode2.setName("Simple Rule Node 2");
+ ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+ ruleNode2.setDebugMode(true);
+ TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
+ configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
+ ruleNode2.setConfiguration(mapper.valueToTree(configuration2));
+
+ secondaryMetaData.setNodes(Collections.singletonList(ruleNode2));
+ secondaryMetaData.setFirstNodeIndex(0);
+ secondaryMetaData = saveRuleChainMetaData(secondaryMetaData);
+ Assert.assertNotNull(secondaryMetaData);
+
+ // Saving the device
+ Device device = new Device();
+ device.setName("My device");
+ device.setType("default");
+ device = doPost("/api/device", device, Device.class);
+
+ attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+ Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey1", "serverAttributeValue1"), System.currentTimeMillis())));
+ attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+ Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey2", "serverAttributeValue2"), System.currentTimeMillis())));
+
+
+ Thread.sleep(1000);
+
+ // Pushing Message to the system
+ TbMsg tbMsg = new TbMsg(UUIDs.timeBased(),
+ "CUSTOM",
+ device.getId(),
+ new TbMsgMetaData(),
+ "{}", null, null, 0L);
+ actorService.onMsg(new ServiceToRuleEngineMsg(savedTenant.getId(), tbMsg));
+
+ Thread.sleep(3000);
+
+ TimePageData<Event> eventsPage = getDebugEvents(savedTenant.getId(), rootRuleChain.getFirstRuleNodeId(), 1000);
+ List<Event> events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList());
+
+ Assert.assertEquals(2, events.size());
+
+ Event inEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+ Assert.assertEquals(rootRuleChain.getFirstRuleNodeId(), inEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+ Event outEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+ Assert.assertEquals(rootRuleChain.getFirstRuleNodeId(), outEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+ Assert.assertEquals("serverAttributeValue1", getMetadata(outEvent).get("ss_serverAttributeKey1").asText());
+
+ RuleChain finalRuleChain = rootRuleChain;
+ RuleNode lastRuleNode = secondaryMetaData.getNodes().stream().filter(node -> !node.getId().equals(finalRuleChain.getFirstRuleNodeId())).findFirst().get();
+
+ eventsPage = getDebugEvents(savedTenant.getId(), lastRuleNode.getId(), 1000);
+ events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList());
+
+
+ Assert.assertEquals(2, events.size());
+
+ inEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+ Assert.assertEquals(lastRuleNode.getId(), inEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+ outEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+ Assert.assertEquals(lastRuleNode.getId(), outEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+ Assert.assertEquals("serverAttributeValue1", getMetadata(outEvent).get("ss_serverAttributeKey1").asText());
+ Assert.assertEquals("serverAttributeValue2", getMetadata(outEvent).get("ss_serverAttributeKey2").asText());
+
+ List<TbMsg> unAckMsgList = Lists.newArrayList(msgQueueService.findUnprocessed(savedTenant.getId(), rootRuleChain.getId().getId(), 0L));
+ Assert.assertEquals(0, unAckMsgList.size());
+
+ unAckMsgList = Lists.newArrayList(msgQueueService.findUnprocessed(savedTenant.getId(), secondaryRuleChain.getId().getId(), 0L));
+ Assert.assertEquals(0, unAckMsgList.size());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
new file mode 100644
index 0000000..7ac0789
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
@@ -0,0 +1,234 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.rules.lifecycle;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import org.thingsboard.server.controller.AbstractRuleEngineControllerTest;
+import org.thingsboard.server.dao.attributes.AttributesService;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author Valerii Sosliuk
+ */
+@Slf4j
+public abstract class AbstractRuleEngineLifecycleIntegrationTest extends AbstractRuleEngineControllerTest {
+
+ protected Tenant savedTenant;
+ protected User tenantAdmin;
+
+ @Autowired
+ protected ActorService actorService;
+
+ @Autowired
+ protected AttributesService attributesService;
+
+ @Before
+ public void beforeTest() throws Exception {
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+ ruleChainService.deleteRuleChainsByTenantId(savedTenant.getId());
+
+ tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(savedTenant.getId());
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ createUserAndLogin(tenantAdmin, "testPassword1");
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ loginSysAdmin();
+ if (savedTenant != null) {
+ doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk());
+ }
+ }
+
+ @Test
+ public void testRuleChainWithOneRule() throws Exception {
+ // Creating Rule Chain
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setName("Simple Rule Chain");
+ ruleChain.setTenantId(savedTenant.getId());
+ ruleChain.setRoot(true);
+ ruleChain.setDebugMode(true);
+ ruleChain = saveRuleChain(ruleChain);
+ Assert.assertNull(ruleChain.getFirstRuleNodeId());
+
+ RuleChainMetaData metaData = new RuleChainMetaData();
+ metaData.setRuleChainId(ruleChain.getId());
+
+ RuleNode ruleNode = new RuleNode();
+ ruleNode.setName("Simple Rule Node");
+ ruleNode.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+ ruleNode.setDebugMode(true);
+ TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
+ configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey"));
+ ruleNode.setConfiguration(mapper.valueToTree(configuration));
+
+ metaData.setNodes(Collections.singletonList(ruleNode));
+ metaData.setFirstNodeIndex(0);
+
+ metaData = saveRuleChainMetaData(metaData);
+ Assert.assertNotNull(metaData);
+
+ ruleChain = getRuleChain(ruleChain.getId());
+ Assert.assertNotNull(ruleChain.getFirstRuleNodeId());
+
+ // Saving the device
+ Device device = new Device();
+ device.setName("My device");
+ device.setType("default");
+ device = doPost("/api/device", device, Device.class);
+
+ attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+ Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey", "serverAttributeValue"), System.currentTimeMillis())));
+
+ Thread.sleep(1000);
+
+ // Pushing Message to the system
+ TbMsg tbMsg = new TbMsg(UUIDs.timeBased(),
+ "CUSTOM",
+ device.getId(),
+ new TbMsgMetaData(),
+ "{}",
+ null, null, 0L);
+ actorService.onMsg(new ServiceToRuleEngineMsg(savedTenant.getId(), tbMsg));
+
+ Thread.sleep(3000);
+
+ TimePageData<Event> eventsPage = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000);
+ List<Event> events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList());
+
+ Assert.assertEquals(2, events.size());
+
+ Event inEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+ Assert.assertEquals(ruleChain.getFirstRuleNodeId(), inEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+ Event outEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+ Assert.assertEquals(ruleChain.getFirstRuleNodeId(), outEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+ Assert.assertEquals("serverAttributeValue", getMetadata(outEvent).get("ss_serverAttributeKey").asText());
+ }
+
+ @Test
+ public void testRuleChainWithOneRuleAndMsgFromQueue() throws Exception {
+ // Creating Rule Chain
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setName("Simple Rule Chain");
+ ruleChain.setTenantId(savedTenant.getId());
+ ruleChain.setRoot(true);
+ ruleChain.setDebugMode(true);
+ ruleChain = saveRuleChain(ruleChain);
+ Assert.assertNull(ruleChain.getFirstRuleNodeId());
+
+ // Saving the device
+ Device device = new Device();
+ device.setName("My device");
+ device.setType("default");
+ device = doPost("/api/device", device, Device.class);
+
+ attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+ Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey", "serverAttributeValue"), System.currentTimeMillis())));
+
+ // Pushing Message to the system
+ TbMsg tbMsg = new TbMsg(UUIDs.timeBased(),
+ "CUSTOM",
+ device.getId(),
+ new TbMsgMetaData(),
+ "{}",
+ ruleChain.getId(), null, 0L);
+ msgQueueService.put(device.getTenantId(), tbMsg, ruleChain.getId().getId(), 0L);
+
+ Thread.sleep(1000);
+
+ RuleChainMetaData metaData = new RuleChainMetaData();
+ metaData.setRuleChainId(ruleChain.getId());
+
+ RuleNode ruleNode = new RuleNode();
+ ruleNode.setName("Simple Rule Node");
+ ruleNode.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+ ruleNode.setDebugMode(true);
+ TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
+ configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey"));
+ ruleNode.setConfiguration(mapper.valueToTree(configuration));
+
+ metaData.setNodes(Collections.singletonList(ruleNode));
+ metaData.setFirstNodeIndex(0);
+
+ metaData = saveRuleChainMetaData(metaData);
+ Assert.assertNotNull(metaData);
+
+ ruleChain = getRuleChain(ruleChain.getId());
+ Assert.assertNotNull(ruleChain.getFirstRuleNodeId());
+
+ Thread.sleep(3000);
+
+ TimePageData<Event> eventsPage = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000);
+ List<Event> events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList());
+
+ Assert.assertEquals(2, events.size());
+
+ Event inEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+ Assert.assertEquals(ruleChain.getFirstRuleNodeId(), inEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+ Event outEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+ Assert.assertEquals(ruleChain.getFirstRuleNodeId(), outEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+ Assert.assertEquals("serverAttributeValue", getMetadata(outEvent).get("ss_serverAttributeKey").asText());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/rules/RuleEngineNoSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/rules/RuleEngineNoSqlTestSuite.java
new file mode 100644
index 0000000..bffe491
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/rules/RuleEngineNoSqlTestSuite.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.rules;
+
+import org.cassandraunit.dataset.cql.ClassPathCQLDataSet;
+import org.junit.ClassRule;
+import org.junit.extensions.cpsuite.ClasspathSuite;
+import org.junit.runner.RunWith;
+import org.thingsboard.server.dao.CustomCassandraCQLUnit;
+import org.thingsboard.server.dao.CustomSqlUnit;
+
+import java.util.Arrays;
+
+@RunWith(ClasspathSuite.class)
+@ClasspathSuite.ClassnameFilters({
+ "org.thingsboard.server.rules.flow.nosql.*Test",
+ "org.thingsboard.server.rules.lifecycle.nosql.*Test"
+})
+public class RuleEngineNoSqlTestSuite {
+
+ @ClassRule
+ public static CustomCassandraCQLUnit cassandraUnit =
+ new CustomCassandraCQLUnit(
+ Arrays.asList(
+ new ClassPathCQLDataSet("cassandra/schema.cql", false, false),
+ new ClassPathCQLDataSet("cassandra/system-data.cql", false, false)),
+ "cassandra-test.yaml", 30000l);
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java
new file mode 100644
index 0000000..7b13e2f
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.rules;
+
+import org.junit.ClassRule;
+import org.junit.extensions.cpsuite.ClasspathSuite;
+import org.junit.runner.RunWith;
+import org.thingsboard.server.dao.CustomSqlUnit;
+
+import java.util.Arrays;
+
+@RunWith(ClasspathSuite.class)
+@ClasspathSuite.ClassnameFilters({
+ "org.thingsboard.server.rules.flow.sql.*Test",
+ "org.thingsboard.server.rules.lifecycle.sql.*Test"})
+public class RuleEngineSqlTestSuite {
+
+ @ClassRule
+ public static CustomSqlUnit sqlUnit = new CustomSqlUnit(
+ Arrays.asList("sql/schema.sql", "sql/system-data.sql"),
+ "sql/drop-all-tables.sql",
+ "sql-test.properties");
+}
diff --git a/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java b/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
index ed3750d..ba2bb65 100644
--- a/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
+++ b/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
@@ -22,7 +22,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.rule.engine.api.MailService;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
@Profile("test")
@Configuration
diff --git a/application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java b/application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java
new file mode 100644
index 0000000..ea70384
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java
@@ -0,0 +1,171 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.script;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.collect.Sets;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.rule.engine.api.ScriptEngine;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+public class RuleNodeJsScriptEngineTest {
+
+ private ScriptEngine scriptEngine;
+ private TestNashornJsSandboxService jsSandboxService;
+
+ @Before
+ public void beforeTest() throws Exception {
+ jsSandboxService = new TestNashornJsSandboxService(false, 1, 100, 3);
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ jsSandboxService.stop();
+ }
+
+ @Test
+ public void msgCanBeUpdated() throws ScriptException {
+ String function = "metadata.temp = metadata.temp * 10; return {metadata: metadata};";
+ scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
+
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "7");
+ metaData.putValue("humidity", "99");
+ String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
+
+ TbMsg actual = scriptEngine.executeUpdate(msg);
+ assertEquals("70", actual.getMetaData().getValue("temp"));
+ scriptEngine.destroy();
+ }
+
+ @Test
+ public void newAttributesCanBeAddedInMsg() throws ScriptException {
+ String function = "metadata.newAttr = metadata.humidity - msg.passed; return {metadata: metadata};";
+ scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "7");
+ metaData.putValue("humidity", "99");
+ String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
+
+ TbMsg actual = scriptEngine.executeUpdate(msg);
+ assertEquals("94", actual.getMetaData().getValue("newAttr"));
+ scriptEngine.destroy();
+ }
+
+ @Test
+ public void payloadCanBeUpdated() throws ScriptException {
+ String function = "msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine'; return {msg: msg};";
+ scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "7");
+ metaData.putValue("humidity", "99");
+ String rawJson = "{\"name\":\"Vit\",\"passed\": 5,\"bigObj\":{\"prop\":42}}";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
+
+ TbMsg actual = scriptEngine.executeUpdate(msg);
+
+ String expectedJson = "{\"name\":\"Vit\",\"passed\":35,\"bigObj\":{\"prop\":42,\"newProp\":\"Ukraine\"}}";
+ assertEquals(expectedJson, actual.getData());
+ scriptEngine.destroy();
+ }
+
+ @Test
+ public void metadataAccessibleForFilter() throws ScriptException {
+ String function = "return metadata.humidity < 15;";
+ scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "7");
+ metaData.putValue("humidity", "99");
+ String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
+ assertFalse(scriptEngine.executeFilter(msg));
+ scriptEngine.destroy();
+ }
+
+ @Test
+ public void dataAccessibleForFilter() throws ScriptException {
+ String function = "return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 7 && msg.bigObj.prop == 42;";
+ scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "7");
+ metaData.putValue("humidity", "99");
+ String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
+ assertTrue(scriptEngine.executeFilter(msg));
+ scriptEngine.destroy();
+ }
+
+ @Test
+ public void dataAccessibleForSwitch() throws ScriptException {
+ String jsCode = "function nextRelation(metadata, msg) {\n" +
+ " if(msg.passed == 5 && metadata.temp == 10)\n" +
+ " return 'one'\n" +
+ " else\n" +
+ " return 'two';\n" +
+ "};\n" +
+ "\n" +
+ "return nextRelation(metadata, msg);";
+ scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode);
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "10");
+ metaData.putValue("humidity", "99");
+ String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
+ Set<String> actual = scriptEngine.executeSwitch(msg);
+ assertEquals(Sets.newHashSet("one"), actual);
+ scriptEngine.destroy();
+ }
+
+ @Test
+ public void multipleRelationsReturnedFromSwitch() throws ScriptException {
+ String jsCode = "function nextRelation(metadata, msg) {\n" +
+ " if(msg.passed == 5 && metadata.temp == 10)\n" +
+ " return ['three', 'one']\n" +
+ " else\n" +
+ " return 'two';\n" +
+ "};\n" +
+ "\n" +
+ "return nextRelation(metadata, msg);";
+ scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode);
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "10");
+ metaData.putValue("humidity", "99");
+ String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
+ Set<String> actual = scriptEngine.executeSwitch(msg);
+ assertEquals(Sets.newHashSet("one", "three"), actual);
+ scriptEngine.destroy();
+ }
+
+}
\ No newline at end of file
diff --git a/application/src/test/java/org/thingsboard/server/service/script/TestNashornJsSandboxService.java b/application/src/test/java/org/thingsboard/server/service/script/TestNashornJsSandboxService.java
new file mode 100644
index 0000000..731c4bb
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/script/TestNashornJsSandboxService.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.thingsboard.server.service.script;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import delight.nashornsandbox.NashornSandbox;
+import delight.nashornsandbox.NashornSandboxes;
+
+import javax.script.ScriptException;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class TestNashornJsSandboxService extends AbstractNashornJsSandboxService {
+
+ private boolean useJsSandbox;
+ private final int monitorThreadPoolSize;
+ private final long maxCpuTime;
+ private final int maxErrors;
+
+ public TestNashornJsSandboxService(boolean useJsSandbox, int monitorThreadPoolSize, long maxCpuTime, int maxErrors) {
+ this.useJsSandbox = useJsSandbox;
+ this.monitorThreadPoolSize = monitorThreadPoolSize;
+ this.maxCpuTime = maxCpuTime;
+ this.maxErrors = maxErrors;
+ init();
+ }
+
+ @Override
+ protected boolean useJsSandbox() {
+ return useJsSandbox;
+ }
+
+ @Override
+ protected int getMonitorThreadPoolSize() {
+ return monitorThreadPoolSize;
+ }
+
+ @Override
+ protected long getMaxCpuTime() {
+ return maxCpuTime;
+ }
+
+ @Override
+ protected int getMaxErrors() {
+ return maxErrors;
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java
index cfa0c58..97c6749 100644
--- a/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java
+++ b/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java
@@ -35,5 +35,4 @@ public class SystemSqlTestSuite {
"sql/drop-all-tables.sql",
"sql-test.properties");
-
}
common/data/pom.xml 2(+1 -1)
diff --git a/common/data/pom.xml b/common/data/pom.xml
index 577fcff..793d889 100644
--- a/common/data/pom.xml
+++ b/common/data/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.4.1-SNAPSHOT</version>
+ <version>2.0.0-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
index 70f5042..125406c 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
@@ -22,6 +22,7 @@ import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasName;
+import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -31,7 +32,7 @@ import org.thingsboard.server.common.data.id.TenantId;
@Data
@Builder
@AllArgsConstructor
-public class Alarm extends BaseData<AlarmId> implements HasName {
+public class Alarm extends BaseData<AlarmId> implements HasName, HasTenantId {
private TenantId tenantId;
private String type;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
index cc3c111..c7b246c 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
@@ -17,16 +17,13 @@ package org.thingsboard.server.common.data.asset;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.EqualsAndHashCode;
-import org.thingsboard.server.common.data.HasAdditionalInfo;
-import org.thingsboard.server.common.data.HasName;
-import org.thingsboard.server.common.data.SearchTextBased;
-import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
+import org.thingsboard.server.common.data.*;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
@EqualsAndHashCode(callSuper = true)
-public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements HasName {
+public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements HasName, HasTenantId, HasCustomerId {
private static final long serialVersionUID = 2807343040519543363L;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
index 03115a9..078c97b 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
@@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import com.fasterxml.jackson.databind.JsonNode;
-public class Customer extends ContactBased<CustomerId> implements HasName {
+public class Customer extends ContactBased<CustomerId> implements HasName, HasTenantId {
private static final long serialVersionUID = -1599722990298929275L;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
index a776d7b..6b1c4a4 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
@@ -37,7 +37,25 @@ public class DataConstants {
public static final String ERROR = "ERROR";
public static final String LC_EVENT = "LC_EVENT";
public static final String STATS = "STATS";
+ public static final String DEBUG_RULE_NODE = "DEBUG_RULE_NODE";
public static final String ONEWAY = "ONEWAY";
public static final String TWOWAY = "TWOWAY";
+
+ public static final String IN = "IN";
+ public static final String OUT = "OUT";
+
+ public static final String INACTIVITY_EVENT = "INACTIVITY_EVENT";
+ public static final String CONNECT_EVENT = "CONNECT_EVENT";
+ public static final String DISCONNECT_EVENT = "DISCONNECT_EVENT";
+ public static final String ACTIVITY_EVENT = "ACTIVITY_EVENT";
+
+ public static final String ENTITY_CREATED = "ENTITY_CREATED";
+ public static final String ENTITY_UPDATED = "ENTITY_UPDATED";
+ public static final String ENTITY_DELETED = "ENTITY_DELETED";
+ public static final String ENTITY_ASSIGNED = "ENTITY_ASSIGNED";
+ public static final String ENTITY_UNASSIGNED = "ENTITY_UNASSIGNED";
+ public static final String ATTRIBUTES_UPDATED = "ATTRIBUTES_UPDATED";
+ public static final String ATTRIBUTES_DELETED = "ATTRIBUTES_DELETED";
+
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
index 13fa011..95662c1 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
@@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import com.fasterxml.jackson.databind.JsonNode;
@EqualsAndHashCode(callSuper = true)
-public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implements HasName {
+public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implements HasName, HasTenantId, HasCustomerId {
private static final long serialVersionUID = 2807343040519543363L;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
index 05b558e..fe9c018 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
@@ -19,5 +19,5 @@ package org.thingsboard.server.common.data;
* @author Andrew Shvayka
*/
public enum EntityType {
- TENANT, CUSTOMER, USER, RULE, PLUGIN, DASHBOARD, ASSET, DEVICE, ALARM
+ TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
index e45cb91..0ecc7c6 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
@@ -33,6 +33,10 @@ public class EntityIdFactory {
return getByTypeAndUuid(EntityType.valueOf(type), uuid);
}
+ public static EntityId getByTypeAndUuid(EntityType type, String uuid) {
+ return getByTypeAndUuid(type, UUID.fromString(uuid));
+ }
+
public static EntityId getByTypeAndUuid(EntityType type, UUID uuid) {
switch (type) {
case TENANT:
@@ -41,10 +45,6 @@ public class EntityIdFactory {
return new CustomerId(uuid);
case USER:
return new UserId(uuid);
- case RULE:
- return new RuleId(uuid);
- case PLUGIN:
- return new PluginId(uuid);
case DASHBOARD:
return new DashboardId(uuid);
case DEVICE:
@@ -53,6 +53,10 @@ public class EntityIdFactory {
return new AssetId(uuid);
case ALARM:
return new AlarmId(uuid);
+ case RULE_CHAIN:
+ return new RuleChainId(uuid);
+ case RULE_NODE:
+ return new RuleNodeId(uuid);
}
throw new IllegalArgumentException("EntityType " + type + " is not supported!");
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java
index 6ac5136..e832807 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java
@@ -17,9 +17,10 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonIgnore;
+import java.io.Serializable;
import java.util.UUID;
-public abstract class IdBased<I extends UUIDBased> {
+public abstract class IdBased<I extends UUIDBased> implements Serializable {
protected I id;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java
index 34f8c3a..ffd7822 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java
@@ -20,6 +20,7 @@ import java.util.List;
import java.util.NoSuchElementException;
import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UUIDBased;
public class PageDataIterable<T extends SearchTextBased<? extends UUIDBased>> implements Iterable<T>, Iterator<T> {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java
index 45fb590..1335147 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java
@@ -20,6 +20,6 @@ package org.thingsboard.server.common.data.plugin;
*/
public enum ComponentType {
- FILTER, PROCESSOR, ACTION, PLUGIN
+ ENRICHMENT, FILTER, TRANSFORMATION, ACTION, EXTERNAL
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java
index 90e0253..5599055 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java
@@ -19,6 +19,8 @@ public enum RelationTypeGroup {
COMMON,
ALARM,
- DASHBOARD
+ DASHBOARD,
+ RULE_CHAIN,
+ RULE_NODE
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java
new file mode 100644
index 0000000..7ecd6df
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.rule;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.RuleChainId;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by igor on 3/13/18.
+ */
+@Data
+public class RuleChainMetaData {
+
+ private RuleChainId ruleChainId;
+
+ private Integer firstNodeIndex;
+
+ private List<RuleNode> nodes;
+
+ private List<NodeConnectionInfo> connections;
+
+ private List<RuleChainConnectionInfo> ruleChainConnections;
+
+ public void addConnectionInfo(int fromIndex, int toIndex, String type) {
+ NodeConnectionInfo connectionInfo = new NodeConnectionInfo();
+ connectionInfo.setFromIndex(fromIndex);
+ connectionInfo.setToIndex(toIndex);
+ connectionInfo.setType(type);
+ if (connections == null) {
+ connections = new ArrayList<>();
+ }
+ connections.add(connectionInfo);
+ }
+ public void addRuleChainConnectionInfo(int fromIndex, RuleChainId targetRuleChainId, String type, JsonNode additionalInfo) {
+ RuleChainConnectionInfo connectionInfo = new RuleChainConnectionInfo();
+ connectionInfo.setFromIndex(fromIndex);
+ connectionInfo.setTargetRuleChainId(targetRuleChainId);
+ connectionInfo.setType(type);
+ connectionInfo.setAdditionalInfo(additionalInfo);
+ if (ruleChainConnections == null) {
+ ruleChainConnections = new ArrayList<>();
+ }
+ ruleChainConnections.add(connectionInfo);
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/User.java b/common/data/src/main/java/org/thingsboard/server/common/data/User.java
index c893d64..15c52dc 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/User.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/User.java
@@ -15,9 +15,11 @@
*/
package org.thingsboard.server.common.data;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
@@ -25,7 +27,7 @@ import org.thingsboard.server.common.data.security.Authority;
import com.fasterxml.jackson.databind.JsonNode;
@EqualsAndHashCode(callSuper = true)
-public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements HasName {
+public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements HasName, HasTenantId, HasCustomerId {
private static final long serialVersionUID = 8250339805336035966L;
@@ -138,4 +140,18 @@ public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements H
return builder.toString();
}
+ @JsonIgnore
+ public boolean isSystemAdmin() {
+ return tenantId == null || EntityId.NULL_UUID.equals(tenantId.getId());
+ }
+
+ @JsonIgnore
+ public boolean isTenantAdmin() {
+ return !isSystemAdmin() && (customerId == null || EntityId.NULL_UUID.equals(customerId.getId()));
+ }
+
+ @JsonIgnore
+ public boolean isCustomerUser() {
+ return !isSystemAdmin() && !isTenantAdmin();
+ }
}
common/message/pom.xml 11(+10 -1)
diff --git a/common/message/pom.xml b/common/message/pom.xml
index 9e97d34..c24cfd3 100644
--- a/common/message/pom.xml
+++ b/common/message/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.4.1-SNAPSHOT</version>
+ <version>2.0.0-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>
@@ -57,6 +57,11 @@
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
+ <groupId>com.google.protobuf</groupId>
+ <artifactId>protobuf-java</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
@@ -70,6 +75,10 @@
<build>
<plugins>
+ <plugin>
+ <groupId>org.xolstice.maven.plugins</groupId>
+ <artifactId>protobuf-maven-plugin</artifactId>
+ </plugin>
</plugins>
</build>
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ClusterEventMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ClusterEventMsg.java
index 67f4de7..7d157f6 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ClusterEventMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ClusterEventMsg.java
@@ -16,14 +16,20 @@
package org.thingsboard.server.common.msg.cluster;
import lombok.Data;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
/**
* @author Andrew Shvayka
*/
@Data
-public final class ClusterEventMsg {
+public final class ClusterEventMsg implements TbActorMsg {
private final ServerAddress serverAddress;
private final boolean added;
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.CLUSTER_EVENT_MSG;
+ }
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ToAllNodesMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ToAllNodesMsg.java
index 1dc33c0..877689d 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ToAllNodesMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ToAllNodesMsg.java
@@ -15,10 +15,12 @@
*/
package org.thingsboard.server.common.msg.cluster;
+import org.thingsboard.server.common.msg.TbActorMsg;
+
import java.io.Serializable;
/**
* @author Andrew Shvayka
*/
-public interface ToAllNodesMsg extends Serializable {
+public interface ToAllNodesMsg extends Serializable, TbActorMsg {
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesSubscribeMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesSubscribeMsg.java
index 6894ac5..cadaf3c 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesSubscribeMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesSubscribeMsg.java
@@ -16,14 +16,14 @@
package org.thingsboard.server.common.msg.core;
import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
/**
* @author Andrew Shvayka
*/
public class AttributesSubscribeMsg implements FromDeviceMsg {
@Override
- public MsgType getMsgType() {
- return MsgType.SUBSCRIBE_ATTRIBUTES_REQUEST;
+ public SessionMsgType getMsgType() {
+ return SessionMsgType.SUBSCRIBE_ATTRIBUTES_REQUEST;
}
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUnsubscribeMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUnsubscribeMsg.java
index e3fcd6f..c98ad2a 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUnsubscribeMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUnsubscribeMsg.java
@@ -16,14 +16,15 @@
package org.thingsboard.server.common.msg.core;
import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
/**
* @author Andrew Shvayka
*/
public class AttributesUnsubscribeMsg implements FromDeviceMsg {
@Override
- public MsgType getMsgType() {
- return MsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST;
+ public SessionMsgType getMsgType() {
+ return SessionMsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST;
}
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUpdateNotification.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUpdateNotification.java
index 1329489..1f0f9dd 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUpdateNotification.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUpdateNotification.java
@@ -17,7 +17,8 @@ package org.thingsboard.server.common.msg.core;
import lombok.ToString;
import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
import org.thingsboard.server.common.msg.session.ToDeviceMsg;
@ToString
@@ -36,9 +37,8 @@ public class AttributesUpdateNotification implements ToDeviceMsg {
return true;
}
- @Override
- public MsgType getMsgType() {
- return MsgType.ATTRIBUTES_UPDATE_NOTIFICATION;
+ public SessionMsgType getSessionMsgType() {
+ return SessionMsgType.ATTRIBUTES_UPDATE_NOTIFICATION;
}
public AttributesKVMsg getData() {
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicCommandAckResponse.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicCommandAckResponse.java
index ef772c3..3ee04ba 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicCommandAckResponse.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicCommandAckResponse.java
@@ -15,26 +15,27 @@
*/
package org.thingsboard.server.common.msg.core;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
public class BasicCommandAckResponse extends BasicResponseMsg<Integer> implements StatusCodeResponse {
private static final long serialVersionUID = 1L;
- public static BasicCommandAckResponse onSuccess(MsgType requestMsgType, Integer requestId) {
+ public static BasicCommandAckResponse onSuccess(SessionMsgType requestMsgType, Integer requestId) {
return BasicCommandAckResponse.onSuccess(requestMsgType, requestId, 200);
}
- public static BasicCommandAckResponse onSuccess(MsgType requestMsgType, Integer requestId, Integer code) {
+ public static BasicCommandAckResponse onSuccess(SessionMsgType requestMsgType, Integer requestId, Integer code) {
return new BasicCommandAckResponse(requestMsgType, requestId, true, null, code);
}
- public static BasicCommandAckResponse onError(MsgType requestMsgType, Integer requestId, Exception error) {
+ public static BasicCommandAckResponse onError(SessionMsgType requestMsgType, Integer requestId, Exception error) {
return new BasicCommandAckResponse(requestMsgType, requestId, false, error, null);
}
- private BasicCommandAckResponse(MsgType requestMsgType, Integer requestId, boolean success, Exception error, Integer code) {
- super(requestMsgType, requestId, MsgType.TO_DEVICE_RPC_RESPONSE_ACK, success, error, code);
+ private BasicCommandAckResponse(SessionMsgType requestMsgType, Integer requestId, boolean success, Exception error, Integer code) {
+ super(requestMsgType, requestId, SessionMsgType.TO_DEVICE_RPC_RESPONSE_ACK, success, error, code);
}
@Override
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java
index b431c14..e0f6d7e 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java
@@ -16,7 +16,8 @@
package org.thingsboard.server.common.msg.core;
import lombok.ToString;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
import java.util.Collections;
import java.util.Optional;
@@ -41,8 +42,8 @@ public class BasicGetAttributesRequest extends BasicRequest implements GetAttrib
}
@Override
- public MsgType getMsgType() {
- return MsgType.GET_ATTRIBUTES_REQUEST;
+ public SessionMsgType getMsgType() {
+ return SessionMsgType.GET_ATTRIBUTES_REQUEST;
}
@Override
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesResponse.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesResponse.java
index 5072de2..e3eb15d 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesResponse.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesResponse.java
@@ -17,23 +17,24 @@ package org.thingsboard.server.common.msg.core;
import lombok.ToString;
import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
@ToString
public class BasicGetAttributesResponse extends BasicResponseMsg<AttributesKVMsg> implements GetAttributesResponse {
private static final long serialVersionUID = 1L;
- public static BasicGetAttributesResponse onSuccess(MsgType requestMsgType, int requestId, AttributesKVMsg code) {
+ public static BasicGetAttributesResponse onSuccess(SessionMsgType requestMsgType, int requestId, AttributesKVMsg code) {
return new BasicGetAttributesResponse(requestMsgType, requestId, true, null, code);
}
- public static BasicGetAttributesResponse onError(MsgType requestMsgType, int requestId, Exception error) {
+ public static BasicGetAttributesResponse onError(SessionMsgType requestMsgType, int requestId, Exception error) {
return new BasicGetAttributesResponse(requestMsgType, requestId, false, error, null);
}
- private BasicGetAttributesResponse(MsgType requestMsgType, int requestId, boolean success, Exception error, AttributesKVMsg code) {
- super(requestMsgType, requestId, MsgType.GET_ATTRIBUTES_RESPONSE, success, error, code);
+ private BasicGetAttributesResponse(SessionMsgType requestMsgType, int requestId, boolean success, Exception error, AttributesKVMsg code) {
+ super(requestMsgType, requestId, SessionMsgType.GET_ATTRIBUTES_RESPONSE, success, error, code);
}
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicResponseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicResponseMsg.java
index caaa15c..c61d8e5 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicResponseMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicResponseMsg.java
@@ -18,32 +18,33 @@ package org.thingsboard.server.common.msg.core;
import java.io.Serializable;
import java.util.Optional;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
public class BasicResponseMsg<T extends Serializable> implements ResponseMsg<T> {
private static final long serialVersionUID = 1L;
- private final MsgType requestMsgType;
+ private final SessionMsgType requestMsgType;
private final Integer requestId;
- private final MsgType msgType;
+ private final SessionMsgType sessionMsgType;
private final boolean success;
private final T data;
private final Exception error;
- protected BasicResponseMsg(MsgType requestMsgType, Integer requestId, MsgType msgType, boolean success, Exception error, T data) {
+ protected BasicResponseMsg(SessionMsgType requestMsgType, Integer requestId, SessionMsgType sessionMsgType, boolean success, Exception error, T data) {
super();
this.requestMsgType = requestMsgType;
this.requestId = requestId;
- this.msgType = msgType;
+ this.sessionMsgType = sessionMsgType;
this.success = success;
this.error = error;
this.data = data;
}
@Override
- public MsgType getRequestMsgType() {
+ public SessionMsgType getRequestMsgType() {
return requestMsgType;
}
@@ -72,8 +73,7 @@ public class BasicResponseMsg<T extends Serializable> implements ResponseMsg<T>
return "BasicResponseMsg [success=" + success + ", data=" + data + ", error=" + error + "]";
}
- @Override
- public MsgType getMsgType() {
- return msgType;
+ public SessionMsgType getSessionMsgType() {
+ return sessionMsgType;
}
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicStatusCodeResponse.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicStatusCodeResponse.java
index 22b525b..f21aa85 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicStatusCodeResponse.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicStatusCodeResponse.java
@@ -16,26 +16,27 @@
package org.thingsboard.server.common.msg.core;
import lombok.ToString;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
@ToString
public class BasicStatusCodeResponse extends BasicResponseMsg<Integer> implements StatusCodeResponse {
private static final long serialVersionUID = 1L;
- public static BasicStatusCodeResponse onSuccess(MsgType requestMsgType, Integer requestId) {
+ public static BasicStatusCodeResponse onSuccess(SessionMsgType requestMsgType, Integer requestId) {
return BasicStatusCodeResponse.onSuccess(requestMsgType, requestId, 0);
}
- public static BasicStatusCodeResponse onSuccess(MsgType requestMsgType, Integer requestId, Integer code) {
+ public static BasicStatusCodeResponse onSuccess(SessionMsgType requestMsgType, Integer requestId, Integer code) {
return new BasicStatusCodeResponse(requestMsgType, requestId, true, null, code);
}
- public static BasicStatusCodeResponse onError(MsgType requestMsgType, Integer requestId, Exception error) {
+ public static BasicStatusCodeResponse onError(SessionMsgType requestMsgType, Integer requestId, Exception error) {
return new BasicStatusCodeResponse(requestMsgType, requestId, false, error, null);
}
- private BasicStatusCodeResponse(MsgType requestMsgType, Integer requestId, boolean success, Exception error, Integer code) {
- super(requestMsgType, requestId, MsgType.STATUS_CODE_RESPONSE, success, error, code);
+ private BasicStatusCodeResponse(SessionMsgType requestMsgType, Integer requestId, boolean success, Exception error, Integer code) {
+ super(requestMsgType, requestId, SessionMsgType.STATUS_CODE_RESPONSE, success, error, code);
}
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicTelemetryUploadRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicTelemetryUploadRequest.java
index 1d96a65..60faeb1 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicTelemetryUploadRequest.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicTelemetryUploadRequest.java
@@ -21,7 +21,8 @@ import java.util.List;
import java.util.Map;
import org.thingsboard.server.common.data.kv.KvEntry;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
public class BasicTelemetryUploadRequest extends BasicRequest implements TelemetryUploadRequest {
@@ -48,8 +49,8 @@ public class BasicTelemetryUploadRequest extends BasicRequest implements Telemet
}
@Override
- public MsgType getMsgType() {
- return MsgType.POST_TELEMETRY_REQUEST;
+ public SessionMsgType getMsgType() {
+ return SessionMsgType.POST_TELEMETRY_REQUEST;
}
@Override
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ResponseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ResponseMsg.java
index 2eb0959..3e70460 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ResponseMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ResponseMsg.java
@@ -18,12 +18,12 @@ package org.thingsboard.server.common.msg.core;
import java.io.Serializable;
import java.util.Optional;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
import org.thingsboard.server.common.msg.session.ToDeviceMsg;
public interface ResponseMsg<T extends Serializable> extends ToDeviceMsg {
- MsgType getRequestMsgType();
+ SessionMsgType getRequestMsgType();
Integer getRequestId();
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcSubscribeMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcSubscribeMsg.java
index d4fc1d7..f8f24e8 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcSubscribeMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcSubscribeMsg.java
@@ -16,14 +16,15 @@
package org.thingsboard.server.common.msg.core;
import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
/**
* @author Andrew Shvayka
*/
public class RpcSubscribeMsg implements FromDeviceMsg {
@Override
- public MsgType getMsgType() {
- return MsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST;
+ public SessionMsgType getMsgType() {
+ return SessionMsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST;
}
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcUnsubscribeMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcUnsubscribeMsg.java
index b1532d7..23eb238 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcUnsubscribeMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcUnsubscribeMsg.java
@@ -16,14 +16,15 @@
package org.thingsboard.server.common.msg.core;
import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
/**
* @author Andrew Shvayka
*/
public class RpcUnsubscribeMsg implements FromDeviceMsg {
@Override
- public MsgType getMsgType() {
- return MsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST;
+ public SessionMsgType getMsgType() {
+ return SessionMsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST;
}
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineError.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineError.java
index bc8ceb4..dcfde0f 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineError.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineError.java
@@ -21,7 +21,7 @@ package org.thingsboard.server.common.msg.core;
public enum RuleEngineError {
- NO_RULES, NO_ACTIVE_RULES, NO_FILTERS_MATCHED, NO_REQUEST_FROM_ACTIONS, NO_TWO_WAY_ACTIONS, NO_RESPONSE_FROM_ACTIONS, PLUGIN_TIMEOUT(true);
+ QUEUE_PUT_TIMEOUT(true), SERVER_ERROR(true), TIMEOUT;
private final boolean critical;
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineErrorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineErrorMsg.java
index 5cb3314..e0ff23b 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineErrorMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineErrorMsg.java
@@ -16,7 +16,8 @@
package org.thingsboard.server.common.msg.core;
import lombok.Data;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
import org.thingsboard.server.common.msg.session.ToDeviceMsg;
/**
@@ -25,7 +26,7 @@ import org.thingsboard.server.common.msg.session.ToDeviceMsg;
@Data
public class RuleEngineErrorMsg implements ToDeviceMsg {
- private final MsgType inMsgType;
+ private final SessionMsgType inSessionMsgType;
private final RuleEngineError error;
@Override
@@ -33,27 +34,18 @@ public class RuleEngineErrorMsg implements ToDeviceMsg {
return false;
}
- @Override
- public MsgType getMsgType() {
- return MsgType.RULE_ENGINE_ERROR;
+ public SessionMsgType getSessionMsgType() {
+ return SessionMsgType.RULE_ENGINE_ERROR;
}
public String getErrorMsg() {
switch (error) {
- case NO_RULES:
- return "No rules configured!";
- case NO_ACTIVE_RULES:
- return "No active rules!";
- case NO_FILTERS_MATCHED:
- return "No rules that match current message!";
- case NO_REQUEST_FROM_ACTIONS:
- return "Rule filters match, but no plugin message produced by rule action!";
- case NO_TWO_WAY_ACTIONS:
- return "Rule filters match, but no rule with two-way action configured!";
- case NO_RESPONSE_FROM_ACTIONS:
- return "Rule filters match, message processed by plugin, but no response produced by rule action!";
- case PLUGIN_TIMEOUT:
- return "Timeout during processing of message by plugin!";
+ case QUEUE_PUT_TIMEOUT:
+ return "Timeout during persistence of the message to the queue!";
+ case SERVER_ERROR:
+ return "Error during processing of message by the server!";
+ case TIMEOUT:
+ return "Timeout during processing of message by the server!";
default:
throw new RuntimeException("Error " + error + " is not supported!");
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseMsg.java
index 738bc6e..6d3ad5e 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseMsg.java
@@ -16,14 +16,15 @@
package org.thingsboard.server.common.msg.core;
import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
/**
* @author Andrew Shvayka
*/
public class SessionCloseMsg implements FromDeviceMsg {
@Override
- public MsgType getMsgType() {
- return MsgType.SESSION_CLOSE;
+ public SessionMsgType getMsgType() {
+ return SessionMsgType.SESSION_CLOSE;
}
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java
index bf3c982..bf36a9b 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java
@@ -17,7 +17,8 @@ package org.thingsboard.server.common.msg.core;
import lombok.ToString;
import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
import org.thingsboard.server.common.msg.session.ToDeviceMsg;
@ToString
@@ -30,9 +31,8 @@ public class SessionCloseNotification implements ToDeviceMsg {
return true;
}
- @Override
- public MsgType getMsgType() {
- return MsgType.SESSION_CLOSE;
+ public SessionMsgType getSessionMsgType() {
+ return SessionMsgType.SESSION_CLOSE;
}
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java
index f3a7bc7..28e319e 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java
@@ -15,15 +15,21 @@
*/
package org.thingsboard.server.common.msg.core;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
/**
* @author Andrew Shvayka
*/
+@Data
public class SessionOpenMsg implements FromDeviceMsg {
+
@Override
- public MsgType getMsgType() {
- return MsgType.SESSION_OPEN;
+ public SessionMsgType getMsgType() {
+ return SessionMsgType.SESSION_OPEN;
}
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcRequestMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcRequestMsg.java
index c9eeb4e..d1a1f96 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcRequestMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcRequestMsg.java
@@ -16,7 +16,8 @@
package org.thingsboard.server.common.msg.core;
import lombok.Data;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
import org.thingsboard.server.common.msg.session.ToDeviceMsg;
/**
@@ -29,9 +30,8 @@ public class ToDeviceRpcRequestMsg implements ToDeviceMsg {
private final String method;
private final String params;
- @Override
- public MsgType getMsgType() {
- return MsgType.TO_DEVICE_RPC_REQUEST;
+ public SessionMsgType getSessionMsgType() {
+ return SessionMsgType.TO_DEVICE_RPC_REQUEST;
}
@Override
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcResponseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcResponseMsg.java
index ec739b3..4fa3024 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcResponseMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcResponseMsg.java
@@ -17,7 +17,8 @@ package org.thingsboard.server.common.msg.core;
import lombok.Data;
import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
/**
* @author Andrew Shvayka
@@ -29,7 +30,7 @@ public class ToDeviceRpcResponseMsg implements FromDeviceMsg {
private final String data;
@Override
- public MsgType getMsgType() {
- return MsgType.TO_DEVICE_RPC_RESPONSE;
+ public SessionMsgType getMsgType() {
+ return SessionMsgType.TO_DEVICE_RPC_RESPONSE;
}
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java
index 87708a7..3823aca 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java
@@ -17,7 +17,7 @@ package org.thingsboard.server.common.msg.core;
import lombok.Data;
import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
/**
* @author Andrew Shvayka
@@ -30,7 +30,7 @@ public class ToServerRpcRequestMsg implements FromDeviceRequestMsg {
private final String params;
@Override
- public MsgType getMsgType() {
- return MsgType.TO_SERVER_RPC_REQUEST;
+ public SessionMsgType getMsgType() {
+ return SessionMsgType.TO_SERVER_RPC_REQUEST;
}
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcResponseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcResponseMsg.java
index b9a43d1..82f44e9 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcResponseMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcResponseMsg.java
@@ -17,7 +17,8 @@ package org.thingsboard.server.common.msg.core;
import lombok.Data;
import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
import org.thingsboard.server.common.msg.session.ToDeviceMsg;
/**
@@ -29,9 +30,8 @@ public class ToServerRpcResponseMsg implements ToDeviceMsg {
private final int requestId;
private final String data;
- @Override
- public MsgType getMsgType() {
- return MsgType.TO_SERVER_RPC_RESPONSE;
+ public SessionMsgType getSessionMsgType() {
+ return SessionMsgType.TO_SERVER_RPC_RESPONSE;
}
@Override
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
new file mode 100644
index 0000000..7702788
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg;
+
+/**
+ * Created by ashvayka on 15.03.18.
+ */
+//TODO: add all "See" references
+public enum MsgType {
+
+ /**
+ * ADDED/UPDATED/DELETED events for server nodes.
+ *
+ * See {@link org.thingsboard.server.common.msg.cluster.ClusterEventMsg}
+ */
+ CLUSTER_EVENT_MSG,
+
+ /**
+ * All messages, could be send to cluster
+ */
+ SEND_TO_CLUSTER_MSG,
+
+ /**
+ * ADDED/UPDATED/DELETED events for main entities.
+ *
+ * See {@link org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg}
+ */
+ COMPONENT_LIFE_CYCLE_MSG,
+
+ /**
+ * Misc messages from the REST API/SERVICE layer to the new rule engine.
+ *
+ * See {@link org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg}
+ */
+ SERVICE_TO_RULE_ENGINE_MSG,
+
+ /**
+ * Message that is sent by RuleChainActor to RuleActor with command to process TbMsg.
+ */
+ RULE_CHAIN_TO_RULE_MSG,
+
+ /**
+ * Message that is sent by RuleChainActor to other RuleChainActor with command to process TbMsg.
+ */
+ RULE_CHAIN_TO_RULE_CHAIN_MSG,
+
+ /**
+ * Message that is sent by RuleActor to RuleChainActor with command to process TbMsg by next nodes in chain.
+ */
+ RULE_TO_RULE_CHAIN_TELL_NEXT_MSG,
+
+ /**
+ * Message that is sent by RuleActor implementation to RuleActor itself to log the error.
+ */
+ RULE_TO_SELF_ERROR_MSG,
+
+ /**
+ * Message that is sent by RuleActor implementation to RuleActor itself to process the message.
+ */
+ RULE_TO_SELF_MSG,
+
+ /**
+ * Message that is sent by Session Actor to Device Actor. Represents messages from the device itself.
+ */
+ DEVICE_SESSION_TO_DEVICE_ACTOR_MSG,
+
+ DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG,
+
+ DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG,
+
+ DEVICE_NAME_OR_TYPE_UPDATE_TO_DEVICE_ACTOR_MSG,
+
+ DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG,
+
+ SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG,
+
+ DEVICE_ACTOR_SERVER_SIDE_RPC_TIMEOUT_MSG,
+
+ DEVICE_ACTOR_CLIENT_SIDE_RPC_TIMEOUT_MSG,
+
+ DEVICE_ACTOR_QUEUE_TIMEOUT_MSG,
+
+ /**
+ * Message that is sent from the Device Actor to Rule Engine. Requires acknowledgement
+ */
+ DEVICE_ACTOR_TO_RULE_ENGINE_MSG,
+
+ /**
+ * Message that is sent from Rule Engine to the Device Actor when message is successfully pushed to queue.
+ */
+ RULE_ENGINE_QUEUE_PUT_ACK_MSG, ACTOR_SYSTEM_TO_DEVICE_SESSION_ACTOR_MSG, TRANSPORT_TO_DEVICE_SESSION_ACTOR_MSG, SESSION_TIMEOUT_MSG, SESSION_CTRL_MSG;
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java
index d48c3fe..eec2de0 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java
@@ -15,14 +15,15 @@
*/
package org.thingsboard.server.common.msg.plugin;
-import lombok.Data;
import lombok.Getter;
import lombok.ToString;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
-import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
@@ -32,34 +33,26 @@ import java.util.Optional;
* @author Andrew Shvayka
*/
@ToString
-public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg {
+public class ComponentLifecycleMsg implements TbActorMsg, TenantAwareMsg, ToAllNodesMsg {
@Getter
private final TenantId tenantId;
- private final PluginId pluginId;
- private final RuleId ruleId;
+ @Getter
+ private final EntityId entityId;
@Getter
private final ComponentLifecycleEvent event;
- public static ComponentLifecycleMsg forPlugin(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent event) {
- return new ComponentLifecycleMsg(tenantId, pluginId, null, event);
- }
-
- public static ComponentLifecycleMsg forRule(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent event) {
- return new ComponentLifecycleMsg(tenantId, null, ruleId, event);
- }
-
- private ComponentLifecycleMsg(TenantId tenantId, PluginId pluginId, RuleId ruleId, ComponentLifecycleEvent event) {
+ public ComponentLifecycleMsg(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent event) {
this.tenantId = tenantId;
- this.pluginId = pluginId;
- this.ruleId = ruleId;
+ this.entityId = entityId;
this.event = event;
}
- public Optional<PluginId> getPluginId() {
- return Optional.ofNullable(pluginId);
+ public Optional<RuleChainId> getRuleChainId() {
+ return entityId.getEntityType() == EntityType.RULE_CHAIN ? Optional.of((RuleChainId) entityId) : Optional.empty();
}
- public Optional<RuleId> getRuleId() {
- return Optional.ofNullable(ruleId);
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.COMPONENT_LIFE_CYCLE_MSG;
}
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
index a24bdcd..49fad13 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.common.msg.session.ctrl;
import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
public class SessionCloseMsg implements SessionCtrlMsg {
@@ -60,4 +61,8 @@ public class SessionCloseMsg implements SessionCtrlMsg {
return timeout;
}
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.SESSION_CTRL_MSG;
+ }
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceMsg.java
index 19d45a7..71b6057 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceMsg.java
@@ -19,6 +19,6 @@ import java.io.Serializable;
public interface FromDeviceMsg extends Serializable {
- MsgType getMsgType();
+ SessionMsgType getMsgType();
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionCtrlMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionCtrlMsg.java
index 19ca219..8082f72 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionCtrlMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionCtrlMsg.java
@@ -15,8 +15,9 @@
*/
package org.thingsboard.server.common.msg.session;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
-public interface SessionCtrlMsg extends SessionAwareMsg {
+public interface SessionCtrlMsg extends SessionAwareMsg, TbActorMsg {
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceMsg.java
index 31ec1fa..705e864 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceMsg.java
@@ -21,6 +21,6 @@ public interface ToDeviceMsg extends Serializable {
boolean isSuccess();
- MsgType getMsgType();
+ SessionMsgType getSessionMsgType();
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
new file mode 100644
index 0000000..a6eb64e
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
@@ -0,0 +1,112 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.gen.MsgProtos;
+
+import java.io.Serializable;
+import java.nio.ByteBuffer;
+import java.util.UUID;
+
+/**
+ * Created by ashvayka on 13.01.18.
+ */
+@Data
+@AllArgsConstructor
+public final class TbMsg implements Serializable {
+
+ private final UUID id;
+ private final String type;
+ private final EntityId originator;
+ private final TbMsgMetaData metaData;
+ private final TbMsgDataType dataType;
+ private final String data;
+
+ //The following fields are not persisted to DB, because they can always be recovered from the context;
+ private final RuleChainId ruleChainId;
+ private final RuleNodeId ruleNodeId;
+ private final long clusterPartition;
+
+ public TbMsg(UUID id, String type, EntityId originator, TbMsgMetaData metaData, String data,
+ RuleChainId ruleChainId, RuleNodeId ruleNodeId, long clusterPartition) {
+ this.id = id;
+ this.type = type;
+ this.originator = originator;
+ this.metaData = metaData;
+ this.data = data;
+ this.dataType = TbMsgDataType.JSON;
+ this.ruleChainId = ruleChainId;
+ this.ruleNodeId = ruleNodeId;
+ this.clusterPartition = clusterPartition;
+ }
+
+ public static ByteBuffer toBytes(TbMsg msg) {
+ MsgProtos.TbMsgProto.Builder builder = MsgProtos.TbMsgProto.newBuilder();
+ builder.setId(msg.getId().toString());
+ builder.setType(msg.getType());
+ builder.setEntityType(msg.getOriginator().getEntityType().name());
+ builder.setEntityIdMSB(msg.getOriginator().getId().getMostSignificantBits());
+ builder.setEntityIdLSB(msg.getOriginator().getId().getLeastSignificantBits());
+
+ if (msg.getRuleChainId() != null) {
+ builder.setRuleChainIdMSB(msg.getRuleChainId().getId().getMostSignificantBits());
+ builder.setRuleChainIdLSB(msg.getRuleChainId().getId().getLeastSignificantBits());
+ }
+
+ if (msg.getRuleNodeId() != null) {
+ builder.setRuleNodeIdMSB(msg.getRuleNodeId().getId().getMostSignificantBits());
+ builder.setRuleNodeIdLSB(msg.getRuleNodeId().getId().getLeastSignificantBits());
+ }
+
+ if (msg.getMetaData() != null) {
+ builder.setMetaData(MsgProtos.TbMsgMetaDataProto.newBuilder().putAllData(msg.getMetaData().getData()).build());
+ }
+
+ builder.setDataType(msg.getDataType().ordinal());
+ builder.setData(msg.getData());
+ byte[] bytes = builder.build().toByteArray();
+ return ByteBuffer.wrap(bytes);
+ }
+
+ public static TbMsg fromBytes(ByteBuffer buffer) {
+ try {
+ MsgProtos.TbMsgProto proto = MsgProtos.TbMsgProto.parseFrom(buffer.array());
+ TbMsgMetaData metaData = new TbMsgMetaData(proto.getMetaData().getDataMap());
+ EntityId entityId = EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB()));
+ RuleChainId ruleChainId = new RuleChainId(new UUID(proto.getRuleChainIdMSB(), proto.getRuleChainIdLSB()));
+ RuleNodeId ruleNodeId = null;
+ if(proto.getRuleNodeIdMSB() != 0L && proto.getRuleNodeIdLSB() != 0L) {
+ ruleNodeId = new RuleNodeId(new UUID(proto.getRuleNodeIdMSB(), proto.getRuleNodeIdLSB()));
+ }
+ TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()];
+ return new TbMsg(UUID.fromString(proto.getId()), proto.getType(), entityId, metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, proto.getClusterPartition());
+ } catch (InvalidProtocolBufferException e) {
+ throw new IllegalStateException("Could not parse protobuf for TbMsg", e);
+ }
+ }
+
+ public TbMsg copy(UUID newId, RuleChainId ruleChainId, RuleNodeId ruleNodeId, long clusterPartition) {
+ return new TbMsg(newId, type, originator, metaData.copy(), dataType, data, ruleChainId, ruleNodeId, clusterPartition);
+ }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java
new file mode 100644
index 0000000..e157aaa
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Created by ashvayka on 13.01.18.
+ */
+@Data
+@NoArgsConstructor
+public final class TbMsgMetaData implements Serializable {
+
+ private final Map<String, String> data = new ConcurrentHashMap<>();
+
+ public TbMsgMetaData(Map<String, String> data) {
+ this.data.putAll(data);
+ }
+
+ public String getValue(String key) {
+ return data.get(key);
+ }
+
+ public void putValue(String key, String value) {
+ data.put(key, value);
+ }
+
+ public Map<String, String> values() {
+ return new HashMap<>(data);
+ }
+
+ public TbMsgMetaData copy() {
+ return new TbMsgMetaData(new ConcurrentHashMap<>(data));
+ }
+}
common/message/src/main/proto/tbmsg.proto 45(+45 -0)
diff --git a/common/message/src/main/proto/tbmsg.proto b/common/message/src/main/proto/tbmsg.proto
new file mode 100644
index 0000000..60003dc
--- /dev/null
+++ b/common/message/src/main/proto/tbmsg.proto
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+syntax = "proto3";
+package msgqueue;
+
+option java_package = "org.thingsboard.server.common.msg.gen";
+option java_outer_classname = "MsgProtos";
+
+message TbMsgMetaDataProto {
+ map<string, string> data = 1;
+}
+
+message TbMsgProto {
+ string id = 1;
+ string type = 2;
+ string entityType = 3;
+ int64 entityIdMSB = 4;
+ int64 entityIdLSB = 5;
+
+ int64 ruleChainIdMSB = 6;
+ int64 ruleChainIdLSB = 7;
+
+ int64 ruleNodeIdMSB = 8;
+ int64 ruleNodeIdLSB = 9;
+ int64 clusterPartition = 10;
+
+ TbMsgMetaDataProto metaData = 11;
+
+ int32 dataType = 12;
+ string data = 13;
+
+}
\ No newline at end of file
common/pom.xml 2(+1 -1)
diff --git a/common/pom.xml b/common/pom.xml
index 0f92175..8644257 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.4.1-SNAPSHOT</version>
+ <version>2.0.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
common/transport/pom.xml 2(+1 -1)
diff --git a/common/transport/pom.xml b/common/transport/pom.xml
index 18f2de6..fe0ab72 100644
--- a/common/transport/pom.xml
+++ b/common/transport/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.4.1-SNAPSHOT</version>
+ <version>2.0.0-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java
index 390266a..e7c1734 100644
--- a/common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java
@@ -36,9 +36,16 @@ public class JsonConverter {
return convertToTelemetry(jsonObject, BasicRequest.DEFAULT_REQUEST_ID);
}
+ public static TelemetryUploadRequest convertToTelemetry(JsonElement jsonObject, long ts) throws JsonSyntaxException {
+ return convertToTelemetry(jsonObject, ts, BasicRequest.DEFAULT_REQUEST_ID);
+ }
+
public static TelemetryUploadRequest convertToTelemetry(JsonElement jsonObject, int requestId) throws JsonSyntaxException {
+ return convertToTelemetry(jsonObject, System.currentTimeMillis(), requestId);
+ }
+
+ private static TelemetryUploadRequest convertToTelemetry(JsonElement jsonObject, long systemTs, int requestId) throws JsonSyntaxException {
BasicTelemetryUploadRequest request = new BasicTelemetryUploadRequest(requestId);
- long systemTs = System.currentTimeMillis();
if (jsonObject.isJsonObject()) {
parseObject(request, systemTs, jsonObject);
} else if (jsonObject.isJsonArray()) {
@@ -118,13 +125,13 @@ public class JsonConverter {
}
}
- public static UpdateAttributesRequest convertToAttributes(JsonElement element) {
+ public static AttributesUpdateRequest convertToAttributes(JsonElement element) {
return convertToAttributes(element, BasicRequest.DEFAULT_REQUEST_ID);
}
- public static UpdateAttributesRequest convertToAttributes(JsonElement element, int requestId) {
+ public static AttributesUpdateRequest convertToAttributes(JsonElement element, int requestId) {
if (element.isJsonObject()) {
- BasicUpdateAttributesRequest request = new BasicUpdateAttributesRequest(requestId);
+ BasicAttributesUpdateRequest request = new BasicAttributesUpdateRequest(requestId);
long ts = System.currentTimeMillis();
request.add(parseValues(element.getAsJsonObject()).stream().map(kv -> new BaseAttributeKvEntry(kv, ts)).collect(Collectors.toList()));
return request;
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostIntervalRegistryLogger.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostIntervalRegistryLogger.java
new file mode 100644
index 0000000..65767f1
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostIntervalRegistryLogger.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport.quota.host;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.transport.quota.inmemory.IntervalRegistryLogger;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@Component
+@Slf4j
+public class HostIntervalRegistryLogger extends IntervalRegistryLogger {
+
+ private final long logIntervalMin;
+
+ public HostIntervalRegistryLogger(@Value("${quota.host.log.topSize}") int topSize,
+ @Value("${quota.host.log.intervalMin}") long logIntervalMin,
+ HostRequestIntervalRegistry intervalRegistry) {
+ super(topSize, logIntervalMin, intervalRegistry);
+ this.logIntervalMin = logIntervalMin;
+ }
+
+ protected void log(Map<String, Long> top, int uniqHosts, long requestsCount) {
+ long rps = requestsCount / TimeUnit.MINUTES.toSeconds(logIntervalMin);
+ StringBuilder builder = new StringBuilder("Quota Statistic : ");
+ builder.append("uniqHosts : ").append(uniqHosts).append("; ");
+ builder.append("requestsCount : ").append(requestsCount).append("; ");
+ builder.append("RPS : ").append(rps).append(" ");
+ builder.append("top -> ");
+ for (Map.Entry<String, Long> host : top.entrySet()) {
+ builder.append(host.getKey()).append(" : ").append(host.getValue()).append("; ");
+ }
+
+ log.info(builder.toString());
+ }
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostRequestIntervalRegistry.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostRequestIntervalRegistry.java
new file mode 100644
index 0000000..9b3b461
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostRequestIntervalRegistry.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport.quota.host;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.transport.quota.inmemory.KeyBasedIntervalRegistry;
+
+/**
+ * @author Vitaliy Paromskiy
+ * @version 1.0
+ */
+@Component
+@Slf4j
+public class HostRequestIntervalRegistry extends KeyBasedIntervalRegistry {
+
+ public HostRequestIntervalRegistry(@Value("${quota.host.intervalMs}") long intervalDurationMs,
+ @Value("${quota.host.ttlMs}") long ttlMs,
+ @Value("${quota.host.whitelist}") String whiteList,
+ @Value("${quota.host.blacklist}") String blackList) {
+ super(intervalDurationMs, ttlMs, whiteList, blackList, "host");
+ }
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostRequestsQuotaService.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostRequestsQuotaService.java
new file mode 100644
index 0000000..69342b5
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostRequestsQuotaService.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport.quota.host;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.transport.quota.AbstractQuotaService;
+
+/**
+ * @author Vitaliy Paromskiy
+ * @version 1.0
+ */
+@Service
+@Slf4j
+public class HostRequestsQuotaService extends AbstractQuotaService {
+
+ public HostRequestsQuotaService(HostRequestIntervalRegistry requestRegistry, HostRequestLimitPolicy requestsPolicy,
+ HostIntervalRegistryCleaner registryCleaner, HostIntervalRegistryLogger registryLogger,
+ @Value("${quota.host.enabled}") boolean enabled) {
+ super(requestRegistry, requestsPolicy, registryCleaner, registryLogger, enabled);
+ }
+
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryCleaner.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryCleaner.java
index a227d2a..0c510ff 100644
--- a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryCleaner.java
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryCleaner.java
@@ -16,10 +16,7 @@
package org.thingsboard.server.common.transport.quota.inmemory;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Component;
-import javax.annotation.PreDestroy;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@@ -28,15 +25,14 @@ import java.util.concurrent.TimeUnit;
* @author Vitaliy Paromskiy
* @version 1.0
*/
-@Component
@Slf4j
-public class IntervalRegistryCleaner {
+public abstract class IntervalRegistryCleaner {
- private final HostRequestIntervalRegistry intervalRegistry;
+ private final KeyBasedIntervalRegistry intervalRegistry;
private final long cleanPeriodMs;
private ScheduledExecutorService executor;
- public IntervalRegistryCleaner(HostRequestIntervalRegistry intervalRegistry, @Value("${quota.host.cleanPeriodMs}") long cleanPeriodMs) {
+ public IntervalRegistryCleaner(KeyBasedIntervalRegistry intervalRegistry, long cleanPeriodMs) {
this.intervalRegistry = intervalRegistry;
this.cleanPeriodMs = cleanPeriodMs;
}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java
index 8b34a6b..30399a1 100644
--- a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java
@@ -17,8 +17,6 @@ package org.thingsboard.server.common.transport.quota.inmemory;
import com.google.common.collect.MinMaxPriorityQueue;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Component;
import java.util.Comparator;
import java.util.Map;
@@ -32,17 +30,15 @@ import java.util.stream.Collectors;
* @author Vitaliy Paromskiy
* @version 1.0
*/
-@Component
@Slf4j
-public class IntervalRegistryLogger {
+public abstract class IntervalRegistryLogger {
private final int topSize;
- private final HostRequestIntervalRegistry intervalRegistry;
+ private final KeyBasedIntervalRegistry intervalRegistry;
private final long logIntervalMin;
private ScheduledExecutorService executor;
- public IntervalRegistryLogger(@Value("${quota.log.topSize}") int topSize, @Value("${quota.log.intervalMin}") long logIntervalMin,
- HostRequestIntervalRegistry intervalRegistry) {
+ public IntervalRegistryLogger(int topSize, long logIntervalMin, KeyBasedIntervalRegistry intervalRegistry) {
this.topSize = topSize;
this.logIntervalMin = logIntervalMin;
this.intervalRegistry = intervalRegistry;
@@ -79,17 +75,5 @@ public class IntervalRegistryLogger {
return topQueue.stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
- private void log(Map<String, Long> top, int uniqHosts, long requestsCount) {
- long rps = requestsCount / TimeUnit.MINUTES.toSeconds(logIntervalMin);
- StringBuilder builder = new StringBuilder("Quota Statistic : ");
- builder.append("uniqHosts : ").append(uniqHosts).append("; ");
- builder.append("requestsCount : ").append(requestsCount).append("; ");
- builder.append("RPS : ").append(rps).append(" ");
- builder.append("top -> ");
- for (Map.Entry<String, Long> host : top.entrySet()) {
- builder.append(host.getKey()).append(" : ").append(host.getValue()).append("; ");
- }
-
- log.info(builder.toString());
- }
+ protected abstract void log(Map<String, Long> top, int uniqHosts, long requestsCount);
}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/tenant/TenantIntervalRegistryLogger.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/tenant/TenantIntervalRegistryLogger.java
new file mode 100644
index 0000000..c56f457
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/tenant/TenantIntervalRegistryLogger.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport.quota.tenant;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.transport.quota.inmemory.IntervalRegistryLogger;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Component
+public class TenantIntervalRegistryLogger extends IntervalRegistryLogger {
+
+ private final long logIntervalMin;
+
+ public TenantIntervalRegistryLogger(@Value("${quota.rule.tenant.log.topSize}") int topSize,
+ @Value("${quota.rule.tenant.log.intervalMin}") long logIntervalMin,
+ TenantMsgsIntervalRegistry intervalRegistry) {
+ super(topSize, logIntervalMin, intervalRegistry);
+ this.logIntervalMin = logIntervalMin;
+ }
+
+ protected void log(Map<String, Long> top, int uniqHosts, long requestsCount) {
+ long rps = requestsCount / TimeUnit.MINUTES.toSeconds(logIntervalMin);
+ StringBuilder builder = new StringBuilder("Tenant Quota Statistic : ");
+ builder.append("uniqTenants : ").append(uniqHosts).append("; ");
+ builder.append("requestsCount : ").append(requestsCount).append("; ");
+ builder.append("RPS : ").append(rps).append(" ");
+ builder.append("top -> ");
+ for (Map.Entry<String, Long> host : top.entrySet()) {
+ builder.append(host.getKey()).append(" : ").append(host.getValue()).append("; ");
+ }
+
+ log.info(builder.toString());
+ }
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/TransportAdaptor.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/TransportAdaptor.java
index 93a6e99..080f874 100644
--- a/common/transport/src/main/java/org/thingsboard/server/common/transport/TransportAdaptor.java
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/TransportAdaptor.java
@@ -16,7 +16,7 @@
package org.thingsboard.server.common.transport;
import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
import org.thingsboard.server.common.msg.session.SessionActorToAdaptorMsg;
import org.thingsboard.server.common.msg.session.SessionContext;
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
@@ -25,7 +25,7 @@ import java.util.Optional;
public interface TransportAdaptor<C extends SessionContext, T, V> {
- AdaptorToSessionActorMsg convertToActorMsg(C ctx, MsgType type, T inbound) throws AdaptorException;
+ AdaptorToSessionActorMsg convertToActorMsg(C ctx, SessionMsgType type, T inbound) throws AdaptorException;
Optional<V> convertToAdaptorMsg(C ctx, SessionActorToAdaptorMsg msg) throws AdaptorException;
diff --git a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestLimitPolicyTest.java b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestLimitPolicyTest.java
index 174d182..07e03ef 100644
--- a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestLimitPolicyTest.java
+++ b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestLimitPolicyTest.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.common.transport.quota;
import org.junit.Test;
+import org.thingsboard.server.common.transport.quota.host.HostRequestLimitPolicy;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
diff --git a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaServiceTest.java b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaServiceTest.java
index 547f0cf..20f8a55 100644
--- a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaServiceTest.java
+++ b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaServiceTest.java
@@ -17,9 +17,7 @@ package org.thingsboard.server.common.transport.quota;
import org.junit.Before;
import org.junit.Test;
-import org.thingsboard.server.common.transport.quota.inmemory.HostRequestIntervalRegistry;
-import org.thingsboard.server.common.transport.quota.inmemory.IntervalRegistryCleaner;
-import org.thingsboard.server.common.transport.quota.inmemory.IntervalRegistryLogger;
+import org.thingsboard.server.common.transport.quota.host.*;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -35,8 +33,8 @@ public class HostRequestsQuotaServiceTest {
private HostRequestIntervalRegistry requestRegistry = mock(HostRequestIntervalRegistry.class);
private HostRequestLimitPolicy requestsPolicy = mock(HostRequestLimitPolicy.class);
- private IntervalRegistryCleaner registryCleaner = mock(IntervalRegistryCleaner.class);
- private IntervalRegistryLogger registryLogger = mock(IntervalRegistryLogger.class);
+ private HostIntervalRegistryCleaner registryCleaner = mock(HostIntervalRegistryCleaner.class);
+ private HostIntervalRegistryLogger registryLogger = mock(HostIntervalRegistryLogger.class);
@Before
public void init() {
diff --git a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistryTest.java b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistryTest.java
index 78b82ee..b49dd00 100644
--- a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistryTest.java
+++ b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistryTest.java
@@ -15,11 +15,9 @@
*/
package org.thingsboard.server.common.transport.quota.inmemory;
-import com.google.common.collect.Sets;
import org.junit.Before;
import org.junit.Test;
-
-import java.util.Collections;
+import org.thingsboard.server.common.transport.quota.host.HostRequestIntervalRegistry;
import static org.junit.Assert.assertEquals;
diff --git a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLoggerTest.java b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLoggerTest.java
index c9139ae..6e51420 100644
--- a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLoggerTest.java
+++ b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLoggerTest.java
@@ -18,6 +18,8 @@ package org.thingsboard.server.common.transport.quota.inmemory;
import com.google.common.collect.ImmutableMap;
import org.junit.Before;
import org.junit.Test;
+import org.thingsboard.server.common.transport.quota.host.HostIntervalRegistryLogger;
+import org.thingsboard.server.common.transport.quota.host.HostRequestIntervalRegistry;
import java.util.Collections;
import java.util.Map;
@@ -37,7 +39,7 @@ public class IntervalRegistryLoggerTest {
@Before
public void init() {
- logger = new IntervalRegistryLogger(3, 10, requestRegistry);
+ logger = new HostIntervalRegistryLogger(3, 10, requestRegistry);
}
@Test
dao/pom.xml 25(+10 -15)
diff --git a/dao/pom.xml b/dao/pom.xml
index 3eb267e..b9a180a 100644
--- a/dao/pom.xml
+++ b/dao/pom.xml
@@ -20,10 +20,9 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.4.1-SNAPSHOT</version>
+ <version>2.0.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
- <groupId>org.thingsboard</groupId>
<artifactId>dao</artifactId>
<packaging>jar</packaging>
@@ -39,7 +38,11 @@
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>data</artifactId>
- </dependency>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.common</groupId>
+ <artifactId>message</artifactId>
+ </dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
@@ -142,26 +145,18 @@
<scope>test</scope>
</dependency>
<dependency>
- <groupId>org.apache.curator</groupId>
- <artifactId>curator-x-discovery</artifactId>
- </dependency>
- <dependency>
- <groupId>com.hazelcast</groupId>
- <artifactId>hazelcast-zookeeper</artifactId>
+ <groupId>com.google.protobuf</groupId>
+ <artifactId>protobuf-java</artifactId>
</dependency>
<dependency>
- <groupId>com.hazelcast</groupId>
- <artifactId>hazelcast</artifactId>
+ <groupId>org.apache.curator</groupId>
+ <artifactId>curator-x-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
- <groupId>com.hazelcast</groupId>
- <artifactId>hazelcast-spring</artifactId>
- </dependency>
- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
index 4da7df2..6638818 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
@@ -15,8 +15,15 @@
*/
package org.thingsboard.server.dao.alarm;
+import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.ListenableFuture;
-import org.thingsboard.server.common.data.alarm.*;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmId;
+import org.thingsboard.server.common.data.alarm.AlarmInfo;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
+import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TimePageData;
@@ -30,7 +37,7 @@ public interface AlarmService {
ListenableFuture<Boolean> ackAlarm(AlarmId alarmId, long ackTs);
- ListenableFuture<Boolean> clearAlarm(AlarmId alarmId, long ackTs);
+ ListenableFuture<Boolean> clearAlarm(AlarmId alarmId, JsonNode details, long ackTs);
ListenableFuture<Alarm> findAlarmByIdAsync(AlarmId alarmId);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
index e669ae2..23fe85a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
@@ -16,8 +16,8 @@
package org.thingsboard.server.dao.alarm;
+import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Function;
-import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
@@ -25,19 +25,25 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.Tenant;
-import org.thingsboard.server.common.data.alarm.*;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmId;
+import org.thingsboard.server.common.data.alarm.AlarmInfo;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
+import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
-import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
-import org.thingsboard.server.common.data.relation.EntitySearchDirection;
-import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TenantDao;
@@ -187,7 +193,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
}
@Override
- public ListenableFuture<Boolean> clearAlarm(AlarmId alarmId, long clearTime) {
+ public ListenableFuture<Boolean> clearAlarm(AlarmId alarmId, JsonNode details, long clearTime) {
return getAndUpdate(alarmId, new Function<Alarm, Boolean>() {
@Nullable
@Override
@@ -199,6 +205,9 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
AlarmStatus newStatus = oldStatus.isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK;
alarm.setStatus(newStatus);
alarm.setClearTs(clearTime);
+ if (details != null) {
+ alarm.setDetails(details);
+ }
alarmDao.save(alarm);
updateRelations(alarm, oldStatus, newStatus);
return true;
@@ -218,15 +227,14 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
public ListenableFuture<AlarmInfo> findAlarmInfoByIdAsync(AlarmId alarmId) {
log.trace("Executing findAlarmInfoByIdAsync [{}]", alarmId);
validateId(alarmId, "Incorrect alarmId " + alarmId);
- return Futures.transform(alarmDao.findAlarmByIdAsync(alarmId.getId()),
- (AsyncFunction<Alarm, AlarmInfo>) alarm1 -> {
- AlarmInfo alarmInfo = new AlarmInfo(alarm1);
+ return Futures.transformAsync(alarmDao.findAlarmByIdAsync(alarmId.getId()),
+ a -> {
+ AlarmInfo alarmInfo = new AlarmInfo(a);
return Futures.transform(
- entityService.fetchEntityNameAsync(alarmInfo.getOriginator()), (Function<String, AlarmInfo>)
- originatorName -> {
- alarmInfo.setOriginatorName(originatorName);
- return alarmInfo;
- }
+ entityService.fetchEntityNameAsync(alarmInfo.getOriginator()), originatorName -> {
+ alarmInfo.setOriginatorName(originatorName);
+ return alarmInfo;
+ }
);
});
}
@@ -235,18 +243,17 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
public ListenableFuture<TimePageData<AlarmInfo>> findAlarms(AlarmQuery query) {
ListenableFuture<List<AlarmInfo>> alarms = alarmDao.findAlarms(query);
if (query.getFetchOriginator() != null && query.getFetchOriginator().booleanValue()) {
- alarms = Futures.transform(alarms, (AsyncFunction<List<AlarmInfo>, List<AlarmInfo>>) input -> {
+ alarms = Futures.transformAsync(alarms, input -> {
List<ListenableFuture<AlarmInfo>> alarmFutures = new ArrayList<>(input.size());
for (AlarmInfo alarmInfo : input) {
alarmFutures.add(Futures.transform(
- entityService.fetchEntityNameAsync(alarmInfo.getOriginator()), (Function<String, AlarmInfo>)
- originatorName -> {
- if (originatorName == null) {
- originatorName = "Deleted";
- }
- alarmInfo.setOriginatorName(originatorName);
- return alarmInfo;
- }
+ entityService.fetchEntityNameAsync(alarmInfo.getOriginator()), originatorName -> {
+ if (originatorName == null) {
+ originatorName = "Deleted";
+ }
+ alarmInfo.setOriginatorName(originatorName);
+ return alarmInfo;
+ }
));
}
return Futures.successfulAsList(alarmFutures);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java
index 1233c7f..646c055 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java
@@ -17,8 +17,6 @@ package org.thingsboard.server.dao.alarm;
import com.datastax.driver.core.querybuilder.QueryBuilder;
import com.datastax.driver.core.querybuilder.Select;
-import com.google.common.base.Function;
-import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
@@ -45,7 +43,12 @@ import java.util.UUID;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_BY_ID_VIEW_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TYPE_PROPERTY;
@Component
@Slf4j
@@ -102,12 +105,12 @@ public class CassandraAlarmDao extends CassandraAbstractModelDao<AlarmEntity, Al
}
String relationType = BaseAlarmService.ALARM_RELATION_PREFIX + searchStatusName;
ListenableFuture<List<EntityRelation>> relations = relationDao.findRelations(affectedEntity, relationType, RelationTypeGroup.ALARM, EntityType.ALARM, query.getPageLink());
- return Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<AlarmInfo>>) input -> {
+ return Futures.transformAsync(relations, input -> {
List<ListenableFuture<AlarmInfo>> alarmFutures = new ArrayList<>(input.size());
for (EntityRelation relation : input) {
alarmFutures.add(Futures.transform(
findAlarmByIdAsync(relation.getTo().getId()),
- (Function<Alarm, AlarmInfo>) AlarmInfo::new));
+ AlarmInfo::new));
}
return Futures.successfulAsList(alarmFutures);
});
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
index dcd9523..1eafb07 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
@@ -17,7 +17,6 @@ package org.thingsboard.server.dao.asset;
import com.google.common.base.Function;
-import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
@@ -45,12 +44,19 @@ import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.tenant.TenantDao;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
import java.util.stream.Collectors;
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
-import static org.thingsboard.server.dao.service.Validator.*;
+import static org.thingsboard.server.dao.service.Validator.validateId;
+import static org.thingsboard.server.dao.service.Validator.validateIds;
+import static org.thingsboard.server.dao.service.Validator.validatePageLink;
+import static org.thingsboard.server.dao.service.Validator.validateString;
@Service
@Slf4j
@@ -194,10 +200,10 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ
@Override
public ListenableFuture<List<Asset>> findAssetsByQuery(AssetSearchQuery query) {
ListenableFuture<List<EntityRelation>> relations = relationService.findByQuery(query.toEntitySearchQuery());
- ListenableFuture<List<Asset>> assets = Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<Asset>>) relations1 -> {
+ ListenableFuture<List<Asset>> assets = Futures.transformAsync(relations, r -> {
EntitySearchDirection direction = query.toEntitySearchQuery().getParameters().getDirection();
List<ListenableFuture<Asset>> futures = new ArrayList<>();
- for (EntityRelation relation : relations1) {
+ for (EntityRelation relation : r) {
EntityId entityId = direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom();
if (entityId.getEntityType() == EntityType.ASSET) {
futures.add(findAssetByIdAsync(new AssetId(entityId.getId())));
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java
index 64ec718..80a6f43 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java
@@ -36,10 +36,30 @@ import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao;
import org.thingsboard.server.dao.util.NoSqlDao;
import javax.annotation.Nullable;
-import java.util.*;
-
-import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.in;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_BY_TENANT_AND_NAME_VIEW_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
@Component
@Slf4j
diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java
index 8ae9dc8..a736a69 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java
@@ -15,7 +15,11 @@
*/
package org.thingsboard.server.dao.attributes;
-import com.datastax.driver.core.*;
+import com.datastax.driver.core.BoundStatement;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.Row;
+import com.datastax.driver.core.Statement;
import com.datastax.driver.core.querybuilder.QueryBuilder;
import com.datastax.driver.core.querybuilder.Select;
import com.google.common.base.Function;
@@ -41,7 +45,12 @@ import java.util.stream.Collectors;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ATTRIBUTES_KV_CF;
+import static org.thingsboard.server.dao.model.ModelConstants.ATTRIBUTE_KEY_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ATTRIBUTE_TYPE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.LAST_UPDATE_TS_COLUMN;
/**
* @author Andrew Shvayka
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
index 19651a0..d2e6f05 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
@@ -15,15 +15,16 @@
*/
package org.thingsboard.server.dao.audit;
-import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasName;
-import org.thingsboard.server.common.data.User;
-import org.thingsboard.server.common.data.audit.ActionStatus;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.audit.AuditLog;
-import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
index 23fadeb..a30c1b4 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
@@ -34,10 +34,16 @@ import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.audit.ActionStatus;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.audit.AuditLog;
-import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.id.AuditLogId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.audit.sink.AuditLogSink;
import org.thingsboard.server.dao.entity.EntityService;
@@ -158,11 +164,20 @@ public class AuditLogServiceImpl implements AuditLogService {
switch(actionType) {
case ADDED:
case UPDATED:
- ObjectNode entityNode = objectMapper.valueToTree(entity);
- if (entityId.getEntityType() == EntityType.DASHBOARD) {
- entityNode.put("configuration", "");
+ if (entity != null) {
+ ObjectNode entityNode = objectMapper.valueToTree(entity);
+ if (entityId.getEntityType() == EntityType.DASHBOARD) {
+ entityNode.put("configuration", "");
+ }
+ actionData.set("entity", entityNode);
+ }
+ if (entityId.getEntityType() == EntityType.RULE_CHAIN) {
+ RuleChainMetaData ruleChainMetaData = extractParameter(RuleChainMetaData.class, additionalInfo);
+ if (ruleChainMetaData != null) {
+ ObjectNode ruleChainMetaDataNode = objectMapper.valueToTree(ruleChainMetaData);
+ actionData.set("metadata", ruleChainMetaDataNode);
+ }
}
- actionData.set("entity", entityNode);
break;
case DELETED:
case ACTIVATED:
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
index fd02b5f..764f468 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
@@ -48,13 +48,22 @@ import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.StringJoiner;
+import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_BY_CUSTOMER_ID_CF;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_BY_ENTITY_ID_CF;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_BY_TENANT_ID_CF;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_BY_USER_ID_CF;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_COLUMN_FAMILY_NAME;
@Component
@Slf4j
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
index 3706e50..b14d6b1 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
@@ -22,7 +22,11 @@ import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.audit.AuditLog;
-import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java
index cfb69f3..3be33f3 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java
@@ -18,7 +18,6 @@ package org.thingsboard.server.dao.cache;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java b/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
index ca94822..fd61976 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
@@ -16,10 +16,17 @@
package org.thingsboard.server.dao.cassandra;
-import com.datastax.driver.core.*;
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.ConsistencyLevel;
+import com.datastax.driver.core.HostDistance;
+import com.datastax.driver.core.PoolingOptions;
import com.datastax.driver.core.ProtocolOptions.Compression;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.mapping.DefaultPropertyMapper;
import com.datastax.driver.mapping.Mapper;
+import com.datastax.driver.mapping.MappingConfiguration;
import com.datastax.driver.mapping.MappingManager;
+import com.datastax.driver.mapping.PropertyAccessStrategy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -149,7 +156,13 @@ public abstract class AbstractCassandraCluster {
} else {
session = cluster.connect();
}
- mappingManager = new MappingManager(session);
+// For Cassandra Driver version 3.5.0
+ DefaultPropertyMapper propertyMapper = new DefaultPropertyMapper();
+ propertyMapper.setPropertyAccessStrategy(PropertyAccessStrategy.FIELDS);
+ MappingConfiguration configuration = MappingConfiguration.builder().withPropertyMapper(propertyMapper).build();
+ mappingManager = new MappingManager(session, configuration);
+// For Cassandra Driver version 3.0.0
+// mappingManager = new MappingManager(session);
break;
} catch (Exception e) {
log.warn("Failed to initialize cassandra cluster due to {}. Will retry in {} ms", e.getMessage(), initRetryInterval);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java
index 9fea618..598f98a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java
@@ -33,7 +33,9 @@ import java.util.UUID;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TITLE_PROPERTY;
@Component
@Slf4j
@NoSqlDao
diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
index fc434bf..c7bcfbc 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
@@ -18,7 +18,11 @@ package org.thingsboard.server.dao;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.dao.model.ToData;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
public abstract class DaoUtil {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java
index 8091b2a..13a27c2 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java
@@ -15,7 +15,6 @@
*/
package org.thingsboard.server.dao.dashboard;
-import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
@@ -40,7 +39,9 @@ import java.util.List;
import java.util.UUID;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_TENANT_ID_PROPERTY;
@Component
@Slf4j
@@ -77,7 +78,7 @@ public class CassandraDashboardInfoDao extends CassandraAbstractSearchTextDao<Da
ListenableFuture<List<EntityRelation>> relations = relationDao.findRelations(new CustomerId(customerId), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.DASHBOARD, EntityType.DASHBOARD, pageLink);
- return Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<DashboardInfo>>) input -> {
+ return Futures.transformAsync(relations, input -> {
List<ListenableFuture<DashboardInfo>> dashboardFutures = new ArrayList<>(input.size());
for (EntityRelation relation : input) {
dashboardFutures.add(findByIdAsync(relation.getTo().getId()));
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
index 44d18ac..c69364b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
@@ -26,8 +26,6 @@ import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
-import java.util.Set;
-
public interface DashboardService {
Dashboard findDashboardById(DashboardId dashboardId);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
index 6ce5286..e1f4e3c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
@@ -22,8 +22,10 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
-import org.thingsboard.server.common.data.*;
-import org.thingsboard.server.common.data.alarm.AlarmInfo;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.DashboardInfo;
+import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -36,8 +38,6 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.dao.customer.CustomerDao;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
-import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.dao.relation.RelationDao;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.TimePaginatedRemover;
@@ -45,11 +45,7 @@ import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.tenant.TenantDao;
import javax.annotation.Nullable;
-import java.sql.Time;
-import java.util.ArrayList;
-import java.util.HashSet;
import java.util.List;
-import java.util.Set;
import java.util.concurrent.ExecutionException;
import static org.thingsboard.server.dao.service.Validator.validateId;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java
index 641c464..110f207 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java
@@ -36,10 +36,30 @@ import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao;
import org.thingsboard.server.dao.util.NoSqlDao;
import javax.annotation.Nullable;
-import java.util.*;
-
-import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.in;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_BY_TENANT_AND_NAME_VIEW_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
@Component
@Slf4j
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
index 3a7c5e3..c5edd9e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
@@ -26,7 +26,6 @@ import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import java.util.List;
-import java.util.Optional;
public interface DeviceService {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
index 9120619..3930e3a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
@@ -16,7 +16,6 @@
package org.thingsboard.server.dao.device;
import com.google.common.base.Function;
-import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
@@ -28,7 +27,11 @@ import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
-import org.thingsboard.server.common.data.*;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.EntitySubtype;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
@@ -48,13 +51,20 @@ import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.tenant.TenantDao;
import javax.annotation.Nullable;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CACHE;
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
-import static org.thingsboard.server.dao.service.Validator.*;
+import static org.thingsboard.server.dao.service.Validator.validateId;
+import static org.thingsboard.server.dao.service.Validator.validateIds;
+import static org.thingsboard.server.dao.service.Validator.validatePageLink;
+import static org.thingsboard.server.dao.service.Validator.validateString;
@Service
@Slf4j
@@ -227,10 +237,10 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
@Override
public ListenableFuture<List<Device>> findDevicesByQuery(DeviceSearchQuery query) {
ListenableFuture<List<EntityRelation>> relations = relationService.findByQuery(query.toEntitySearchQuery());
- ListenableFuture<List<Device>> devices = Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<Device>>) relations1 -> {
+ ListenableFuture<List<Device>> devices = Futures.transformAsync(relations, r -> {
EntitySearchDirection direction = query.toEntitySearchQuery().getParameters().getDirection();
List<ListenableFuture<Device>> futures = new ArrayList<>();
- for (EntityRelation relation : relations1) {
+ for (EntityRelation relation : r) {
EntityId entityId = direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom();
if (entityId.getEntityType() == EntityType.DEVICE) {
futures.add(findDeviceByIdAsync(new DeviceId(entityId.getId())));
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
index 50723f3..bc530ab 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
@@ -20,8 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.dao.relation.RelationService;
-import java.util.concurrent.ExecutionException;
-
@Slf4j
public abstract class AbstractEntityService {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
index acc88eb..1b67ca0 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
@@ -29,8 +29,7 @@ import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
-import org.thingsboard.server.dao.plugin.PluginService;
-import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
@@ -48,12 +47,6 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
private DeviceService deviceService;
@Autowired
- private RuleService ruleService;
-
- @Autowired
- private PluginService pluginService;
-
- @Autowired
private TenantService tenantService;
@Autowired
@@ -68,6 +61,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
@Autowired
private AlarmService alarmService;
+ @Autowired
+ private RuleChainService ruleChainService;
+
@Override
public void deleteEntityRelations(EntityId entityId) {
super.deleteEntityRelations(entityId);
@@ -85,12 +81,6 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
case DEVICE:
hasName = deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId()));
break;
- case RULE:
- hasName = ruleService.findRuleByIdAsync(new RuleId(entityId.getId()));
- break;
- case PLUGIN:
- hasName = pluginService.findPluginByIdAsync(new PluginId(entityId.getId()));
- break;
case TENANT:
hasName = tenantService.findTenantByIdAsync(new TenantId(entityId.getId()));
break;
@@ -106,6 +96,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
case ALARM:
hasName = alarmService.findAlarmByIdAsync(new AlarmId(entityId.getId()));
break;
+ case RULE_CHAIN:
+ hasName = ruleChainService.findRuleChainByIdAsync(new RuleChainId(entityId.getId()));
+ break;
default:
throw new IllegalStateException("Not Implemented!");
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java
index b8ba4a7..7dddec1 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.event;
+import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -44,6 +45,12 @@ public class BaseEventService implements EventService {
}
@Override
+ public ListenableFuture<Event> saveAsync(Event event) {
+ eventValidator.validate(event);
+ return eventDao.saveAsync(event);
+ }
+
+ @Override
public Optional<Event> saveIfNotExists(Event event) {
eventValidator.validate(event);
if (StringUtils.isEmpty(event.getUid())) {
@@ -82,6 +89,11 @@ public class BaseEventService implements EventService {
return new TimePageData<>(events, pageLink);
}
+ @Override
+ public List<Event> findLatestEvents(TenantId tenantId, EntityId entityId, String eventType, int limit) {
+ return eventDao.findLatestEvents(tenantId.getId(), entityId, eventType, limit);
+ }
+
private DataValidator<Event> eventValidator =
new DataValidator<Event>() {
@Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java
index 43d3fd5..7549e40 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java
@@ -15,11 +15,13 @@
*/
package org.thingsboard.server.dao.event;
-import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
import com.datastax.driver.core.querybuilder.Insert;
import com.datastax.driver.core.querybuilder.QueryBuilder;
import com.datastax.driver.core.querybuilder.Select;
import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
@@ -38,6 +40,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
+import java.util.concurrent.ExecutionException;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
@@ -62,6 +65,15 @@ public class CassandraBaseEventDao extends CassandraAbstractSearchTimeDao<EventE
@Override
public Event save(Event event) {
+ try {
+ return saveAsync(event).get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new IllegalStateException("Could not save EventEntity", e);
+ }
+ }
+
+ @Override
+ public ListenableFuture<Event> saveAsync(Event event) {
log.debug("Save event [{}] ", event);
if (event.getTenantId() == null) {
log.trace("Save system event with predefined id {}", systemTenantId);
@@ -73,7 +85,8 @@ public class CassandraBaseEventDao extends CassandraAbstractSearchTimeDao<EventE
if (StringUtils.isEmpty(event.getUid())) {
event.setUid(event.getId().toString());
}
- return save(new EventEntity(event), false).orElse(null);
+ ListenableFuture<Optional<Event>> optionalSave = saveAsync(new EventEntity(event), false);
+ return Futures.transform(optionalSave, opt -> opt.orElse(null));
}
@Override
@@ -134,7 +147,30 @@ public class CassandraBaseEventDao extends CassandraAbstractSearchTimeDao<EventE
return DaoUtil.convertDataList(entities);
}
+ @Override
+ public List<Event> findLatestEvents(UUID tenantId, EntityId entityId, String eventType, int limit) {
+ log.trace("Try to find latest events by tenant [{}], entity [{}], type [{}] and limit [{}]", tenantId, entityId, eventType, limit);
+ Select select = select().from(EVENT_BY_TYPE_AND_ID_VIEW_NAME);
+ Select.Where query = select.where();
+ query.and(eq(ModelConstants.EVENT_TENANT_ID_PROPERTY, tenantId));
+ query.and(eq(ModelConstants.EVENT_ENTITY_TYPE_PROPERTY, entityId.getEntityType()));
+ query.and(eq(ModelConstants.EVENT_ENTITY_ID_PROPERTY, entityId.getId()));
+ query.and(eq(ModelConstants.EVENT_TYPE_PROPERTY, eventType));
+ query.limit(limit);
+ query.orderBy(QueryBuilder.desc(ModelConstants.EVENT_TYPE_PROPERTY), QueryBuilder.desc(ModelConstants.ID_PROPERTY));
+ List<EventEntity> entities = findListByStatement(query);
+ return DaoUtil.convertDataList(entities);
+ }
+
private Optional<Event> save(EventEntity entity, boolean ifNotExists) {
+ try {
+ return saveAsync(entity, ifNotExists).get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new IllegalStateException("Could not save EventEntity", e);
+ }
+ }
+
+ private ListenableFuture<Optional<Event>> saveAsync(EventEntity entity, boolean ifNotExists) {
if (entity.getId() == null) {
entity.setId(UUIDs.timeBased());
}
@@ -149,11 +185,13 @@ public class CassandraBaseEventDao extends CassandraAbstractSearchTimeDao<EventE
if (ifNotExists) {
insert = insert.ifNotExists();
}
- ResultSet rs = executeWrite(insert);
- if (rs.wasApplied()) {
- return Optional.of(DaoUtil.getData(entity));
- } else {
- return Optional.empty();
- }
+ ResultSetFuture resultSetFuture = executeAsyncWrite(insert);
+ return Futures.transform(resultSetFuture, rs -> {
+ if (rs.wasApplied()) {
+ return Optional.of(DaoUtil.getData(entity));
+ } else {
+ return Optional.empty();
+ }
+ });
}
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java
index fb5c0fb..9469c61 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.event;
+import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.TimePageLink;
@@ -38,6 +39,14 @@ public interface EventDao extends Dao<Event> {
Event save(Event event);
/**
+ * Save or update event object async
+ *
+ * @param event the event object
+ * @return saved event object future
+ */
+ ListenableFuture<Event> saveAsync(Event event);
+
+ /**
* Save event object if it is not yet saved
*
* @param event the event object
@@ -76,4 +85,16 @@ public interface EventDao extends Dao<Event> {
* @return the event list
*/
List<Event> findEvents(UUID tenantId, EntityId entityId, String eventType, TimePageLink pageLink);
+
+ /**
+ * Find latest events by tenantId, entityId and eventType.
+ *
+ * @param tenantId the tenantId
+ * @param entityId the entityId
+ * @param eventType the eventType
+ * @param limit the limit
+ * @return the event list
+ */
+ List<Event> findLatestEvents(UUID tenantId, EntityId entityId, String eventType, int limit);
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/EventService.java b/dao/src/main/java/org/thingsboard/server/dao/event/EventService.java
index edee190..0698c6b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/event/EventService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/event/EventService.java
@@ -15,18 +15,22 @@
*/
package org.thingsboard.server.dao.event;
+import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
+import java.util.List;
import java.util.Optional;
public interface EventService {
Event save(Event event);
+ ListenableFuture<Event> saveAsync(Event event);
+
Optional<Event> saveIfNotExists(Event event);
Optional<Event> findEvent(TenantId tenantId, EntityId entityId, String eventType, String eventUid);
@@ -34,4 +38,7 @@ public interface EventService {
TimePageData<Event> findEvents(TenantId tenantId, EntityId entityId, TimePageLink pageLink);
TimePageData<Event> findEvents(TenantId tenantId, EntityId entityId, String eventType, TimePageLink pageLink);
+
+ List<Event> findLatestEvents(TenantId tenantId, EntityId entityId, String eventType, int limit);
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/BaseEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/BaseEntity.java
index 0aaf9c2..01ea41b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/BaseEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/BaseEntity.java
@@ -15,7 +15,6 @@
*/
package org.thingsboard.server.dao.model;
-import java.io.Serializable;
import java.util.UUID;
public interface BaseEntity<D> extends ToData<D> {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/EntitySubtypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/EntitySubtypeEntity.java
index e4e4d61..16622de 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/EntitySubtypeEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/EntitySubtypeEntity.java
@@ -25,7 +25,10 @@ import org.thingsboard.server.dao.model.type.EntityTypeCodec;
import java.util.UUID;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_TYPE_PROPERTY;
@Table(name = ENTITY_SUBTYPE_COLUMN_FAMILY_NAME)
public class EntitySubtypeEntity {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
index c2b55c9..3a934eb 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.dao.model;
import com.datastax.driver.core.utils.UUIDs;
import org.apache.commons.lang3.ArrayUtils;
import org.thingsboard.server.common.data.UUIDConverter;
+import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation;
import java.util.UUID;
@@ -29,6 +30,7 @@ public class ModelConstants {
public static final UUID NULL_UUID = UUIDs.startOf(0);
public static final String NULL_UUID_STR = UUIDConverter.fromTimeUUID(NULL_UUID);
+ public static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);
/**
* Generic constants.
@@ -273,21 +275,6 @@ public class ModelConstants {
public static final String DASHBOARD_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "dashboard_by_tenant_and_search_text";
/**
- * Cassandra plugin metadata constants.
- */
- public static final String PLUGIN_COLUMN_FAMILY_NAME = "plugin";
- public static final String PLUGIN_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY;
- public static final String PLUGIN_NAME_PROPERTY = "name";
- public static final String PLUGIN_API_TOKEN_PROPERTY = "api_token";
- public static final String PLUGIN_CLASS_PROPERTY = "plugin_class";
- public static final String PLUGIN_ACCESS_PROPERTY = "public_access";
- public static final String PLUGIN_STATE_PROPERTY = STATE_PROPERTY;
- public static final String PLUGIN_CONFIGURATION_PROPERTY = "configuration";
-
- public static final String PLUGIN_BY_API_TOKEN_COLUMN_FAMILY_NAME = "plugin_by_api_token";
- public static final String PLUGIN_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "plugin_by_tenant_and_search_text";
-
- /**
* Cassandra plugin component metadata constants.
*/
public static final String COMPONENT_DESCRIPTOR_COLUMN_FAMILY_NAME = "component_descriptor";
@@ -303,22 +290,6 @@ public class ModelConstants {
public static final String COMPONENT_DESCRIPTOR_BY_ID = "component_desc_by_id";
/**
- * Cassandra rule metadata constants.
- */
- public static final String RULE_COLUMN_FAMILY_NAME = "rule";
- public static final String RULE_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY;
- public static final String RULE_NAME_PROPERTY = "name";
- public static final String RULE_STATE_PROPERTY = STATE_PROPERTY;
- public static final String RULE_WEIGHT_PROPERTY = "weight";
- public static final String RULE_PLUGIN_TOKEN_PROPERTY = "plugin_token";
- public static final String RULE_FILTERS = "filters";
- public static final String RULE_PROCESSOR = "processor";
- public static final String RULE_ACTION = "action";
-
- public static final String RULE_BY_PLUGIN_TOKEN = "rule_by_plugin_token";
- public static final String RULE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "rule_by_tenant_and_search_text";
-
- /**
* Cassandra event constants.
*/
public static final String EVENT_COLUMN_FAMILY_NAME = "event";
@@ -332,6 +303,29 @@ public class ModelConstants {
public static final String EVENT_BY_TYPE_AND_ID_VIEW_NAME = "event_by_type_and_id";
public static final String EVENT_BY_ID_VIEW_NAME = "event_by_id";
+ public static final String DEBUG_MODE = "debug_mode";
+
+ /**
+ * Cassandra rule chain constants.
+ */
+ public static final String RULE_CHAIN_COLUMN_FAMILY_NAME = "rule_chain";
+ public static final String RULE_CHAIN_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY;
+ public static final String RULE_CHAIN_NAME_PROPERTY = "name";
+ public static final String RULE_CHAIN_FIRST_RULE_NODE_ID_PROPERTY = "first_rule_node_id";
+ public static final String RULE_CHAIN_ROOT_PROPERTY = "root";
+ public static final String RULE_CHAIN_CONFIGURATION_PROPERTY = "configuration";
+
+ public static final String RULE_CHAIN_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "rule_chain_by_tenant_and_search_text";
+
+ /**
+ * Cassandra rule node constants.
+ */
+ public static final String RULE_NODE_COLUMN_FAMILY_NAME = "rule_node";
+ public static final String RULE_NODE_CHAIN_ID_PROPERTY = "rule_chain_id";
+ public static final String RULE_NODE_TYPE_PROPERTY = "type";
+ public static final String RULE_NODE_NAME_PROPERTY = "name";
+ public static final String RULE_NODE_CONFIGURATION_PROPERTY = "configuration";
+
/**
* Cassandra attributes and timeseries constants.
*/
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AdminSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AdminSettingsEntity.java
index 29c9859..7b8e847 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AdminSettingsEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AdminSettingsEntity.java
@@ -29,7 +29,10 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
import java.util.UUID;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_JSON_VALUE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_KEY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
@Table(name = ADMIN_SETTINGS_COLUMN_FAMILY_NAME)
@EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AlarmEntity.java
index 72bf4df..99df84f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AlarmEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AlarmEntity.java
@@ -16,7 +16,10 @@
package org.thingsboard.server.dao.model.nosql;
import com.datastax.driver.core.utils.UUIDs;
-import com.datastax.driver.mapping.annotations.*;
+import com.datastax.driver.mapping.annotations.ClusteringColumn;
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@@ -35,7 +38,20 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
import java.util.UUID;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ACK_TS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_CLEAR_TS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_DETAILS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_END_TS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_PROPAGATE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_SEVERITY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_START_TS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_STATUS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
@Table(name = ALARM_COLUMN_FAMILY_NAME)
@EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java
index 11eb905..da9bdb4 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java
@@ -31,7 +31,14 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
import java.util.UUID;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_ADDITIONAL_INFO_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
@Table(name = ASSET_COLUMN_FAMILY_NAME)
@EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
index ab2e3bc..aa145ed 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
@@ -25,7 +25,11 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionStatus;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.audit.AuditLog;
-import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.id.AuditLogId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.model.type.ActionStatusCodec;
import org.thingsboard.server.dao.model.type.ActionTypeCodec;
@@ -34,7 +38,19 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
import java.util.UUID;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_DATA_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_STATUS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_BY_ENTITY_ID_CF;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ENTITY_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_USER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_USER_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
@Table(name = AUDIT_LOG_BY_ENTITY_ID_CF)
@Data
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/ComponentDescriptorEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/ComponentDescriptorEntity.java
index 4998ae5..6593304 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/ComponentDescriptorEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/ComponentDescriptorEntity.java
@@ -28,7 +28,15 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
import java.util.UUID;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_ACTIONS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_CLASS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_CONFIGURATION_DESCRIPTOR_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_SCOPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
/**
* @author Andrew Shvayka
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/CustomerEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/CustomerEntity.java
index 0952037..d13a54e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/CustomerEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/CustomerEntity.java
@@ -30,7 +30,20 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
import java.util.UUID;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ADDRESS2_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ADDRESS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CITY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.COUNTRY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_ADDITIONAL_INFO_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TITLE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EMAIL_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.PHONE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.STATE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ZIP_PROPERTY;
@Table(name = CUSTOMER_COLUMN_FAMILY_NAME)
@EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java
index 622d6df..74963f8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java
@@ -38,7 +38,13 @@ import java.io.IOException;
import java.util.HashSet;
import java.util.UUID;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_CONFIGURATION_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_TITLE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
@Table(name = DASHBOARD_COLUMN_FAMILY_NAME)
@EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java
index ab0ec1c..ce5f723 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java
@@ -36,7 +36,12 @@ import java.io.IOException;
import java.util.HashSet;
import java.util.UUID;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_TITLE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
@Table(name = DASHBOARD_COLUMN_FAMILY_NAME)
@EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceCredentialsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceCredentialsEntity.java
index 8483865..5e4bb0f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceCredentialsEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceCredentialsEntity.java
@@ -30,7 +30,12 @@ import org.thingsboard.server.dao.model.type.DeviceCredentialsTypeCodec;
import java.util.UUID;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CREDENTIALS_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CREDENTIALS_CREDENTIALS_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CREDENTIALS_CREDENTIALS_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CREDENTIALS_CREDENTIALS_VALUE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CREDENTIALS_DEVICE_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
@Table(name = DEVICE_CREDENTIALS_COLUMN_FAMILY_NAME)
@EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java
index ef0c5fe..3543840 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java
@@ -31,7 +31,14 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
import java.util.UUID;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_ADDITIONAL_INFO_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
@Table(name = DEVICE_COLUMN_FAMILY_NAME)
@EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java
index 3b55d38..7f12359 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java
@@ -16,7 +16,10 @@
package org.thingsboard.server.dao.model.nosql;
import com.datastax.driver.core.utils.UUIDs;
-import com.datastax.driver.mapping.annotations.*;
+import com.datastax.driver.mapping.annotations.ClusteringColumn;
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -31,7 +34,14 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
import java.util.UUID;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_BODY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_ENTITY_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_ENTITY_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_UID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
/**
* @author Andrew Shvayka
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/TenantEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/TenantEntity.java
index 9140f9e..b16a733 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/TenantEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/TenantEntity.java
@@ -29,7 +29,20 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
import java.util.UUID;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ADDRESS2_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ADDRESS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CITY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.COUNTRY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EMAIL_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.PHONE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.STATE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_ADDITIONAL_INFO_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_REGION_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_TITLE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ZIP_PROPERTY;
@Table(name = TENANT_COLUMN_FAMILY_NAME)
@EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserCredentialsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserCredentialsEntity.java
index ef441de..1e7fbe2 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserCredentialsEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserCredentialsEntity.java
@@ -27,7 +27,13 @@ import org.thingsboard.server.dao.model.BaseEntity;
import java.util.UUID;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_ENABLED_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_PASSWORD_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_RESET_TOKEN_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_USER_ID_PROPERTY;
@Table(name = USER_CREDENTIALS_COLUMN_FAMILY_NAME)
@EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserEntity.java
index 69b5a63..f3f845d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserEntity.java
@@ -33,7 +33,16 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
import java.util.UUID;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_ADDITIONAL_INFO_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_AUTHORITY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_EMAIL_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_FIRST_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_LAST_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_TENANT_ID_PROPERTY;
@Table(name = USER_COLUMN_FAMILY_NAME)
@EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetsBundleEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetsBundleEntity.java
index 26cd88d..6704f1a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetsBundleEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetsBundleEntity.java
@@ -30,7 +30,13 @@ import org.thingsboard.server.dao.model.SearchTextEntity;
import java.nio.ByteBuffer;
import java.util.UUID;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_ALIAS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_IMAGE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_TITLE_PROPERTY;
@Table(name = WIDGETS_BUNDLE_COLUMN_FAMILY_NAME)
@EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetTypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetTypeEntity.java
index 1d37aaf..fe2d76e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetTypeEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetTypeEntity.java
@@ -30,7 +30,13 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
import java.util.UUID;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_ALIAS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_DESCRIPTOR_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_TENANT_ID_PROPERTY;
@Table(name = WIDGET_TYPE_COLUMN_FAMILY_NAME)
@EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AdminSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AdminSettingsEntity.java
index 7891bfc..b5851f0 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AdminSettingsEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AdminSettingsEntity.java
@@ -32,7 +32,9 @@ import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_JSON_VALUE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_KEY_PROPERTY;
@Data
@EqualsAndHashCode(callSuper = true)
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmEntity.java
index 65ca386..32f95e1 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmEntity.java
@@ -34,9 +34,24 @@ import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.mapping.JsonStringType;
-import javax.persistence.*;
-
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Table;
+
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ACK_TS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_CLEAR_TS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_END_TS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_PROPAGATE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_SEVERITY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_START_TS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_STATUS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TYPE_PROPERTY;
@Data
@EqualsAndHashCode(callSuper = true)
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java
index a32f2d6..2da7396 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java
@@ -35,7 +35,12 @@ import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
@Data
@EqualsAndHashCode(callSuper = true)
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java
index 81722a1..587a314 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java
@@ -17,13 +17,33 @@ package org.thingsboard.server.dao.model.sql;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+import org.thingsboard.server.common.data.kv.DoubleDataEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.dao.model.ToData;
-import javax.persistence.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+import javax.persistence.Table;
import java.io.Serializable;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ATTRIBUTE_KEY_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ATTRIBUTE_TYPE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.BOOLEAN_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.DOUBLE_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.LAST_UPDATE_TS_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.LONG_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.STRING_VALUE_COLUMN;
@Data
@Entity
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
index 6d13561..d4e8e27 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
@@ -25,15 +25,33 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionStatus;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.audit.AuditLog;
-import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.id.AuditLogId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.mapping.JsonStringType;
-import javax.persistence.*;
-
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Table;
+
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_DATA_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_STATUS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ENTITY_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_USER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_USER_NAME_PROPERTY;
@Data
@EqualsAndHashCode(callSuper = true)
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java
index 9739889..66fbcc3 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java
@@ -29,7 +29,11 @@ import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.SearchTextEntity;
import org.thingsboard.server.dao.util.mapping.JsonStringType;
-import javax.persistence.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Table;
@Data
@EqualsAndHashCode(callSuper = true)
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceCredentialsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceCredentialsEntity.java
index 676ae2d..faa036e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceCredentialsEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceCredentialsEntity.java
@@ -26,7 +26,11 @@ import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
-import javax.persistence.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Table;
@Data
@EqualsAndHashCode(callSuper = true)
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EventEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EventEntity.java
index 6f6b942..aaf2c36 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EventEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EventEntity.java
@@ -31,9 +31,19 @@ import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.util.mapping.JsonStringType;
-import javax.persistence.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Table;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_BODY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_ENTITY_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_ENTITY_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_UID_PROPERTY;
@Data
@EqualsAndHashCode(callSuper = true)
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java
index 2f94669..6d0f6d0 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java
@@ -26,9 +26,20 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.dao.model.ToData;
import org.thingsboard.server.dao.util.mapping.JsonStringType;
-import javax.persistence.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+import javax.persistence.Table;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ADDITIONAL_INFO_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.RELATION_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.RELATION_FROM_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.RELATION_FROM_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_GROUP_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_PROPERTY;
@Data
@Entity
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvEntity.java
index 3afe007..a6d3ea6 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvEntity.java
@@ -17,12 +17,31 @@ package org.thingsboard.server.dao.model.sql;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+import org.thingsboard.server.common.data.kv.DoubleDataEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.model.ToData;
-import javax.persistence.*;
-
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+import javax.persistence.Table;
+
+import static org.thingsboard.server.dao.model.ModelConstants.BOOLEAN_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.DOUBLE_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.KEY_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.LONG_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.STRING_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN;
@Data
@Entity
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvLatestEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvLatestEntity.java
index 67267c9..cb7851c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvLatestEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvLatestEntity.java
@@ -17,12 +17,31 @@ package org.thingsboard.server.dao.model.sql;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+import org.thingsboard.server.common.data.kv.DoubleDataEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.model.ToData;
-import javax.persistence.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+import javax.persistence.Table;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.BOOLEAN_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.DOUBLE_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.KEY_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.LONG_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.STRING_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN;
@Data
@Entity
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java
index 3df2aa2..7f18cbe 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java
@@ -31,7 +31,11 @@ import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.SearchTextEntity;
import org.thingsboard.server.dao.util.mapping.JsonStringType;
-import javax.persistence.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Table;
import static org.thingsboard.server.common.data.UUIDConverter.fromString;
import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
index 5c93066..d1af167 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
@@ -15,20 +15,39 @@
*/
package org.thingsboard.server.dao.nosql;
-import com.datastax.driver.core.*;
+import com.datastax.driver.core.BoundStatement;
+import com.datastax.driver.core.CodecRegistry;
+import com.datastax.driver.core.ConsistencyLevel;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.Statement;
+import com.datastax.driver.core.TypeCodec;
import com.datastax.driver.core.exceptions.CodecNotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
-import org.thingsboard.server.dao.model.type.*;
+import org.thingsboard.server.dao.model.type.AuthorityCodec;
+import org.thingsboard.server.dao.model.type.ComponentLifecycleStateCodec;
+import org.thingsboard.server.dao.model.type.ComponentScopeCodec;
+import org.thingsboard.server.dao.model.type.ComponentTypeCodec;
+import org.thingsboard.server.dao.model.type.DeviceCredentialsTypeCodec;
+import org.thingsboard.server.dao.model.type.EntityTypeCodec;
+import org.thingsboard.server.dao.model.type.JsonCodec;
import org.thingsboard.server.dao.util.BufferedRateLimiter;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
@Slf4j
public abstract class CassandraAbstractDao {
@Autowired
protected CassandraCluster cluster;
+ private ConcurrentMap<String, PreparedStatement> preparedStatementMap = new ConcurrentHashMap<>();
+
@Autowired
private BufferedRateLimiter rateLimiter;
@@ -55,7 +74,7 @@ public abstract class CassandraAbstractDao {
}
protected PreparedStatement prepare(String query) {
- return getSession().prepare(query);
+ return preparedStatementMap.computeIfAbsent(query, i -> getSession().prepare(i));
}
private void registerCodecIfNotFound(CodecRegistry registry, TypeCodec<?> codec) {
@@ -83,15 +102,27 @@ public abstract class CassandraAbstractDao {
}
private ResultSet execute(Statement statement, ConsistencyLevel level) {
- log.debug("Execute cassandra statement {}", statement);
+ if (log.isDebugEnabled()) {
+ log.debug("Execute cassandra statement {}", statementToString(statement));
+ }
return executeAsync(statement, level).getUninterruptibly();
}
private ResultSetFuture executeAsync(Statement statement, ConsistencyLevel level) {
- log.debug("Execute cassandra async statement {}", statement);
+ if (log.isDebugEnabled()) {
+ log.debug("Execute cassandra async statement {}", statementToString(statement));
+ }
if (statement.getConsistencyLevel() == null) {
statement.setConsistencyLevel(level);
}
return new RateLimitedResultSetFuture(getSession(), rateLimiter, statement);
}
+
+ private static String statementToString(Statement statement) {
+ if (statement instanceof BoundStatement) {
+ return ((BoundStatement)statement).preparedStatement().getQueryString();
+ } else {
+ return statement.toString();
+ }
+ }
}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java
index d250563..2b10bbc 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java
@@ -19,7 +19,6 @@ import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.ResultSetFuture;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.Statement;
-import com.google.common.base.Function;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@@ -28,7 +27,11 @@ import org.thingsboard.server.dao.exception.BufferLimitException;
import org.thingsboard.server.dao.util.AsyncRateLimiter;
import javax.annotation.Nullable;
-import java.util.concurrent.*;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
public class RateLimitedResultSetFuture implements ResultSetFuture {
@@ -36,14 +39,14 @@ public class RateLimitedResultSetFuture implements ResultSetFuture {
private final ListenableFuture<Void> rateLimitFuture;
public RateLimitedResultSetFuture(Session session, AsyncRateLimiter rateLimiter, Statement statement) {
- this.rateLimitFuture = Futures.withFallback(rateLimiter.acquireAsync(), t -> {
+ this.rateLimitFuture = Futures.catchingAsync(rateLimiter.acquireAsync(), Throwable.class, t -> {
if (!(t instanceof BufferLimitException)) {
rateLimiter.release();
}
return Futures.immediateFailedFuture(t);
});
this.originalFuture = Futures.transform(rateLimitFuture,
- (Function<Void, ResultSetFuture>) i -> executeAsyncWithRelease(rateLimiter, session, statement));
+ i -> executeAsyncWithRelease(rateLimiter, session, statement));
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/memory/InMemoryMsgQueue.java b/dao/src/main/java/org/thingsboard/server/dao/queue/memory/InMemoryMsgQueue.java
new file mode 100644
index 0000000..9305778
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/queue/memory/InMemoryMsgQueue.java
@@ -0,0 +1,123 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.queue.memory;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.dao.queue.MsgQueue;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+
+/**
+ * Created by ashvayka on 27.04.18.
+ */
+@Component
+@ConditionalOnProperty(prefix = "actors.rule.queue", value = "type", havingValue = "memory", matchIfMissing = true)
+@Slf4j
+public class InMemoryMsgQueue implements MsgQueue {
+
+ private ListeningExecutorService queueExecutor;
+ private Map<TenantId, Map<InMemoryMsgKey, Map<UUID, TbMsg>>> data = new HashMap<>();
+
+ @PostConstruct
+ public void init() {
+ // Should be always single threaded due to absence of locks.
+ queueExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
+ }
+
+ @PreDestroy
+ public void stop() {
+ if (queueExecutor != null) {
+ queueExecutor.shutdownNow();
+ }
+ }
+
+ @Override
+ public ListenableFuture<Void> put(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition) {
+ return queueExecutor.submit(() -> {
+ data.computeIfAbsent(tenantId, key -> new HashMap<>()).
+ computeIfAbsent(new InMemoryMsgKey(nodeId, clusterPartition), key -> new HashMap<>()).put(msg.getId(), msg);
+ return null;
+ });
+ }
+
+ @Override
+ public ListenableFuture<Void> ack(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition) {
+ return queueExecutor.submit(() -> {
+ Map<InMemoryMsgKey, Map<UUID, TbMsg>> tenantMap = data.get(tenantId);
+ if (tenantMap != null) {
+ InMemoryMsgKey key = new InMemoryMsgKey(nodeId, clusterPartition);
+ Map<UUID, TbMsg> map = tenantMap.get(key);
+ if (map != null) {
+ map.remove(msg.getId());
+ if (map.isEmpty()) {
+ tenantMap.remove(key);
+ }
+ }
+ if (tenantMap.isEmpty()) {
+ data.remove(tenantId);
+ }
+ }
+ return null;
+ });
+ }
+
+ @Override
+ public Iterable<TbMsg> findUnprocessed(TenantId tenantId, UUID nodeId, long clusterPartition) {
+ ListenableFuture<List<TbMsg>> list = queueExecutor.submit(() -> {
+ Map<InMemoryMsgKey, Map<UUID, TbMsg>> tenantMap = data.get(tenantId);
+ if (tenantMap != null) {
+ InMemoryMsgKey key = new InMemoryMsgKey(nodeId, clusterPartition);
+ Map<UUID, TbMsg> map = tenantMap.get(key);
+ if (map != null) {
+ return new ArrayList<>(map.values());
+ } else {
+ return Collections.emptyList();
+ }
+ } else {
+ return Collections.emptyList();
+ }
+ });
+ try {
+ return list.get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public ListenableFuture<Void> cleanUp(TenantId tenantId) {
+ return queueExecutor.submit(() -> {
+ data.remove(tenantId);
+ return null;
+ });
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/QueueBenchmark.java b/dao/src/main/java/org/thingsboard/server/dao/queue/QueueBenchmark.java
new file mode 100644
index 0000000..ca61a63
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/queue/QueueBenchmark.java
@@ -0,0 +1,152 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.queue;
+
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.HostDistance;
+import com.datastax.driver.core.PoolingOptions;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.context.annotation.Bean;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgDataType;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.annotation.Nullable;
+import java.net.InetSocketAddress;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+//@SpringBootApplication
+//@EnableAutoConfiguration
+//@ComponentScan({"org.thingsboard.rule.engine"})
+//@PropertySource("classpath:processing-pipeline.properties")
+@Slf4j
+public class QueueBenchmark implements CommandLineRunner {
+
+ public static void main(String[] args) {
+ try {
+ SpringApplication.run(QueueBenchmark.class, args);
+ } catch (Throwable th) {
+ th.printStackTrace();
+ System.exit(0);
+ }
+ }
+
+ @Autowired
+ private MsgQueue msgQueue;
+
+ @Override
+ public void run(String... strings) throws Exception {
+ System.out.println("It works + " + msgQueue);
+
+
+ long start = System.currentTimeMillis();
+ int msgCount = 10000000;
+ AtomicLong count = new AtomicLong(0);
+ ExecutorService service = Executors.newFixedThreadPool(100);
+
+ CountDownLatch latch = new CountDownLatch(msgCount);
+ for (int i = 0; i < msgCount; i++) {
+ service.submit(() -> {
+ boolean isFinished = false;
+ while (!isFinished) {
+ try {
+ TbMsg msg = randomMsg();
+ UUID nodeId = UUIDs.timeBased();
+ ListenableFuture<Void> put = msgQueue.put(new TenantId(EntityId.NULL_UUID), msg, nodeId, 100L);
+// ListenableFuture<Void> put = msgQueue.ack(msg, nodeId, 100L);
+ Futures.addCallback(put, new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(@Nullable Void result) {
+ latch.countDown();
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+// t.printStackTrace();
+ System.out.println("onFailure, because:" + t.getMessage());
+ latch.countDown();
+ }
+ });
+ isFinished = true;
+ } catch (Throwable th) {
+// th.printStackTrace();
+ System.out.println("Repeat query, because:" + th.getMessage());
+// latch.countDown();
+ }
+ }
+ });
+ }
+
+ long prev = 0L;
+ while (latch.getCount() != 0) {
+ TimeUnit.SECONDS.sleep(1);
+ long curr = latch.getCount();
+ long rps = prev - curr;
+ prev = curr;
+ System.out.println("rps = " + rps);
+ }
+
+ long end = System.currentTimeMillis();
+ System.out.println("final rps = " + (msgCount / (end - start) * 1000));
+
+ System.out.println("Finished");
+
+ }
+
+ private TbMsg randomMsg() {
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("key", "value");
+ String dataStr = "someContent";
+ return new TbMsg(UUIDs.timeBased(), "type", null, metaData, TbMsgDataType.JSON, dataStr, new RuleChainId(UUIDs.timeBased()), new RuleNodeId(UUIDs.timeBased()), 0L);
+ }
+
+ @Bean
+ public Session session() {
+ Cluster thingsboard = Cluster.builder()
+ .addContactPointsWithPorts(new InetSocketAddress("127.0.0.1", 9042))
+ .withClusterName("thingsboard")
+// .withSocketOptions(socketOpts.getOpts())
+ .withPoolingOptions(new PoolingOptions()
+ .setMaxRequestsPerConnection(HostDistance.LOCAL, 32768)
+ .setMaxRequestsPerConnection(HostDistance.REMOTE, 32768)).build();
+
+ Session session = thingsboard.connect("thingsboard");
+ return session;
+ }
+
+ @Bean
+ public int defaultTtl() {
+ return 6000;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
index 55838d6..ba6d09d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
@@ -15,7 +15,11 @@
*/
package org.thingsboard.server.dao.relation;
-import com.datastax.driver.core.*;
+import com.datastax.driver.core.BoundStatement;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
+import com.datastax.driver.core.Row;
import com.datastax.driver.core.querybuilder.QueryBuilder;
import com.datastax.driver.core.querybuilder.Select;
import com.fasterxml.jackson.databind.JsonNode;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
index e9f808a..ea29f6f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
@@ -29,12 +29,23 @@ import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.relation.*;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntityRelationInfo;
+import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.EntityTypeFilter;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import javax.annotation.Nullable;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer;
@@ -166,129 +177,96 @@ public class BaseRelationService implements RelationService {
}
@Override
- public boolean deleteEntityRelations(EntityId entity) {
- Cache cache = cacheManager.getCache(RELATIONS_CACHE);
- log.trace("Executing deleteEntityRelations [{}]", entity);
- validate(entity);
- List<ListenableFuture<List<EntityRelation>>> inboundRelationsList = new ArrayList<>();
- for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
- inboundRelationsList.add(relationDao.findAllByTo(entity, typeGroup));
- }
- ListenableFuture<List<List<EntityRelation>>> inboundRelations = Futures.allAsList(inboundRelationsList);
- ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelations, (List<List<EntityRelation>> relations) ->
- getBooleans(relations, cache, true));
-
- ListenableFuture<Boolean> inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction());
- boolean inboundDeleteResult = false;
+ public void deleteEntityRelations(EntityId entityId) {
try {
- inboundDeleteResult = inboundFuture.get();
+ deleteEntityRelationsAsync(entityId).get();
} catch (InterruptedException | ExecutionException e) {
- log.error("Error deleting entity inbound relations", e);
- }
-
- List<ListenableFuture<List<EntityRelation>>> outboundRelationsList = new ArrayList<>();
- for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
- outboundRelationsList.add(relationDao.findAllByFrom(entity, typeGroup));
- }
- ListenableFuture<List<List<EntityRelation>>> outboundRelations = Futures.allAsList(outboundRelationsList);
- Futures.transform(outboundRelations, (Function<List<List<EntityRelation>>, List<Boolean>>) relations ->
- getBooleans(relations, cache, false));
-
- boolean outboundDeleteResult = relationDao.deleteOutboundRelations(entity);
- return inboundDeleteResult && outboundDeleteResult;
- }
-
- private List<Boolean> getBooleans(List<List<EntityRelation>> relations, Cache cache, boolean isRemove) {
- List<Boolean> results = new ArrayList<>();
- for (List<EntityRelation> relationList : relations) {
- relationList.stream().forEach(relation -> checkFromDeleteSync(cache, results, relation, isRemove));
- }
- return results;
- }
-
- private void checkFromDeleteSync(Cache cache, List<Boolean> results, EntityRelation relation, boolean isRemove) {
- if (isRemove) {
- results.add(relationDao.deleteRelation(relation));
+ throw new RuntimeException(e);
}
- cacheEviction(relation, cache);
}
@Override
- public ListenableFuture<Boolean> deleteEntityRelationsAsync(EntityId entity) {
+ public ListenableFuture<Void> deleteEntityRelationsAsync(EntityId entityId) {
Cache cache = cacheManager.getCache(RELATIONS_CACHE);
- log.trace("Executing deleteEntityRelationsAsync [{}]", entity);
- validate(entity);
+ log.trace("Executing deleteEntityRelationsAsync [{}]", entityId);
+ validate(entityId);
List<ListenableFuture<List<EntityRelation>>> inboundRelationsList = new ArrayList<>();
for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
- inboundRelationsList.add(relationDao.findAllByTo(entity, typeGroup));
+ inboundRelationsList.add(relationDao.findAllByTo(entityId, typeGroup));
}
- ListenableFuture<List<List<EntityRelation>>> inboundRelations = Futures.allAsList(inboundRelationsList);
- ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelations,
- (AsyncFunction<List<List<EntityRelation>>, List<Boolean>>) relations -> {
- List<ListenableFuture<Boolean>> results = getListenableFutures(relations, cache, true);
- return Futures.allAsList(results);
- });
- ListenableFuture<Boolean> inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction());
+ ListenableFuture<List<List<EntityRelation>>> inboundRelations = Futures.allAsList(inboundRelationsList);
List<ListenableFuture<List<EntityRelation>>> outboundRelationsList = new ArrayList<>();
for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
- outboundRelationsList.add(relationDao.findAllByFrom(entity, typeGroup));
+ outboundRelationsList.add(relationDao.findAllByFrom(entityId, typeGroup));
}
+
ListenableFuture<List<List<EntityRelation>>> outboundRelations = Futures.allAsList(outboundRelationsList);
- Futures.transform(outboundRelations, (AsyncFunction<List<List<EntityRelation>>, List<Boolean>>) relations -> {
- List<ListenableFuture<Boolean>> results = getListenableFutures(relations, cache, false);
- return Futures.allAsList(results);
- });
- ListenableFuture<Boolean> outboundFuture = relationDao.deleteOutboundRelationsAsync(entity);
- return Futures.transform(Futures.allAsList(Arrays.asList(inboundFuture, outboundFuture)), getListToBooleanFunction());
+ ListenableFuture<List<Boolean>> inboundDeletions = Futures.transformAsync(inboundRelations,
+ relations -> {
+ List<ListenableFuture<Boolean>> results = deleteRelationGroupsAsync(relations, cache, true);
+ return Futures.allAsList(results);
+ });
+
+ ListenableFuture<List<Boolean>> outboundDeletions = Futures.transformAsync(outboundRelations,
+ relations -> {
+ List<ListenableFuture<Boolean>> results = deleteRelationGroupsAsync(relations, cache, false);
+ return Futures.allAsList(results);
+ });
+
+ ListenableFuture<List<List<Boolean>>> deletionsFuture = Futures.allAsList(inboundDeletions, outboundDeletions);
+
+ return Futures.transform(Futures.transformAsync(deletionsFuture, (deletions) -> relationDao.deleteOutboundRelationsAsync(entityId)), result -> null);
}
- private List<ListenableFuture<Boolean>> getListenableFutures(List<List<EntityRelation>> relations, Cache cache, boolean isRemove) {
+ private List<ListenableFuture<Boolean>> deleteRelationGroupsAsync(List<List<EntityRelation>> relations, Cache cache, boolean deleteFromDb) {
List<ListenableFuture<Boolean>> results = new ArrayList<>();
for (List<EntityRelation> relationList : relations) {
- relationList.stream().forEach(relation -> checkFromDeleteAsync(cache, results, relation, isRemove));
+ relationList.forEach(relation -> results.add(deleteAsync(cache, relation, deleteFromDb)));
}
return results;
}
- private void checkFromDeleteAsync(Cache cache, List<ListenableFuture<Boolean>> results, EntityRelation relation, boolean isRemove) {
- if (isRemove) {
- results.add(relationDao.deleteRelationAsync(relation));
- }
+ private ListenableFuture<Boolean> deleteAsync(Cache cache, EntityRelation relation, boolean deleteFromDb) {
cacheEviction(relation, cache);
+ if (deleteFromDb) {
+ return relationDao.deleteRelationAsync(relation);
+ } else {
+ return Futures.immediateFuture(false);
+ }
}
private void cacheEviction(EntityRelation relation, Cache cache) {
- List<Object> toAndGroup = new ArrayList<>();
- toAndGroup.add(relation.getTo());
- toAndGroup.add(relation.getTypeGroup());
- cache.evict(toAndGroup);
-
- List<Object> toTypeAndGroup = new ArrayList<>();
- toTypeAndGroup.add(relation.getTo());
- toTypeAndGroup.add(relation.getType());
- toTypeAndGroup.add(relation.getTypeGroup());
- cache.evict(toTypeAndGroup);
-
- List<Object> fromAndGroup = new ArrayList<>();
- fromAndGroup.add(relation.getFrom());
- fromAndGroup.add(relation.getTypeGroup());
- cache.evict(fromAndGroup);
-
- List<Object> fromTypeAndGroup = new ArrayList<>();
- fromTypeAndGroup.add(relation.getFrom());
- fromTypeAndGroup.add(relation.getType());
- fromTypeAndGroup.add(relation.getTypeGroup());
- cache.evict(fromTypeAndGroup);
-
- List<Object> fromToTypeAndGroup = new ArrayList<>();
- fromToTypeAndGroup.add(relation.getFrom());
- fromToTypeAndGroup.add(relation.getTo());
- fromToTypeAndGroup.add(relation.getType());
- fromToTypeAndGroup.add(relation.getTypeGroup());
- cache.evict(fromToTypeAndGroup);
+ List<Object> fromToTypeAndTypeGroup = new ArrayList<>();
+ fromToTypeAndTypeGroup.add(relation.getFrom());
+ fromToTypeAndTypeGroup.add(relation.getTo());
+ fromToTypeAndTypeGroup.add(relation.getType());
+ fromToTypeAndTypeGroup.add(relation.getTypeGroup());
+ cache.evict(fromToTypeAndTypeGroup);
+
+ List<Object> fromTypeAndTypeGroup = new ArrayList<>();
+ fromTypeAndTypeGroup.add(relation.getFrom());
+ fromTypeAndTypeGroup.add(relation.getType());
+ fromTypeAndTypeGroup.add(relation.getTypeGroup());
+ cache.evict(fromTypeAndTypeGroup);
+
+ List<Object> fromAndTypeGroup = new ArrayList<>();
+ fromAndTypeGroup.add(relation.getFrom());
+ fromAndTypeGroup.add(relation.getTypeGroup());
+ cache.evict(fromAndTypeGroup);
+
+ List<Object> toAndTypeGroup = new ArrayList<>();
+ toAndTypeGroup.add(relation.getTo());
+ toAndTypeGroup.add(relation.getTypeGroup());
+ cache.evict(toAndTypeGroup);
+
+ List<Object> toTypeAndTypeGroup = new ArrayList<>();
+ fromTypeAndTypeGroup.add(relation.getTo());
+ fromTypeAndTypeGroup.add(relation.getType());
+ fromTypeAndTypeGroup.add(relation.getTypeGroup());
+ cache.evict(toTypeAndTypeGroup);
}
@Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}")
@@ -315,17 +293,16 @@ public class BaseRelationService implements RelationService {
validate(from);
validateTypeGroup(typeGroup);
ListenableFuture<List<EntityRelation>> relations = relationDao.findAllByFrom(from, typeGroup);
- ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations,
- (AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
+ return Futures.transformAsync(relations,
+ relations1 -> {
List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
- relations1.stream().forEach(relation ->
+ relations1.forEach(relation ->
futures.add(fetchRelationInfoAsync(relation,
- relation2 -> relation2.getTo(),
- (EntityRelationInfo relationInfo, String entityName) -> relationInfo.setToName(entityName)))
+ EntityRelation::getTo,
+ EntityRelationInfo::setToName))
);
return Futures.successfulAsList(futures);
});
- return relationsInfo;
}
@Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup}")
@@ -371,30 +348,27 @@ public class BaseRelationService implements RelationService {
validate(to);
validateTypeGroup(typeGroup);
ListenableFuture<List<EntityRelation>> relations = relationDao.findAllByTo(to, typeGroup);
- ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations,
- (AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
+ return Futures.transformAsync(relations,
+ relations1 -> {
List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
- relations1.stream().forEach(relation ->
+ relations1.forEach(relation ->
futures.add(fetchRelationInfoAsync(relation,
- relation2 -> relation2.getFrom(),
- (EntityRelationInfo relationInfo, String entityName) -> relationInfo.setFromName(entityName)))
+ EntityRelation::getFrom,
+ EntityRelationInfo::setFromName))
);
return Futures.successfulAsList(futures);
});
- return relationsInfo;
}
private ListenableFuture<EntityRelationInfo> fetchRelationInfoAsync(EntityRelation relation,
Function<EntityRelation, EntityId> entityIdGetter,
BiConsumer<EntityRelationInfo, String> entityNameSetter) {
ListenableFuture<String> entityName = entityService.fetchEntityNameAsync(entityIdGetter.apply(relation));
- ListenableFuture<EntityRelationInfo> entityRelationInfo =
- Futures.transform(entityName, (Function<String, EntityRelationInfo>) entityName1 -> {
- EntityRelationInfo entityRelationInfo1 = new EntityRelationInfo(relation);
- entityNameSetter.accept(entityRelationInfo1, entityName1);
- return entityRelationInfo1;
- });
- return entityRelationInfo;
+ return Futures.transform(entityName, entityName1 -> {
+ EntityRelationInfo entityRelationInfo1 = new EntityRelationInfo(relation);
+ entityNameSetter.accept(entityRelationInfo1, entityName1);
+ return entityRelationInfo1;
+ });
}
@Cacheable(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup}")
@@ -429,7 +403,7 @@ public class BaseRelationService implements RelationService {
try {
ListenableFuture<Set<EntityRelation>> relationSet = findRelationsRecursively(params.getEntityId(), params.getDirection(), maxLvl, new ConcurrentHashMap<>());
- return Futures.transform(relationSet, (Function<Set<EntityRelation>, List<EntityRelation>>) input -> {
+ return Futures.transform(relationSet, input -> {
List<EntityRelation> relations = new ArrayList<>();
if (filters == null || filters.isEmpty()) {
relations.addAll(input);
@@ -453,10 +427,10 @@ public class BaseRelationService implements RelationService {
log.trace("Executing findInfoByQuery [{}]", query);
ListenableFuture<List<EntityRelation>> relations = findByQuery(query);
EntitySearchDirection direction = query.getParameters().getDirection();
- ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations,
- (AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
+ return Futures.transformAsync(relations,
+ relations1 -> {
List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
- relations1.stream().forEach(relation ->
+ relations1.forEach(relation ->
futures.add(fetchRelationInfoAsync(relation,
relation2 -> direction == EntitySearchDirection.FROM ? relation2.getTo() : relation2.getFrom(),
(EntityRelationInfo relationInfo, String entityName) -> {
@@ -469,7 +443,6 @@ public class BaseRelationService implements RelationService {
);
return Futures.successfulAsList(futures);
});
- return relationsInfo;
}
protected void validate(EntityRelation relation) {
@@ -575,7 +548,7 @@ public class BaseRelationService implements RelationService {
}
//TODO: try to remove this blocking operation
List<Set<EntityRelation>> relations = Futures.successfulAsList(futures).get();
- relations.forEach(r -> r.forEach(d -> children.add(d)));
+ relations.forEach(r -> r.forEach(children::add));
return Futures.immediateFuture(children);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
index ca1b959..da58b11 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
@@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import java.util.List;
+import java.util.concurrent.ExecutionException;
/**
* Created by ashvayka on 27.04.17.
@@ -47,9 +48,9 @@ public interface RelationService {
ListenableFuture<Boolean> deleteRelationAsync(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);
- boolean deleteEntityRelations(EntityId entity);
+ void deleteEntityRelations(EntityId entity);
- ListenableFuture<Boolean> deleteEntityRelationsAsync(EntityId entity);
+ ListenableFuture<Void> deleteEntityRelationsAsync(EntityId entity);
List<EntityRelation> findByFrom(EntityId from, RelationTypeGroup typeGroup);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
new file mode 100644
index 0000000..14de1a4
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
@@ -0,0 +1,375 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.rule;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.common.data.rule.NodeConnectionInfo;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.dao.service.PaginatedRemover;
+import org.thingsboard.server.dao.service.Validator;
+import org.thingsboard.server.dao.tenant.TenantDao;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+/**
+ * Created by igor on 3/12/18.
+ */
+@Service
+@Slf4j
+public class BaseRuleChainService extends AbstractEntityService implements RuleChainService {
+
+ @Autowired
+ private RuleChainDao ruleChainDao;
+
+ @Autowired
+ private RuleNodeDao ruleNodeDao;
+
+ @Autowired
+ private TenantDao tenantDao;
+
+ @Override
+ public RuleChain saveRuleChain(RuleChain ruleChain) {
+ ruleChainValidator.validate(ruleChain);
+ RuleChain savedRuleChain = ruleChainDao.save(ruleChain);
+ if (ruleChain.isRoot() && ruleChain.getId() == null) {
+ try {
+ createRelation(new EntityRelation(savedRuleChain.getTenantId(), savedRuleChain.getId(),
+ EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN));
+ } catch (ExecutionException | InterruptedException e) {
+ log.warn("[{}] Failed to create tenant to root rule chain relation. from: [{}], to: [{}]",
+ savedRuleChain.getTenantId(), savedRuleChain.getId());
+ throw new RuntimeException(e);
+ }
+ }
+ return savedRuleChain;
+ }
+
+ @Override
+ public boolean setRootRuleChain(RuleChainId ruleChainId) {
+ RuleChain ruleChain = ruleChainDao.findById(ruleChainId.getId());
+ if (!ruleChain.isRoot()) {
+ RuleChain previousRootRuleChain = getRootTenantRuleChain(ruleChain.getTenantId());
+ if (!previousRootRuleChain.getId().equals(ruleChain.getId())) {
+ try {
+ deleteRelation(new EntityRelation(previousRootRuleChain.getTenantId(), previousRootRuleChain.getId(),
+ EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN));
+ previousRootRuleChain.setRoot(false);
+ ruleChainDao.save(previousRootRuleChain);
+ createRelation(new EntityRelation(ruleChain.getTenantId(), ruleChain.getId(),
+ EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN));
+ ruleChain.setRoot(true);
+ ruleChainDao.save(ruleChain);
+ return true;
+ } catch (ExecutionException | InterruptedException e) {
+ log.warn("[{}] Failed to set root rule chain, ruleChainId: [{}]", ruleChainId);
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public RuleChainMetaData saveRuleChainMetaData(RuleChainMetaData ruleChainMetaData) {
+ Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id.");
+ RuleChain ruleChain = findRuleChainById(ruleChainMetaData.getRuleChainId());
+ if (ruleChain == null) {
+ return null;
+ }
+
+ List<RuleNode> nodes = ruleChainMetaData.getNodes();
+ List<RuleNode> toAddOrUpdate = new ArrayList<>();
+ List<RuleNode> toDelete = new ArrayList<>();
+
+ Map<RuleNodeId, Integer> ruleNodeIndexMap = new HashMap<>();
+ if (nodes != null) {
+ for (RuleNode node : nodes) {
+ if (node.getId() != null) {
+ ruleNodeIndexMap.put(node.getId(), nodes.indexOf(node));
+ } else {
+ toAddOrUpdate.add(node);
+ }
+ }
+ }
+
+ List<RuleNode> existingRuleNodes = getRuleChainNodes(ruleChainMetaData.getRuleChainId());
+ for (RuleNode existingNode : existingRuleNodes) {
+ deleteEntityRelations(existingNode.getId());
+ Integer index = ruleNodeIndexMap.get(existingNode.getId());
+ if (index != null) {
+ toAddOrUpdate.add(ruleChainMetaData.getNodes().get(index));
+ } else {
+ toDelete.add(existingNode);
+ }
+ }
+ for (RuleNode node : toAddOrUpdate) {
+ node.setRuleChainId(ruleChain.getId());
+ RuleNode savedNode = ruleNodeDao.save(node);
+ try {
+ createRelation(new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(),
+ EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN));
+ } catch (ExecutionException | InterruptedException e) {
+ log.warn("[{}] Failed to create rule chain to rule node relation. from: [{}], to: [{}]",
+ ruleChainMetaData.getRuleChainId(), savedNode.getId());
+ throw new RuntimeException(e);
+ }
+ int index = nodes.indexOf(node);
+ nodes.set(index, savedNode);
+ ruleNodeIndexMap.put(savedNode.getId(), index);
+ }
+ for (RuleNode node : toDelete) {
+ deleteRuleNode(node.getId());
+ }
+ RuleNodeId firstRuleNodeId = null;
+ if (ruleChainMetaData.getFirstNodeIndex() != null) {
+ firstRuleNodeId = nodes.get(ruleChainMetaData.getFirstNodeIndex()).getId();
+ }
+ if ((ruleChain.getFirstRuleNodeId() != null && !ruleChain.getFirstRuleNodeId().equals(firstRuleNodeId))
+ || (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) {
+ ruleChain.setFirstRuleNodeId(firstRuleNodeId);
+ ruleChainDao.save(ruleChain);
+ }
+ if (ruleChainMetaData.getConnections() != null) {
+ for (NodeConnectionInfo nodeConnection : ruleChainMetaData.getConnections()) {
+ EntityId from = nodes.get(nodeConnection.getFromIndex()).getId();
+ EntityId to = nodes.get(nodeConnection.getToIndex()).getId();
+ String type = nodeConnection.getType();
+ try {
+ createRelation(new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE));
+ } catch (ExecutionException | InterruptedException e) {
+ log.warn("[{}] Failed to create rule node relation. from: [{}], to: [{}]", from, to);
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ if (ruleChainMetaData.getRuleChainConnections() != null) {
+ for (RuleChainConnectionInfo nodeToRuleChainConnection : ruleChainMetaData.getRuleChainConnections()) {
+ EntityId from = nodes.get(nodeToRuleChainConnection.getFromIndex()).getId();
+ EntityId to = nodeToRuleChainConnection.getTargetRuleChainId();
+ String type = nodeToRuleChainConnection.getType();
+ try {
+ createRelation(new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE, nodeToRuleChainConnection.getAdditionalInfo()));
+ } catch (ExecutionException | InterruptedException e) {
+ log.warn("[{}] Failed to create rule node to rule chain relation. from: [{}], to: [{}]", from, to);
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ return loadRuleChainMetaData(ruleChainMetaData.getRuleChainId());
+ }
+
+ @Override
+ public RuleChainMetaData loadRuleChainMetaData(RuleChainId ruleChainId) {
+ Validator.validateId(ruleChainId, "Incorrect rule chain id.");
+ RuleChain ruleChain = findRuleChainById(ruleChainId);
+ if (ruleChain == null) {
+ return null;
+ }
+ RuleChainMetaData ruleChainMetaData = new RuleChainMetaData();
+ ruleChainMetaData.setRuleChainId(ruleChainId);
+ List<RuleNode> ruleNodes = getRuleChainNodes(ruleChainId);
+ Map<RuleNodeId, Integer> ruleNodeIndexMap = new HashMap<>();
+ for (RuleNode node : ruleNodes) {
+ ruleNodeIndexMap.put(node.getId(), ruleNodes.indexOf(node));
+ }
+ ruleChainMetaData.setNodes(ruleNodes);
+ if (ruleChain.getFirstRuleNodeId() != null) {
+ ruleChainMetaData.setFirstNodeIndex(ruleNodeIndexMap.get(ruleChain.getFirstRuleNodeId()));
+ }
+ for (RuleNode node : ruleNodes) {
+ int fromIndex = ruleNodeIndexMap.get(node.getId());
+ List<EntityRelation> nodeRelations = getRuleNodeRelations(node.getId());
+ for (EntityRelation nodeRelation : nodeRelations) {
+ String type = nodeRelation.getType();
+ if (nodeRelation.getTo().getEntityType() == EntityType.RULE_NODE) {
+ RuleNodeId toNodeId = new RuleNodeId(nodeRelation.getTo().getId());
+ int toIndex = ruleNodeIndexMap.get(toNodeId);
+ ruleChainMetaData.addConnectionInfo(fromIndex, toIndex, type);
+ } else if (nodeRelation.getTo().getEntityType() == EntityType.RULE_CHAIN) {
+ RuleChainId targetRuleChainId = new RuleChainId(nodeRelation.getTo().getId());
+ ruleChainMetaData.addRuleChainConnectionInfo(fromIndex, targetRuleChainId, type, nodeRelation.getAdditionalInfo());
+ }
+ }
+ }
+ return ruleChainMetaData;
+ }
+
+ @Override
+ public RuleChain findRuleChainById(RuleChainId ruleChainId) {
+ Validator.validateId(ruleChainId, "Incorrect rule chain id for search request.");
+ return ruleChainDao.findById(ruleChainId.getId());
+ }
+
+ @Override
+ public RuleNode findRuleNodeById(RuleNodeId ruleNodeId) {
+ Validator.validateId(ruleNodeId, "Incorrect rule node id for search request.");
+ return ruleNodeDao.findById(ruleNodeId.getId());
+ }
+
+ @Override
+ public ListenableFuture<RuleChain> findRuleChainByIdAsync(RuleChainId ruleChainId) {
+ Validator.validateId(ruleChainId, "Incorrect rule chain id for search request.");
+ return ruleChainDao.findByIdAsync(ruleChainId.getId());
+ }
+
+ @Override
+ public ListenableFuture<RuleNode> findRuleNodeByIdAsync(RuleNodeId ruleNodeId) {
+ Validator.validateId(ruleNodeId, "Incorrect rule node id for search request.");
+ return ruleNodeDao.findByIdAsync(ruleNodeId.getId());
+ }
+
+ @Override
+ public RuleChain getRootTenantRuleChain(TenantId tenantId) {
+ Validator.validateId(tenantId, "Incorrect tenant id for search request.");
+ List<EntityRelation> relations = relationService.findByFrom(tenantId, RelationTypeGroup.RULE_CHAIN);
+ if (relations != null && !relations.isEmpty()) {
+ EntityRelation relation = relations.get(0);
+ RuleChainId ruleChainId = new RuleChainId(relation.getTo().getId());
+ return findRuleChainById(ruleChainId);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public List<RuleNode> getRuleChainNodes(RuleChainId ruleChainId) {
+ Validator.validateId(ruleChainId, "Incorrect rule chain id for search request.");
+ List<EntityRelation> relations = getRuleChainToNodeRelations(ruleChainId);
+ List<RuleNode> ruleNodes = relations.stream().map(relation -> ruleNodeDao.findById(relation.getTo().getId())).collect(Collectors.toList());
+ return ruleNodes;
+ }
+
+ @Override
+ public List<EntityRelation> getRuleNodeRelations(RuleNodeId ruleNodeId) {
+ Validator.validateId(ruleNodeId, "Incorrect rule node id for search request.");
+ return relationService.findByFrom(ruleNodeId, RelationTypeGroup.RULE_NODE);
+ }
+
+ @Override
+ public TextPageData<RuleChain> findTenantRuleChains(TenantId tenantId, TextPageLink pageLink) {
+ Validator.validateId(tenantId, "Incorrect tenant id for search rule chain request.");
+ Validator.validatePageLink(pageLink, "Incorrect PageLink object for search rule chain request.");
+ List<RuleChain> ruleChains = ruleChainDao.findRuleChainsByTenantId(tenantId.getId(), pageLink);
+ return new TextPageData<>(ruleChains, pageLink);
+ }
+
+ @Override
+ public void deleteRuleChainById(RuleChainId ruleChainId) {
+ Validator.validateId(ruleChainId, "Incorrect rule chain id for delete request.");
+ RuleChain ruleChain = ruleChainDao.findById(ruleChainId.getId());
+ if (ruleChain != null && ruleChain.isRoot()) {
+ throw new DataValidationException("Deletion of Root Tenant Rule Chain is prohibited!");
+ }
+ checkRuleNodesAndDelete(ruleChainId);
+ }
+
+ @Override
+ public void deleteRuleChainsByTenantId(TenantId tenantId) {
+ Validator.validateId(tenantId, "Incorrect tenant id for delete rule chains request.");
+ tenantRuleChainsRemover.removeEntities(tenantId);
+ }
+
+ private void checkRuleNodesAndDelete(RuleChainId ruleChainId) {
+ List<EntityRelation> nodeRelations = getRuleChainToNodeRelations(ruleChainId);
+ for (EntityRelation relation : nodeRelations) {
+ deleteRuleNode(relation.getTo());
+ }
+ deleteEntityRelations(ruleChainId);
+ ruleChainDao.removeById(ruleChainId.getId());
+ }
+
+ private List<EntityRelation> getRuleChainToNodeRelations(RuleChainId ruleChainId) {
+ return relationService.findByFrom(ruleChainId, RelationTypeGroup.RULE_CHAIN);
+ }
+
+ private void deleteRuleNode(EntityId entityId) {
+ deleteEntityRelations(entityId);
+ ruleNodeDao.removeById(entityId.getId());
+ }
+
+ private void createRelation(EntityRelation relation) throws ExecutionException, InterruptedException {
+ log.debug("Creating relation: {}", relation);
+ relationService.saveRelation(relation);
+ }
+
+ private void deleteRelation(EntityRelation relation) throws ExecutionException, InterruptedException {
+ log.debug("Deleting relation: {}", relation);
+ relationService.deleteRelation(relation);
+ }
+
+ private DataValidator<RuleChain> ruleChainValidator =
+ new DataValidator<RuleChain>() {
+ @Override
+ protected void validateDataImpl(RuleChain ruleChain) {
+ if (StringUtils.isEmpty(ruleChain.getName())) {
+ throw new DataValidationException("Rule chain name should be specified!.");
+ }
+ if (ruleChain.getTenantId() == null || ruleChain.getTenantId().isNullUid()) {
+ throw new DataValidationException("Rule chain should be assigned to tenant!");
+ }
+ Tenant tenant = tenantDao.findById(ruleChain.getTenantId().getId());
+ if (tenant == null) {
+ throw new DataValidationException("Rule chain is referencing to non-existent tenant!");
+ }
+ if (ruleChain.isRoot()) {
+ RuleChain rootRuleChain = getRootTenantRuleChain(ruleChain.getTenantId());
+ if (rootRuleChain != null && !rootRuleChain.getId().equals(ruleChain.getId())) {
+ throw new DataValidationException("Another root rule chain is present in scope of current tenant!");
+ }
+ }
+ }
+ };
+
+ private PaginatedRemover<TenantId, RuleChain> tenantRuleChainsRemover =
+ new PaginatedRemover<TenantId, RuleChain>() {
+
+ @Override
+ protected List<RuleChain> findEntities(TenantId id, TextPageLink pageLink) {
+ return ruleChainDao.findRuleChainsByTenantId(id.getId(), pageLink);
+ }
+
+ @Override
+ protected void removeEntity(RuleChain entity) {
+ checkRuleNodesAndDelete(entity.getId());
+ }
+ };
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/CassandraRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/CassandraRuleChainDao.java
new file mode 100644
index 0000000..ff672a7
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/CassandraRuleChainDao.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.rule;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.dao.DaoUtil;
+import org.thingsboard.server.dao.model.nosql.RuleChainEntity;
+import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao;
+import org.thingsboard.server.dao.util.NoSqlDao;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static org.thingsboard.server.dao.model.ModelConstants.RULE_CHAIN_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.RULE_CHAIN_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.RULE_CHAIN_TENANT_ID_PROPERTY;
+
+@Component
+@Slf4j
+@NoSqlDao
+public class CassandraRuleChainDao extends CassandraAbstractSearchTextDao<RuleChainEntity, RuleChain> implements RuleChainDao {
+
+ @Override
+ protected Class<RuleChainEntity> getColumnFamilyClass() {
+ return RuleChainEntity.class;
+ }
+
+ @Override
+ protected String getColumnFamilyName() {
+ return RULE_CHAIN_COLUMN_FAMILY_NAME;
+ }
+
+ @Override
+ public List<RuleChain> findRuleChainsByTenantId(UUID tenantId, TextPageLink pageLink) {
+ log.debug("Try to find rule chains by tenantId [{}] and pageLink [{}]", tenantId, pageLink);
+ List<RuleChainEntity> ruleChainEntities = findPageWithTextSearch(RULE_CHAIN_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Collections.singletonList(eq(RULE_CHAIN_TENANT_ID_PROPERTY, tenantId)),
+ pageLink);
+
+ log.trace("Found rule chains [{}] by tenantId [{}] and pageLink [{}]", ruleChainEntities, tenantId, pageLink);
+ return DaoUtil.convertDataList(ruleChainEntities);
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java b/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java
index c8bb38d..52fcf85 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java
@@ -18,7 +18,6 @@ package org.thingsboard.server.dao.service;
import org.thingsboard.server.common.data.id.IdBased;
import org.thingsboard.server.common.data.page.TimePageLink;
-import java.sql.Time;
import java.util.List;
import java.util.UUID;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
index c8af7cb..1947f4a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
@@ -18,7 +18,6 @@ package org.thingsboard.server.dao.service;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.page.BasePageLink;
-import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import java.util.List;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/CassandraAdminSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/settings/CassandraAdminSettingsDao.java
index 6cd3719..ebc66d7 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/settings/CassandraAdminSettingsDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/settings/CassandraAdminSettingsDao.java
@@ -26,7 +26,9 @@ import org.thingsboard.server.dao.util.NoSqlDao;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_BY_KEY_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_KEY_PROPERTY;
@Component
@Slf4j
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
index 0d64d5c..ff6e213 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
@@ -15,8 +15,6 @@
*/
package org.thingsboard.server.dao.sql.alarm;
-import com.google.common.base.Function;
-import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
@@ -102,12 +100,12 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
}
String relationType = BaseAlarmService.ALARM_RELATION_PREFIX + searchStatusName;
ListenableFuture<List<EntityRelation>> relations = relationDao.findRelations(affectedEntity, relationType, RelationTypeGroup.ALARM, EntityType.ALARM, query.getPageLink());
- return Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<AlarmInfo>>) input -> {
+ return Futures.transformAsync(relations, input -> {
List<ListenableFuture<AlarmInfo>> alarmFutures = new ArrayList<>(input.size());
for (EntityRelation relation : input) {
alarmFutures.add(Futures.transform(
findAlarmByIdAsync(relation.getTo().getId()),
- (Function<Alarm, AlarmInfo>) AlarmInfo::new));
+ AlarmInfo::new));
}
return Futures.successfulAsList(alarmFutures);
});
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
index bf1e289..7887ea8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
@@ -19,8 +19,6 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
-import org.thingsboard.server.common.data.EntitySubtype;
-import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.dao.model.sql.AssetEntity;
import org.thingsboard.server.dao.util.SqlDao;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java
index 9f8f673..fd5817c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java
@@ -31,7 +31,12 @@ import org.thingsboard.server.dao.model.sql.AssetEntity;
import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
import org.thingsboard.server.dao.util.SqlDao;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUIDs;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java
index 6a7d119..4c0ff69 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java
@@ -25,7 +25,6 @@ import org.thingsboard.server.dao.model.sql.ComponentDescriptorEntity;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
-import java.util.UUID;
/**
* Created by Valerii Sosliuk on 5/6/2017.
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java
index f481387..ee55038 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java
@@ -23,7 +23,6 @@ import org.thingsboard.server.dao.model.sql.DashboardInfoEntity;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
-import java.util.UUID;
/**
* Created by Valerii Sosliuk on 5/6/2017.
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java
index 4d8d0b2..7f40741 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java
@@ -15,7 +15,6 @@
*/
package org.thingsboard.server.dao.sql.dashboard;
-import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
@@ -38,7 +37,6 @@ import org.thingsboard.server.dao.relation.RelationDao;
import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
import org.thingsboard.server.dao.util.SqlDao;
-import java.sql.Time;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -86,7 +84,7 @@ public class JpaDashboardInfoDao extends JpaAbstractSearchTextDao<DashboardInfoE
ListenableFuture<List<EntityRelation>> relations = relationDao.findRelations(new CustomerId(customerId), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.DASHBOARD, EntityType.DASHBOARD, pageLink);
- return Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<DashboardInfo>>) input -> {
+ return Futures.transformAsync(relations, input -> {
List<ListenableFuture<DashboardInfo>> dashboardFutures = new ArrayList<>(input.size());
for (EntityRelation relation : input) {
dashboardFutures.add(findByIdAsync(relation.getTo().getId()));
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
index 4f3cd7d..e464c65 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
@@ -32,7 +32,12 @@ import org.thingsboard.server.dao.model.sql.DeviceEntity;
import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
import org.thingsboard.server.dao.util.SqlDao;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUIDs;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java
index 89cd944..07fb5a0 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java
@@ -15,12 +15,17 @@
*/
package org.thingsboard.server.dao.sql.event;
+import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
+import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.dao.model.sql.EventEntity;
import org.thingsboard.server.dao.util.SqlDao;
+import java.util.List;
+
/**
* Created by Valerii Sosliuk on 5/3/2017.
*/
@@ -36,4 +41,14 @@ public interface EventRepository extends CrudRepository<EventEntity, String>, Jp
EventEntity findByTenantIdAndEntityTypeAndEntityId(String tenantId,
EntityType entityType,
String entityId);
+
+ @Query("SELECT e FROM EventEntity e WHERE e.tenantId = :tenantId AND e.entityType = :entityType " +
+ "AND e.entityId = :entityId AND e.eventType = :eventType ORDER BY e.eventType DESC, e.id DESC")
+ List<EventEntity> findLatestByTenantIdAndEntityTypeAndEntityIdAndEventType(
+ @Param("tenantId") String tenantId,
+ @Param("entityType") EntityType entityType,
+ @Param("entityId") String entityId,
+ @Param("eventType") String eventType,
+ Pageable pageable);
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java
index 9b22773..5a63ed8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.dao.sql.event;
import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -36,10 +37,7 @@ import org.thingsboard.server.dao.model.sql.EventEntity;
import org.thingsboard.server.dao.sql.JpaAbstractSearchTimeDao;
import org.thingsboard.server.dao.util.SqlDao;
-import javax.persistence.criteria.CriteriaBuilder;
-import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
-import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -85,6 +83,18 @@ public class JpaBaseEventDao extends JpaAbstractSearchTimeDao<EventEntity, Event
}
@Override
+ public ListenableFuture<Event> saveAsync(Event event) {
+ log.debug("Save event [{}] ", event);
+ if (event.getId() == null) {
+ event.setId(new EventId(UUIDs.timeBased()));
+ }
+ if (StringUtils.isEmpty(event.getUid())) {
+ event.setUid(event.getId().toString());
+ }
+ return service.submit(() -> save(new EventEntity(event), false).orElse(null));
+ }
+
+ @Override
public Optional<Event> saveIfNotExists(Event event) {
return save(new EventEntity(event), true);
}
@@ -92,7 +102,7 @@ public class JpaBaseEventDao extends JpaAbstractSearchTimeDao<EventEntity, Event
@Override
public Event findEvent(UUID tenantId, EntityId entityId, String eventType, String eventUid) {
return DaoUtil.getData(eventRepository.findByTenantIdAndEntityTypeAndEntityIdAndEventTypeAndEventUid(
- UUIDConverter.fromTimeUUID(tenantId), entityId.getEntityType(), UUIDConverter.fromTimeUUID(entityId.getId()), eventType, eventUid));
+ UUIDConverter.fromTimeUUID(tenantId), entityId.getEntityType(), UUIDConverter.fromTimeUUID(entityId.getId()), eventType, eventUid));
}
@Override
@@ -109,6 +119,17 @@ public class JpaBaseEventDao extends JpaAbstractSearchTimeDao<EventEntity, Event
return DaoUtil.convertDataList(eventRepository.findAll(where(timeSearchSpec).and(fieldsSpec), pageable).getContent());
}
+ @Override
+ public List<Event> findLatestEvents(UUID tenantId, EntityId entityId, String eventType, int limit) {
+ List<EventEntity> latest = eventRepository.findLatestByTenantIdAndEntityTypeAndEntityIdAndEventType(
+ UUIDConverter.fromTimeUUID(tenantId),
+ entityId.getEntityType(),
+ UUIDConverter.fromTimeUUID(entityId.getId()),
+ eventType,
+ new PageRequest(0, limit));
+ return DaoUtil.convertDataList(latest);
+ }
+
public Optional<Event> save(EventEntity entity, boolean ifNotExists) {
log.debug("Save event [{}] ", entity);
if (entity.getTenantId() == null) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java
index 6776f56..a8de677 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java
@@ -21,7 +21,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
-import org.springframework.data.domain.Sort.Order;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType;
@@ -38,14 +37,10 @@ import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService;
import org.thingsboard.server.dao.sql.JpaAbstractSearchTimeDao;
import org.thingsboard.server.dao.util.SqlDao;
-import javax.persistence.criteria.CriteriaBuilder;
-import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
-import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.List;
-import static org.springframework.data.domain.Sort.Direction.ASC;
import static org.springframework.data.jpa.domain.Specifications.where;
import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
@@ -132,39 +127,35 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
@Override
public boolean deleteRelation(EntityRelation relation) {
RelationCompositeKey key = new RelationCompositeKey(relation);
- boolean relationExistsBeforeDelete = relationRepository.exists(key);
- relationRepository.delete(key);
- return relationExistsBeforeDelete;
+ return deleteRelationIfExists(key);
}
@Override
public ListenableFuture<Boolean> deleteRelationAsync(EntityRelation relation) {
RelationCompositeKey key = new RelationCompositeKey(relation);
return service.submit(
- () -> {
- boolean relationExistsBeforeDelete = relationRepository.exists(key);
- relationRepository.delete(key);
- return relationExistsBeforeDelete;
- });
+ () -> deleteRelationIfExists(key));
}
@Override
public boolean deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
RelationCompositeKey key = getRelationCompositeKey(from, to, relationType, typeGroup);
- boolean relationExistsBeforeDelete = relationRepository.exists(key);
- relationRepository.delete(key);
- return relationExistsBeforeDelete;
+ return deleteRelationIfExists(key);
}
@Override
public ListenableFuture<Boolean> deleteRelationAsync(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
RelationCompositeKey key = getRelationCompositeKey(from, to, relationType, typeGroup);
return service.submit(
- () -> {
- boolean relationExistsBeforeDelete = relationRepository.exists(key);
- relationRepository.delete(key);
- return relationExistsBeforeDelete;
- });
+ () -> deleteRelationIfExists(key));
+ }
+
+ private boolean deleteRelationIfExists(RelationCompositeKey key) {
+ boolean relationExistsBeforeDelete = relationRepository.exists(key);
+ if (relationExistsBeforeDelete) {
+ relationRepository.delete(key);
+ }
+ return relationExistsBeforeDelete;
}
@Override
@@ -172,7 +163,9 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
boolean relationExistsBeforeDelete = relationRepository
.findAllByFromIdAndFromType(UUIDConverter.fromTimeUUID(entity.getId()), entity.getEntityType().name())
.size() > 0;
- relationRepository.deleteByFromIdAndFromType(UUIDConverter.fromTimeUUID(entity.getId()), entity.getEntityType().name());
+ if (relationExistsBeforeDelete) {
+ relationRepository.deleteByFromIdAndFromType(UUIDConverter.fromTimeUUID(entity.getId()), entity.getEntityType().name());
+ }
return relationExistsBeforeDelete;
}
@@ -183,7 +176,9 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
boolean relationExistsBeforeDelete = relationRepository
.findAllByFromIdAndFromType(UUIDConverter.fromTimeUUID(entity.getId()), entity.getEntityType().name())
.size() > 0;
- relationRepository.deleteByFromIdAndFromType(UUIDConverter.fromTimeUUID(entity.getId()), entity.getEntityType().name());
+ if (relationExistsBeforeDelete) {
+ relationRepository.deleteByFromIdAndFromType(UUIDConverter.fromTimeUUID(entity.getId()), entity.getEntityType().name());
+ }
return relationExistsBeforeDelete;
});
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java
new file mode 100644
index 0000000..fb5d20a
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.sql.rule;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.UUIDConverter;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.dao.DaoUtil;
+import org.thingsboard.server.dao.model.sql.RuleChainEntity;
+import org.thingsboard.server.dao.rule.RuleChainDao;
+import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
+import org.thingsboard.server.dao.util.SqlDao;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR;
+
+@Slf4j
+@Component
+@SqlDao
+public class JpaRuleChainDao extends JpaAbstractSearchTextDao<RuleChainEntity, RuleChain> implements RuleChainDao {
+
+ @Autowired
+ private RuleChainRepository ruleChainRepository;
+
+ @Override
+ protected Class getEntityClass() {
+ return RuleChainEntity.class;
+ }
+
+ @Override
+ protected CrudRepository getCrudRepository() {
+ return ruleChainRepository;
+ }
+
+ @Override
+ public List<RuleChain> findRuleChainsByTenantId(UUID tenantId, TextPageLink pageLink) {
+ return DaoUtil.convertDataList(ruleChainRepository
+ .findByTenantId(
+ UUIDConverter.fromTimeUUID(tenantId),
+ Objects.toString(pageLink.getTextSearch(), ""),
+ pageLink.getIdOffset() == null ? NULL_UUID_STR : UUIDConverter.fromTimeUUID(pageLink.getIdOffset()),
+ new PageRequest(0, pageLink.getLimit())));
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java
new file mode 100644
index 0000000..eec2894
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.sql.rule;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.dao.model.sql.RuleNodeEntity;
+import org.thingsboard.server.dao.rule.RuleNodeDao;
+import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
+import org.thingsboard.server.dao.util.SqlDao;
+
+@Slf4j
+@Component
+@SqlDao
+public class JpaRuleNodeDao extends JpaAbstractSearchTextDao<RuleNodeEntity, RuleNode> implements RuleNodeDao {
+
+ @Autowired
+ private RuleNodeRepository ruleNodeRepository;
+
+ @Override
+ protected Class getEntityClass() {
+ return RuleNodeEntity.class;
+ }
+
+ @Override
+ protected CrudRepository getCrudRepository() {
+ return ruleNodeRepository;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
index 6350352..e5f145f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
@@ -17,7 +17,11 @@ package org.thingsboard.server.dao.sql.timeseries;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
-import com.google.common.util.concurrent.*;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -25,7 +29,11 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.UUIDConverter;
import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.TsKvEntity;
import org.thingsboard.server.dao.model.sql.TsKvLatestCompositeKey;
@@ -42,7 +50,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvLatestRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvLatestRepository.java
index c5e8861..7171cef 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvLatestRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvLatestRepository.java
@@ -22,7 +22,6 @@ import org.thingsboard.server.dao.model.sql.TsKvLatestEntity;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
-import java.util.UUID;
@SqlDao
public interface TsKvLatestRepository extends CrudRepository<TsKvLatestEntity, TsKvLatestCompositeKey> {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/CassandraTenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/CassandraTenantDao.java
index fc44403..1c3a779 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/tenant/CassandraTenantDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/CassandraTenantDao.java
@@ -28,7 +28,9 @@ import java.util.Arrays;
import java.util.List;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_BY_REGION_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_REGION_PROPERTY;
@Component
@Slf4j
diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
index 24abe52..d92c941 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
@@ -30,8 +30,7 @@ import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
-import org.thingsboard.server.dao.plugin.PluginService;
-import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
@@ -71,10 +70,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe
private DashboardService dashboardService;
@Autowired
- private RuleService ruleService;
-
- @Autowired
- private PluginService pluginService;
+ private RuleChainService ruleChainService;
@Override
public Tenant findTenantById(TenantId tenantId) {
@@ -108,8 +104,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe
assetService.deleteAssetsByTenantId(tenantId);
deviceService.deleteDevicesByTenantId(tenantId);
userService.deleteTenantAdmins(tenantId);
- ruleService.deleteRulesByTenantId(tenantId);
- pluginService.deletePluginsByTenantId(tenantId);
+ ruleChainService.deleteRuleChainsByTenantId(tenantId);
tenantDao.removeById(tenantId.getId());
deleteEntityRelations(tenantId);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java
index 99a2bf1..ac5ee64 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java
@@ -18,7 +18,14 @@ package org.thingsboard.server.dao.timeseries;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Row;
import lombok.extern.slf4j.Slf4j;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+import org.thingsboard.server.common.data.kv.DataType;
+import org.thingsboard.server.common.data.kv.DoubleDataEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
import javax.annotation.Nullable;
import java.util.List;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
index cda4b16..7aa317c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
@@ -15,7 +15,11 @@
*/
package org.thingsboard.server.dao.timeseries;
-import com.datastax.driver.core.*;
+import com.datastax.driver.core.BoundStatement;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
+import com.datastax.driver.core.Row;
import com.datastax.driver.core.querybuilder.QueryBuilder;
import com.datastax.driver.core.querybuilder.Select;
import com.google.common.base.Function;
@@ -29,8 +33,17 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DataType;
+import org.thingsboard.server.common.data.kv.DoubleDataEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.nosql.CassandraAbstractAsyncDao;
import org.thingsboard.server.dao.util.NoSqlDao;
@@ -69,6 +82,9 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
@Value("${cassandra.query.ts_key_value_partitioning}")
private String partitioning;
+ @Value("${cassandra.query.ts_key_value_ttl}")
+ private long systemTtl;
+
private TsPartitionDate tsFormat;
private PreparedStatement partitionInsertStmt;
@@ -217,7 +233,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
ListenableFuture<List<Long>> partitionsListFuture = Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor);
- ListenableFuture<List<ResultSet>> aggregationChunks = Futures.transform(partitionsListFuture,
+ ListenableFuture<List<ResultSet>> aggregationChunks = Futures.transformAsync(partitionsListFuture,
getFetchChunksAsyncFunction(entityId, key, aggregation, startTs, endTs), readResultsProcessingExecutor);
return Futures.transform(aggregationChunks, new AggregatePartitionsFunction(aggregation, key, ts), readResultsProcessingExecutor);
@@ -274,6 +290,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
@Override
public ListenableFuture<Void> save(EntityId entityId, TsKvEntry tsKvEntry, long ttl) {
+ ttl = computeTtl(ttl);
long partition = toPartitionTs(tsKvEntry.getTs());
DataType type = tsKvEntry.getDataType();
BoundStatement stmt = (ttl == 0 ? getSaveStmt(type) : getSaveTtlStmt(type)).bind();
@@ -291,6 +308,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
@Override
public ListenableFuture<Void> savePartition(EntityId entityId, long tsKvEntryTs, String key, long ttl) {
+ ttl = computeTtl(ttl);
long partition = toPartitionTs(tsKvEntryTs);
log.debug("Saving partition {} for the entity [{}-{}] and key {}", partition, entityId.getEntityType(), entityId.getId(), key);
BoundStatement stmt = (ttl == 0 ? getPartitionInsertStmt() : getPartitionInsertTtlStmt()).bind();
@@ -304,6 +322,17 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
return getFuture(executeAsyncWrite(stmt), rs -> null);
}
+ private long computeTtl(long ttl) {
+ if (systemTtl > 0) {
+ if (ttl == 0) {
+ ttl = systemTtl;
+ } else {
+ ttl = Math.min(systemTtl, ttl);
+ }
+ }
+ return ttl;
+ }
+
@Override
public ListenableFuture<Void> saveLatest(EntityId entityId, TsKvEntry tsKvEntry) {
BoundStatement stmt = getLatestStmt().bind()
diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
index 350bf4f..e1bd4e0 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
@@ -42,7 +42,9 @@ import org.thingsboard.server.dao.tenant.TenantDao;
import java.util.List;
-import static org.thingsboard.server.dao.service.Validator.*;
+import static org.thingsboard.server.dao.service.Validator.validateId;
+import static org.thingsboard.server.dao.service.Validator.validatePageLink;
+import static org.thingsboard.server.dao.service.Validator.validateString;
@Service
@Slf4j
diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java b/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java
index 03eb46f..817845b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java
@@ -25,7 +25,11 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.thingsboard.server.dao.exception.BufferLimitException;
-import java.util.concurrent.*;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Component
@@ -125,6 +129,9 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
if(permits.get() < permitsLimit) {
reprocessQueue();
}
+ if(permits.get() < permitsLimit) {
+ reprocessQueue();
+ }
return lockedFuture.future;
} catch (InterruptedException e) {
return Futures.immediateFailedFuture(new BufferLimitException());
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetsBundleDao.java
index b5d0e5f..f938a98 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetsBundleDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetsBundleDao.java
@@ -29,8 +29,15 @@ import java.util.Arrays;
import java.util.List;
import java.util.UUID;
-import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.in;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_ALIAS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_BY_TENANT_AND_ALIAS_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_TENANT_ID_PROPERTY;
@Component
@Slf4j
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetTypeDao.java
index 43049f9..e31809f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetTypeDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetTypeDao.java
@@ -29,7 +29,11 @@ import java.util.UUID;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_ALIAS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_BY_TENANT_AND_ALIASES_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_TENANT_ID_PROPERTY;
@Component
@Slf4j
dao/src/main/resources/cassandra/schema.cql 144(+83 -61)
diff --git a/dao/src/main/resources/cassandra/schema.cql b/dao/src/main/resources/cassandra/schema.cql
index fc557a9..f03122a 100644
--- a/dao/src/main/resources/cassandra/schema.cql
+++ b/dao/src/main/resources/cassandra/schema.cql
@@ -428,7 +428,7 @@ CREATE TABLE IF NOT EXISTS thingsboard.attributes_kv_cf (
CREATE TABLE IF NOT EXISTS thingsboard.component_descriptor (
id timeuuid,
- type text, //("FILTER", "PROCESSOR", "ACTION", "PLUGIN")
+ type text,
scope text,
name text,
search_text text,
@@ -459,67 +459,12 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.component_desc_by_id AS
PRIMARY KEY ( id, clazz, scope, type )
WITH CLUSTERING ORDER BY ( clazz ASC, scope ASC, type DESC);
-CREATE TABLE IF NOT EXISTS thingsboard.rule (
- id timeuuid,
- tenant_id timeuuid,
- name text,
- state text,
- search_text text,
- weight int,
- plugin_token text,
- filters text, // Format: {"clazz":"A", "name": "Filter A", "configuration": {"types":["TELEMETRY"]}}
- processor text, // Format: {"clazz":"A", "name": "Processor A", "configuration": null}
- action text, // Format: {"clazz":"A", "name": "Action A", "configuration": null}
- additional_info text,
- PRIMARY KEY (id, tenant_id)
-);
-
-CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.rule_by_plugin_token AS
- SELECT *
-
- FROM thingsboard.rule
- WHERE tenant_id IS NOT NULL AND id IS NOT NULL AND plugin_token IS NOT NULL
- PRIMARY KEY (plugin_token, tenant_id, id) WITH CLUSTERING ORDER BY (tenant_id DESC, id DESC);
-
-CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.rule_by_tenant_and_search_text AS
- SELECT *
- FROM thingsboard.rule
- WHERE tenant_id IS NOT NULL AND id IS NOT NULL AND search_text IS NOT NULL
- PRIMARY KEY (tenant_id, search_text, id) WITH CLUSTERING ORDER BY (search_text ASC);
-
-CREATE TABLE IF NOT EXISTS thingsboard.plugin (
- id uuid,
- tenant_id uuid,
- name text,
- state text,
- search_text text,
- api_token text,
- plugin_class text,
- public_access boolean,
- configuration text,
- additional_info text,
- PRIMARY KEY (id, tenant_id)
-);
-
-CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.plugin_by_api_token AS
- SELECT *
- FROM thingsboard.plugin
- WHERE api_token IS NOT NULL AND id IS NOT NULL AND tenant_id IS NOT NULL
- PRIMARY KEY (api_token, id, tenant_id) WITH CLUSTERING ORDER BY (id DESC);
-
-CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.plugin_by_tenant_and_search_text AS
- SELECT *
- from thingsboard.plugin
- WHERE tenant_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
- PRIMARY KEY ( tenant_id, search_text, id )
- WITH CLUSTERING ORDER BY ( search_text ASC, id DESC );
-
CREATE TABLE IF NOT EXISTS thingsboard.event (
tenant_id timeuuid, // tenant or system
id timeuuid,
event_type text,
event_uid text,
- entity_type text, // (device, customer, rule, plugin)
+ entity_type text,
entity_id timeuuid,
body text,
PRIMARY KEY ((tenant_id, entity_type, entity_id), event_type, event_uid)
@@ -542,7 +487,6 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.event_by_id AS
PRIMARY KEY ((tenant_id, entity_type, entity_id), id, event_type, event_uid)
WITH CLUSTERING ORDER BY (id ASC, event_type ASC, event_uid ASC);
-
CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_entity_id (
tenant_id timeuuid,
id timeuuid,
@@ -591,8 +535,6 @@ CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_user_id (
PRIMARY KEY ((tenant_id, user_id), id)
);
-
-
CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_tenant_id (
tenant_id timeuuid,
id timeuuid,
@@ -615,4 +557,84 @@ CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_tenant_id_partitions (
partition bigint,
PRIMARY KEY (( tenant_id ), partition)
) WITH CLUSTERING ORDER BY ( partition ASC )
-AND compaction = { 'class' : 'LeveledCompactionStrategy' };
\ No newline at end of file
+AND compaction = { 'class' : 'LeveledCompactionStrategy' };
+
+CREATE TABLE IF NOT EXISTS thingsboard.msg_queue (
+ node_id timeuuid,
+ cluster_partition bigint,
+ ts_partition bigint,
+ ts bigint,
+ msg blob,
+ PRIMARY KEY ((node_id, cluster_partition, ts_partition), ts))
+WITH CLUSTERING ORDER BY (ts DESC)
+AND compaction = {
+ 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy',
+ 'min_threshold': '5',
+ 'base_time_seconds': '43200',
+ 'max_window_size_seconds': '43200',
+ 'tombstone_threshold': '0.9',
+ 'unchecked_tombstone_compaction': 'true'
+};
+
+CREATE TABLE IF NOT EXISTS thingsboard.msg_ack_queue (
+ node_id timeuuid,
+ cluster_partition bigint,
+ ts_partition bigint,
+ msg_id timeuuid,
+ PRIMARY KEY ((node_id, cluster_partition, ts_partition), msg_id))
+WITH CLUSTERING ORDER BY (msg_id DESC)
+AND compaction = {
+ 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy',
+ 'min_threshold': '5',
+ 'base_time_seconds': '43200',
+ 'max_window_size_seconds': '43200',
+ 'tombstone_threshold': '0.9',
+ 'unchecked_tombstone_compaction': 'true'
+};
+
+CREATE TABLE IF NOT EXISTS thingsboard.processed_msg_partitions (
+ node_id timeuuid,
+ cluster_partition bigint,
+ ts_partition bigint,
+ PRIMARY KEY ((node_id, cluster_partition), ts_partition))
+WITH CLUSTERING ORDER BY (ts_partition DESC)
+AND compaction = {
+ 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy',
+ 'min_threshold': '5',
+ 'base_time_seconds': '43200',
+ 'max_window_size_seconds': '43200',
+ 'tombstone_threshold': '0.9',
+ 'unchecked_tombstone_compaction': 'true'
+};
+
+CREATE TABLE IF NOT EXISTS thingsboard.rule_chain (
+ id uuid,
+ tenant_id uuid,
+ name text,
+ search_text text,
+ first_rule_node_id uuid,
+ root boolean,
+ debug_mode boolean,
+ configuration text,
+ additional_info text,
+ PRIMARY KEY (id, tenant_id)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.rule_chain_by_tenant_and_search_text AS
+ SELECT *
+ from thingsboard.rule_chain
+ WHERE tenant_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( tenant_id, search_text, id )
+ WITH CLUSTERING ORDER BY ( search_text ASC, id DESC );
+
+CREATE TABLE IF NOT EXISTS thingsboard.rule_node (
+ id uuid,
+ rule_chain_id uuid,
+ type text,
+ name text,
+ debug_mode boolean,
+ search_text text,
+ configuration text,
+ additional_info text,
+ PRIMARY KEY (id)
+);
diff --git a/dao/src/main/resources/cassandra/system-data.cql b/dao/src/main/resources/cassandra/system-data.cql
index 2455ac6..16a53ca 100644
--- a/dao/src/main/resources/cassandra/system-data.cql
+++ b/dao/src/main/resources/cassandra/system-data.cql
@@ -279,28 +279,4 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'basic_timeseries',
'{"type":"timeseries","sizeX":8,"sizeY":5,"resources":[],"templateHtml":"","templateCss":".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n","controllerScript":"self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true);\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n","settingsSchema":"{}","dataKeySettingsSchema":"{}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"}',
'Timeseries - Flot' );
-
-/** System plugins and rules **/
-INSERT INTO thingsboard.plugin ( id, tenant_id, name, state, search_text, api_token, plugin_class, public_access,
-configuration )
-VALUES ( minTimeuuid ( '2016-11-01 01:01:01+0000' ), minTimeuuid ( 0 ), 'System Telemetry Plugin', 'ACTIVE',
-'system telemetry plugin', 'telemetry',
-'org.thingsboard.server.extensions.core.plugin.telemetry.TelemetryStoragePlugin', true, '{}' );
-
-INSERT INTO thingsboard.rule ( id, tenant_id, name, plugin_token, state, search_text, weight, filters, processor,
-action )
-VALUES ( minTimeuuid ( '2016-11-01 01:01:02+0000' ), minTimeuuid ( 0 ), 'System Telemetry Rule', 'telemetry', 'ACTIVE',
-'system telemetry rule', 0,
-'[{"clazz":"org.thingsboard.server.extensions.core.filter.MsgTypeFilter", "name":"TelemetryFilter", "configuration": {"messageTypes":["POST_TELEMETRY","POST_ATTRIBUTES","GET_ATTRIBUTES"]}}]',
-null,
-'{"clazz":"org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction", "name":"TelemetryMsgConverterAction", "configuration":{}}'
-);
-
-INSERT INTO thingsboard.plugin ( id, tenant_id, name, state, search_text, api_token, plugin_class, public_access,
-configuration )
-VALUES ( minTimeuuid ( '2016-11-01 01:01:03+0000' ), minTimeuuid ( 0 ), 'System RPC Plugin', 'ACTIVE',
-'system rpc plugin', 'rpc', 'org.thingsboard.server.extensions.core.plugin.rpc.RpcPlugin', true, '{
- "defaultTimeout": 20000
- }' );
-
/** SYSTEM **/
dao/src/main/resources/sql/schema.sql 52(+24 -28)
diff --git a/dao/src/main/resources/sql/schema.sql b/dao/src/main/resources/sql/schema.sql
index 9f03dc8..91e77da 100644
--- a/dao/src/main/resources/sql/schema.sql
+++ b/dao/src/main/resources/sql/schema.sql
@@ -140,19 +140,6 @@ CREATE TABLE IF NOT EXISTS event (
CONSTRAINT event_unq_key UNIQUE (tenant_id, entity_type, entity_id, event_type, event_uid)
);
-CREATE TABLE IF NOT EXISTS plugin (
- id varchar(31) NOT NULL CONSTRAINT plugin_pkey PRIMARY KEY,
- additional_info varchar,
- api_token varchar(255),
- plugin_class varchar(255),
- configuration varchar,
- name varchar(255),
- public_access boolean,
- search_text varchar(255),
- state varchar(255),
- tenant_id varchar(31)
-);
-
CREATE TABLE IF NOT EXISTS relation (
from_id varchar(31),
from_type varchar(255),
@@ -164,20 +151,6 @@ CREATE TABLE IF NOT EXISTS relation (
CONSTRAINT relation_unq_key UNIQUE (from_id, from_type, relation_type_group, relation_type, to_id, to_type)
);
-CREATE TABLE IF NOT EXISTS rule (
- id varchar(31) NOT NULL CONSTRAINT rule_pkey PRIMARY KEY,
- action varchar,
- additional_info varchar,
- filters varchar,
- name varchar(255),
- plugin_token varchar(255),
- processor varchar,
- search_text varchar(255),
- state varchar(255),
- tenant_id varchar(31),
- weight integer
-);
-
CREATE TABLE IF NOT EXISTS tb_user (
id varchar(31) NOT NULL CONSTRAINT tb_user_pkey PRIMARY KEY,
additional_info varchar,
@@ -254,4 +227,27 @@ CREATE TABLE IF NOT EXISTS widgets_bundle (
search_text varchar(255),
tenant_id varchar(31),
title varchar(255)
-);
\ No newline at end of file
+);
+
+CREATE TABLE IF NOT EXISTS rule_chain (
+ id varchar(31) NOT NULL CONSTRAINT rule_chain_pkey PRIMARY KEY,
+ additional_info varchar,
+ configuration varchar(10000000),
+ name varchar(255),
+ first_rule_node_id varchar(31),
+ root boolean,
+ debug_mode boolean,
+ search_text varchar(255),
+ tenant_id varchar(31)
+);
+
+CREATE TABLE IF NOT EXISTS rule_node (
+ id varchar(31) NOT NULL CONSTRAINT rule_node_pkey PRIMARY KEY,
+ rule_chain_id varchar(31),
+ additional_info varchar,
+ configuration varchar(10000000),
+ type varchar(255),
+ name varchar(255),
+ debug_mode boolean,
+ search_text varchar(255)
+);
dao/src/main/resources/sql/system-data.sql 20(+0 -20)
diff --git a/dao/src/main/resources/sql/system-data.sql b/dao/src/main/resources/sql/system-data.sql
index 39fd6bd..d41521a 100644
--- a/dao/src/main/resources/sql/system-data.sql
+++ b/dao/src/main/resources/sql/system-data.sql
@@ -42,23 +42,3 @@ VALUES ( '1e746126eaaefa6a91992ebcb67fe33', 'mail', '{
"username": "",
"password": ""
}' );
-
-/** System plugins and rules **/
-INSERT INTO plugin ( id, tenant_id, name, state, search_text, api_token, plugin_class, public_access, configuration )
-VALUES ( '1e7461160cb2da2a91992ebcb67fe33', '1b21dd2138140008080808080808080', 'System Telemetry Plugin', 'ACTIVE',
- 'system telemetry plugin', 'telemetry',
- 'org.thingsboard.server.extensions.core.plugin.telemetry.TelemetryStoragePlugin', true, '{}' );
-
-INSERT INTO rule ( id, tenant_id, name, plugin_token, state, search_text, weight, filters, processor, action )
-VALUES ( '1e7461165abad4ca91992ebcb67fe33', '1b21dd2138140008080808080808080', 'System Telemetry Rule', 'telemetry', 'ACTIVE',
- 'system telemetry rule', 0,
- '[{"clazz":"org.thingsboard.server.extensions.core.filter.MsgTypeFilter", "name":"TelemetryFilter", "configuration": {"messageTypes":["POST_TELEMETRY","POST_ATTRIBUTES","GET_ATTRIBUTES"]}}]',
- null,
- '{"clazz":"org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction", "name":"TelemetryMsgConverterAction", "configuration":{}}'
-);
-
-INSERT INTO plugin ( id, tenant_id, name, state, search_text, api_token, plugin_class, public_access, configuration )
-VALUES ( '1e746116b3b8994a91992ebcb67fe33', '1b21dd2138140008080808080808080', 'System RPC Plugin', 'ACTIVE',
- 'system rpc plugin', 'rpc', 'org.thingsboard.server.extensions.core.plugin.rpc.RpcPlugin', true, '{
- "defaultTimeout": 20000
- }' );
diff --git a/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java b/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java
index d214a70..1dc3faa 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java
@@ -15,18 +15,17 @@
*/
package org.thingsboard.server.dao;
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.Session;
import org.cassandraunit.BaseCassandraUnit;
import org.cassandraunit.CQLDataLoader;
import org.cassandraunit.dataset.CQLDataSet;
import org.cassandraunit.utils.EmbeddedCassandraServerHelper;
-import com.datastax.driver.core.Cluster;
-import com.datastax.driver.core.Session;
-
import java.util.List;
public class CustomCassandraCQLUnit extends BaseCassandraUnit {
- private List<CQLDataSet> dataSets;
+ protected List<CQLDataSet> dataSets;
public Session session;
public Cluster cluster;
diff --git a/dao/src/test/java/org/thingsboard/server/dao/JpaDbunitTestConfig.java b/dao/src/test/java/org/thingsboard/server/dao/JpaDbunitTestConfig.java
index d83293c..0bf9f19 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/JpaDbunitTestConfig.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/JpaDbunitTestConfig.java
@@ -19,7 +19,6 @@ import com.github.springtestdbunit.bean.DatabaseConfigBean;
import com.github.springtestdbunit.bean.DatabaseDataSourceConnectionFactoryBean;
import org.dbunit.DatabaseUnitException;
import org.dbunit.ext.hsqldb.HsqldbDataTypeFactory;
-import org.dbunit.ext.postgresql.PostgresqlDataTypeFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
diff --git a/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java b/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java
index f49668d..491409a 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java
@@ -15,7 +15,12 @@
*/
package org.thingsboard.server.dao.nosql;
-import com.datastax.driver.core.*;
+import com.datastax.driver.core.ProtocolVersion;
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
+import com.datastax.driver.core.Row;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.Statement;
import com.datastax.driver.core.exceptions.UnsupportedFeatureException;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@@ -29,10 +34,18 @@ import org.mockito.stubbing.Answer;
import org.thingsboard.server.dao.exception.BufferLimitException;
import org.thingsboard.server.dao.util.AsyncRateLimiter;
-import java.util.concurrent.*;
-
-import static org.junit.Assert.*;
-import static org.mockito.Mockito.*;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeoutException;
+
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class RateLimitedResultSetFutureTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java
index 7e25baa..f10462d 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java
@@ -17,7 +17,6 @@ package org.thingsboard.server.dao;
import org.cassandraunit.dataset.cql.ClassPathCQLDataSet;
import org.junit.ClassRule;
-import org.junit.Ignore;
import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters;
import org.junit.runner.RunWith;
@@ -26,7 +25,9 @@ import java.util.Arrays;
@RunWith(ClasspathSuite.class)
@ClassnameFilters({
- "org.thingsboard.server.dao.service.*ServiceNoSqlTest"
+ "org.thingsboard.server.dao.service.*ServiceNoSqlTest",
+ "org.thingsboard.server.dao.service.queue.cassandra.*.*.*Test",
+ "org.thingsboard.server.dao.service.queue.cassandra.*Test"
})
public class NoSqlDaoServiceTestSuite {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
index ffa9f0e..1e1f15d 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
@@ -18,8 +18,6 @@ package org.thingsboard.server.dao.service;
import com.datastax.driver.core.utils.UUIDs;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.fasterxml.jackson.databind.node.TextNode;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
@@ -38,8 +36,6 @@ import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentScope;
import org.thingsboard.server.common.data.plugin.ComponentType;
-import org.thingsboard.server.common.data.plugin.PluginMetaData;
-import org.thingsboard.server.common.data.rule.RuleMetaData;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
@@ -50,9 +46,8 @@ import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.event.EventService;
-import org.thingsboard.server.dao.plugin.PluginService;
import org.thingsboard.server.dao.relation.RelationService;
-import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
@@ -64,8 +59,6 @@ import java.io.IOException;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.ThreadLocalRandom;
@RunWith(SpringRunner.class)
@@ -111,12 +104,6 @@ public abstract class AbstractServiceTest {
protected TimeseriesService tsService;
@Autowired
- protected PluginService pluginService;
-
- @Autowired
- protected RuleService ruleService;
-
- @Autowired
protected EventService eventService;
@Autowired
@@ -126,6 +113,9 @@ public abstract class AbstractServiceTest {
protected AlarmService alarmService;
@Autowired
+ protected RuleChainService ruleChainService;
+
+ @Autowired
private ComponentDescriptorService componentDescriptorService;
class IdComparator<D extends BaseData<? extends UUIDBased>> implements Comparator<D> {
@@ -149,32 +139,6 @@ public abstract class AbstractServiceTest {
return event;
}
- protected PluginMetaData generatePlugin(TenantId tenantId, String token) throws IOException {
- return generatePlugin(tenantId, token, "org.thingsboard.component.PluginTest", "org.thingsboard.component.ActionTest", "TestJsonDescriptor.json", "TestJsonData.json");
- }
-
- protected PluginMetaData generatePlugin(TenantId tenantId, String token, String clazz, String actions, String configurationDescriptorResource, String dataResource) throws IOException {
- if (tenantId == null) {
- tenantId = new TenantId(UUIDs.timeBased());
- }
- if (token == null) {
- token = UUID.randomUUID().toString();
- }
- getOrCreateDescriptor(ComponentScope.TENANT, ComponentType.PLUGIN, clazz, configurationDescriptorResource, actions);
- PluginMetaData pluginMetaData = new PluginMetaData();
- pluginMetaData.setName("Testing");
- pluginMetaData.setClazz(clazz);
- pluginMetaData.setTenantId(tenantId);
- pluginMetaData.setApiToken(token);
- pluginMetaData.setAdditionalInfo(mapper.readTree("{\"test\":\"test\"}"));
- try {
- pluginMetaData.setConfiguration(readFromResource(dataResource));
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- return pluginMetaData;
- }
-
private ComponentDescriptor getOrCreateDescriptor(ComponentScope scope, ComponentType type, String clazz, String configurationDescriptorResource) throws IOException {
return getOrCreateDescriptor(scope, type, clazz, configurationDescriptorResource, null);
}
@@ -198,42 +162,6 @@ public abstract class AbstractServiceTest {
return mapper.readTree(this.getClass().getClassLoader().getResourceAsStream(resourceName));
}
- protected RuleMetaData generateRule(TenantId tenantId, Integer weight, String pluginToken) throws IOException {
- if (tenantId == null) {
- tenantId = new TenantId(UUIDs.timeBased());
- }
- if (weight == null) {
- weight = ThreadLocalRandom.current().nextInt();
- }
-
- RuleMetaData ruleMetaData = new RuleMetaData();
- ruleMetaData.setName("Testing");
- ruleMetaData.setTenantId(tenantId);
- ruleMetaData.setWeight(weight);
- ruleMetaData.setPluginToken(pluginToken);
-
- ruleMetaData.setAction(createNode(ComponentScope.TENANT, ComponentType.ACTION,
- "org.thingsboard.component.ActionTest", "TestJsonDescriptor.json", "TestJsonData.json"));
- ruleMetaData.setProcessor(createNode(ComponentScope.TENANT, ComponentType.PROCESSOR,
- "org.thingsboard.component.ProcessorTest", "TestJsonDescriptor.json", "TestJsonData.json"));
- ruleMetaData.setFilters(mapper.createArrayNode().add(
- createNode(ComponentScope.TENANT, ComponentType.FILTER,
- "org.thingsboard.component.FilterTest", "TestJsonDescriptor.json", "TestJsonData.json")
- ));
-
- ruleMetaData.setAdditionalInfo(mapper.readTree("{}"));
- return ruleMetaData;
- }
-
- protected JsonNode createNode(ComponentScope scope, ComponentType type, String clazz, String configurationDescriptor, String configuration) throws IOException {
- getOrCreateDescriptor(scope, type, clazz, configurationDescriptor);
- ObjectNode oNode = mapper.createObjectNode();
- oNode.set("name", new TextNode("test action"));
- oNode.set("clazz", new TextNode(clazz));
- oNode.set("configuration", readFromResource(configuration));
- return oNode;
- }
-
@Bean
public AuditLogLevelFilter auditLogLevelFilter() {
Map<String,String> mask = new HashMap<>();
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java
index 4ea39f0..41ba94f 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java
@@ -16,7 +16,6 @@
package org.thingsboard.server.dao.service;
import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Assert;
import org.junit.Test;
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java
index fd071e9..03099e9 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java
@@ -21,7 +21,11 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.common.data.Tenant;
-import org.thingsboard.server.common.data.alarm.*;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmInfo;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TimePageData;
@@ -168,7 +172,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest {
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());
- alarmService.clearAlarm(created.getId(), System.currentTimeMillis()).get();
+ alarmService.clearAlarm(created.getId(), null, System.currentTimeMillis()).get();
created = alarmService.findAlarmByIdAsync(created.getId()).get();
alarms = alarmService.findAlarms(AlarmQuery.builder()
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java
index 993e525..02b5186 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java
@@ -32,10 +32,8 @@ import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.exception.DataValidationException;
-import org.thingsboard.server.dao.model.ModelConstants;
import java.io.IOException;
-import java.sql.Time;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java
index 61fbf05..9269fc3 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java
@@ -36,7 +36,10 @@ import org.thingsboard.server.dao.device.DeviceService;
import java.util.UUID;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java
index 032534e..1d771b2 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java
@@ -16,7 +16,6 @@
package org.thingsboard.server.dao.service;
import com.datastax.driver.core.utils.UUIDs;
-import org.apache.commons.lang3.RandomStringUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java
index 3a0b6ae..bc78fd7 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java
@@ -34,7 +34,10 @@ import org.thingsboard.server.dao.relation.RelationService;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import static org.thingsboard.server.common.data.CacheConstants.RELATIONS_CACHE;
public abstract class BaseRelationCacheTest extends AbstractServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java
index 754454a..743aefc 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java
@@ -24,12 +24,12 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.relation.EntityRelation;
-import org.thingsboard.server.common.data.relation.RelationTypeGroup;
-import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.EntityTypeFilter;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
+import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.Collections;
import java.util.List;
@@ -96,7 +96,7 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest {
saveRelation(relationA);
saveRelation(relationB);
- Assert.assertTrue(relationService.deleteEntityRelationsAsync(childId).get());
+ Assert.assertNull(relationService.deleteEntityRelationsAsync(childId).get());
Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java
new file mode 100644
index 0000000..28c6147
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java
@@ -0,0 +1,362 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.thingsboard.server.dao.service;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.dao.exception.DataValidationException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Created by igor on 3/13/18.
+ */
+public abstract class BaseRuleChainServiceTest extends AbstractServiceTest {
+
+ private IdComparator<RuleChain> idComparator = new IdComparator<>();
+ private IdComparator<RuleNode> ruleNodeIdComparator = new IdComparator<>();
+
+ private TenantId tenantId;
+
+ @Before
+ public void before() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = tenantService.saveTenant(tenant);
+ Assert.assertNotNull(savedTenant);
+ tenantId = savedTenant.getId();
+ }
+
+ @After
+ public void after() {
+ tenantService.deleteTenant(tenantId);
+ }
+
+ @Test
+ public void testSaveRuleChain() throws IOException {
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setTenantId(tenantId);
+ ruleChain.setName("My RuleChain");
+
+ RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain);
+ Assert.assertNotNull(savedRuleChain);
+ Assert.assertNotNull(savedRuleChain.getId());
+ Assert.assertTrue(savedRuleChain.getCreatedTime() > 0);
+ Assert.assertEquals(ruleChain.getTenantId(), savedRuleChain.getTenantId());
+ Assert.assertEquals(ruleChain.getName(), savedRuleChain.getName());
+
+ savedRuleChain.setName("My new RuleChain");
+
+ ruleChainService.saveRuleChain(savedRuleChain);
+ RuleChain foundRuleChain = ruleChainService.findRuleChainById(savedRuleChain.getId());
+ Assert.assertEquals(foundRuleChain.getName(), savedRuleChain.getName());
+
+ ruleChainService.deleteRuleChainById(savedRuleChain.getId());
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveRuleChainWithEmptyName() {
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setTenantId(tenantId);
+ ruleChainService.saveRuleChain(ruleChain);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveRuleChainWithInvalidTenant() {
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setName("My RuleChain");
+ ruleChain.setTenantId(new TenantId(UUIDs.timeBased()));
+ ruleChainService.saveRuleChain(ruleChain);
+ }
+
+ @Test
+ public void testFindRuleChainById() {
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setTenantId(tenantId);
+ ruleChain.setName("My RuleChain");
+ RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain);
+ RuleChain foundRuleChain = ruleChainService.findRuleChainById(savedRuleChain.getId());
+ Assert.assertNotNull(foundRuleChain);
+ Assert.assertEquals(savedRuleChain, foundRuleChain);
+ ruleChainService.deleteRuleChainById(savedRuleChain.getId());
+ }
+
+ @Test
+ public void testDeleteRuleChain() {
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setTenantId(tenantId);
+ ruleChain.setName("My RuleChain");
+ RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain);
+ RuleChain foundRuleChain = ruleChainService.findRuleChainById(savedRuleChain.getId());
+ Assert.assertNotNull(foundRuleChain);
+ ruleChainService.deleteRuleChainById(savedRuleChain.getId());
+ foundRuleChain = ruleChainService.findRuleChainById(savedRuleChain.getId());
+ Assert.assertNull(foundRuleChain);
+ }
+
+ @Test
+ public void testFindRuleChainsByTenantId() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Test tenant");
+ tenant = tenantService.saveTenant(tenant);
+
+ TenantId tenantId = tenant.getId();
+
+ List<RuleChain> ruleChains = new ArrayList<>();
+ for (int i = 0; i < 165; i++) {
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setTenantId(tenantId);
+ ruleChain.setName("RuleChain" + i);
+ ruleChains.add(ruleChainService.saveRuleChain(ruleChain));
+ }
+
+ List<RuleChain> loadedRuleChains = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(16);
+ TextPageData<RuleChain> pageData = null;
+ do {
+ pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink);
+ loadedRuleChains.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(ruleChains, idComparator);
+ Collections.sort(loadedRuleChains, idComparator);
+
+ Assert.assertEquals(ruleChains, loadedRuleChains);
+
+ ruleChainService.deleteRuleChainsByTenantId(tenantId);
+
+ pageLink = new TextPageLink(31);
+ pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertTrue(pageData.getData().isEmpty());
+
+ tenantService.deleteTenant(tenantId);
+ }
+
+ @Test
+ public void testFindRuleChainsByTenantIdAndName() {
+ String name1 = "RuleChain name 1";
+ List<RuleChain> ruleChainsName1 = new ArrayList<>();
+ for (int i = 0; i < 123; i++) {
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int) (Math.random() * 17));
+ String name = name1 + suffix;
+ name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+ ruleChain.setName(name);
+ ruleChainsName1.add(ruleChainService.saveRuleChain(ruleChain));
+ }
+ String name2 = "RuleChain name 2";
+ List<RuleChain> ruleChainsName2 = new ArrayList<>();
+ for (int i = 0; i < 193; i++) {
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int) (Math.random() * 15));
+ String name = name2 + suffix;
+ name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+ ruleChain.setName(name);
+ ruleChainsName2.add(ruleChainService.saveRuleChain(ruleChain));
+ }
+
+ List<RuleChain> loadedRuleChainsName1 = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(19, name1);
+ TextPageData<RuleChain> pageData = null;
+ do {
+ pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink);
+ loadedRuleChainsName1.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(ruleChainsName1, idComparator);
+ Collections.sort(loadedRuleChainsName1, idComparator);
+
+ Assert.assertEquals(ruleChainsName1, loadedRuleChainsName1);
+
+ List<RuleChain> loadedRuleChainsName2 = new ArrayList<>();
+ pageLink = new TextPageLink(4, name2);
+ do {
+ pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink);
+ loadedRuleChainsName2.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(ruleChainsName2, idComparator);
+ Collections.sort(loadedRuleChainsName2, idComparator);
+
+ Assert.assertEquals(ruleChainsName2, loadedRuleChainsName2);
+
+ for (RuleChain ruleChain : loadedRuleChainsName1) {
+ ruleChainService.deleteRuleChainById(ruleChain.getId());
+ }
+
+ pageLink = new TextPageLink(4, name1);
+ pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ for (RuleChain ruleChain : loadedRuleChainsName2) {
+ ruleChainService.deleteRuleChainById(ruleChain.getId());
+ }
+
+ pageLink = new TextPageLink(4, name2);
+ pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+ }
+
+ @Test
+ public void testSaveRuleChainMetaData() throws Exception {
+
+ RuleChainMetaData savedRuleChainMetaData = createRuleChainMetadata();
+
+ Assert.assertEquals(3, savedRuleChainMetaData.getNodes().size());
+ Assert.assertEquals(3, savedRuleChainMetaData.getConnections().size());
+
+ for (RuleNode ruleNode : savedRuleChainMetaData.getNodes()) {
+ Assert.assertNotNull(ruleNode.getId());
+ List<EntityRelation> relations = ruleChainService.getRuleNodeRelations(ruleNode.getId());
+ if ("name1".equals(ruleNode.getName())) {
+ Assert.assertEquals(2, relations.size());
+ } else if ("name2".equals(ruleNode.getName())) {
+ Assert.assertEquals(1, relations.size());
+ } else if ("name3".equals(ruleNode.getName())) {
+ Assert.assertEquals(0, relations.size());
+ }
+ }
+
+ List<RuleNode> loadedRuleNodes = ruleChainService.getRuleChainNodes(savedRuleChainMetaData.getRuleChainId());
+
+ Collections.sort(savedRuleChainMetaData.getNodes(), ruleNodeIdComparator);
+ Collections.sort(loadedRuleNodes, ruleNodeIdComparator);
+
+ Assert.assertEquals(savedRuleChainMetaData.getNodes(), loadedRuleNodes);
+
+ ruleChainService.deleteRuleChainById(savedRuleChainMetaData.getRuleChainId());
+ }
+
+ @Test
+ public void testUpdateRuleChainMetaData() throws Exception {
+ RuleChainMetaData savedRuleChainMetaData = createRuleChainMetadata();
+
+ List<RuleNode> ruleNodes = savedRuleChainMetaData.getNodes();
+ int name3Index = -1;
+ for (int i=0;i<ruleNodes.size();i++) {
+ if ("name3".equals(ruleNodes.get(i).getName())) {
+ name3Index = i;
+ break;
+ }
+ }
+
+ RuleNode ruleNode4 = new RuleNode();
+ ruleNode4.setName("name4");
+ ruleNode4.setType("type4");
+ ruleNode4.setConfiguration(mapper.readTree("\"key4\": \"val4\""));
+
+ ruleNodes.set(name3Index, ruleNode4);
+
+ RuleChainMetaData updatedRuleChainMetaData = ruleChainService.saveRuleChainMetaData(savedRuleChainMetaData);
+
+ Assert.assertEquals(3, updatedRuleChainMetaData.getNodes().size());
+ Assert.assertEquals(3, updatedRuleChainMetaData.getConnections().size());
+
+ for (RuleNode ruleNode : updatedRuleChainMetaData.getNodes()) {
+ Assert.assertNotNull(ruleNode.getId());
+ List<EntityRelation> relations = ruleChainService.getRuleNodeRelations(ruleNode.getId());
+ if ("name1".equals(ruleNode.getName())) {
+ Assert.assertEquals(2, relations.size());
+ } else if ("name2".equals(ruleNode.getName())) {
+ Assert.assertEquals(1, relations.size());
+ } else if ("name4".equals(ruleNode.getName())) {
+ Assert.assertEquals(0, relations.size());
+ }
+ }
+
+ List<RuleNode> loadedRuleNodes = ruleChainService.getRuleChainNodes(savedRuleChainMetaData.getRuleChainId());
+
+ Collections.sort(updatedRuleChainMetaData.getNodes(), ruleNodeIdComparator);
+ Collections.sort(loadedRuleNodes, ruleNodeIdComparator);
+
+ Assert.assertEquals(updatedRuleChainMetaData.getNodes(), loadedRuleNodes);
+
+ ruleChainService.deleteRuleChainById(savedRuleChainMetaData.getRuleChainId());
+ }
+
+ private RuleChainMetaData createRuleChainMetadata() throws Exception {
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setName("My RuleChain");
+ ruleChain.setTenantId(tenantId);
+ RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain);
+
+ RuleChainMetaData ruleChainMetaData = new RuleChainMetaData();
+ ruleChainMetaData.setRuleChainId(savedRuleChain.getId());
+
+ ObjectMapper mapper = new ObjectMapper();
+
+ RuleNode ruleNode1 = new RuleNode();
+ ruleNode1.setName("name1");
+ ruleNode1.setType("type1");
+ ruleNode1.setConfiguration(mapper.readTree("\"key1\": \"val1\""));
+
+ RuleNode ruleNode2 = new RuleNode();
+ ruleNode2.setName("name2");
+ ruleNode2.setType("type2");
+ ruleNode2.setConfiguration(mapper.readTree("\"key2\": \"val2\""));
+
+ RuleNode ruleNode3 = new RuleNode();
+ ruleNode3.setName("name3");
+ ruleNode3.setType("type3");
+ ruleNode3.setConfiguration(mapper.readTree("\"key3\": \"val3\""));
+
+ List<RuleNode> ruleNodes = new ArrayList<>();
+ ruleNodes.add(ruleNode1);
+ ruleNodes.add(ruleNode2);
+ ruleNodes.add(ruleNode3);
+ ruleChainMetaData.setFirstNodeIndex(0);
+ ruleChainMetaData.setNodes(ruleNodes);
+
+ ruleChainMetaData.addConnectionInfo(0,1,"success");
+ ruleChainMetaData.addConnectionInfo(0,2,"fail");
+ ruleChainMetaData.addConnectionInfo(1,2,"success");
+
+ return ruleChainService.saveRuleChainMetaData(ruleChainMetaData);
+ }
+
+
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DaoNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DaoNoSqlTest.java
index 9263bc4..82a1a57 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/DaoNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/DaoNoSqlTest.java
@@ -17,7 +17,12 @@ package org.thingsboard.server.dao.service;
import org.springframework.test.context.TestPropertySource;
-import java.lang.annotation.*;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DaoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DaoSqlTest.java
index 285c653..ea95650 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/DaoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/DaoSqlTest.java
@@ -17,7 +17,12 @@ package org.thingsboard.server.dao.service;
import org.springframework.test.context.TestPropertySource;
-import java.lang.annotation.*;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java
index 62af5a5..eb25c86 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java
@@ -20,10 +20,10 @@ import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EventId;
-import org.thingsboard.server.common.data.id.RuleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
@@ -66,15 +66,15 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest {
long endTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 0).toEpochSecond(ZoneOffset.UTC);
long timeAfterEndTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 30).toEpochSecond(ZoneOffset.UTC);
- RuleId ruleId = new RuleId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
TenantId tenantId = new TenantId(UUIDs.timeBased());
- saveEventWithProvidedTime(timeBeforeStartTime, ruleId, tenantId);
- Event savedEvent = saveEventWithProvidedTime(eventTime, ruleId, tenantId);
- Event savedEvent2 = saveEventWithProvidedTime(eventTime+1, ruleId, tenantId);
- Event savedEvent3 = saveEventWithProvidedTime(eventTime+2, ruleId, tenantId);
- saveEventWithProvidedTime(timeAfterEndTime, ruleId, tenantId);
+ saveEventWithProvidedTime(timeBeforeStartTime, customerId, tenantId);
+ Event savedEvent = saveEventWithProvidedTime(eventTime, customerId, tenantId);
+ Event savedEvent2 = saveEventWithProvidedTime(eventTime+1, customerId, tenantId);
+ Event savedEvent3 = saveEventWithProvidedTime(eventTime+2, customerId, tenantId);
+ saveEventWithProvidedTime(timeAfterEndTime, customerId, tenantId);
- TimePageData<Event> events = eventService.findEvents(tenantId, ruleId, DataConstants.STATS,
+ TimePageData<Event> events = eventService.findEvents(tenantId, customerId, DataConstants.STATS,
new TimePageLink(2, startTime, endTime, true));
Assert.assertNotNull(events.getData());
@@ -84,7 +84,7 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest {
Assert.assertTrue(events.hasNext());
Assert.assertNotNull(events.getNextPageLink());
- events = eventService.findEvents(tenantId, ruleId, DataConstants.STATS, events.getNextPageLink());
+ events = eventService.findEvents(tenantId, customerId, DataConstants.STATS, events.getNextPageLink());
Assert.assertNotNull(events.getData());
Assert.assertTrue(events.getData().size() == 1);
@@ -101,15 +101,15 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest {
long endTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 0).toEpochSecond(ZoneOffset.UTC);
long timeAfterEndTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 30).toEpochSecond(ZoneOffset.UTC);
- RuleId ruleId = new RuleId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
TenantId tenantId = new TenantId(UUIDs.timeBased());
- saveEventWithProvidedTime(timeBeforeStartTime, ruleId, tenantId);
- Event savedEvent = saveEventWithProvidedTime(eventTime, ruleId, tenantId);
- Event savedEvent2 = saveEventWithProvidedTime(eventTime+1, ruleId, tenantId);
- Event savedEvent3 = saveEventWithProvidedTime(eventTime+2, ruleId, tenantId);
- saveEventWithProvidedTime(timeAfterEndTime, ruleId, tenantId);
+ saveEventWithProvidedTime(timeBeforeStartTime, customerId, tenantId);
+ Event savedEvent = saveEventWithProvidedTime(eventTime, customerId, tenantId);
+ Event savedEvent2 = saveEventWithProvidedTime(eventTime+1, customerId, tenantId);
+ Event savedEvent3 = saveEventWithProvidedTime(eventTime+2, customerId, tenantId);
+ saveEventWithProvidedTime(timeAfterEndTime, customerId, tenantId);
- TimePageData<Event> events = eventService.findEvents(tenantId, ruleId, DataConstants.STATS,
+ TimePageData<Event> events = eventService.findEvents(tenantId, customerId, DataConstants.STATS,
new TimePageLink(2, startTime, endTime, false));
Assert.assertNotNull(events.getData());
@@ -119,7 +119,7 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest {
Assert.assertTrue(events.hasNext());
Assert.assertNotNull(events.getNextPageLink());
- events = eventService.findEvents(tenantId, ruleId, DataConstants.STATS, events.getNextPageLink());
+ events = eventService.findEvents(tenantId, customerId, DataConstants.STATS, events.getNextPageLink());
Assert.assertNotNull(events.getData());
Assert.assertTrue(events.getData().size() == 1);
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AdminSettingsServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AdminSettingsServiceNoSqlTest.java
index c30f864..97e9e81 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AdminSettingsServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AdminSettingsServiceNoSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.nosql;
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.dao.service.BaseAdminSettingsServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
@DaoNoSqlTest
public class AdminSettingsServiceNoSqlTest extends BaseAdminSettingsServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AlarmServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AlarmServiceNoSqlTest.java
index f822adb..499d3c6 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AlarmServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AlarmServiceNoSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.nosql;
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.dao.service.BaseAlarmServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
@DaoNoSqlTest
public class AlarmServiceNoSqlTest extends BaseAlarmServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AssetServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AssetServiceNoSqlTest.java
index 3887788..9543560 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AssetServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AssetServiceNoSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.nosql;
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.dao.service.BaseAssetServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
@DaoNoSqlTest
public class AssetServiceNoSqlTest extends BaseAssetServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/CustomerServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/CustomerServiceNoSqlTest.java
index 99c77de..905b723 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/CustomerServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/CustomerServiceNoSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.nosql;
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.dao.service.BaseCustomerServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
@DaoNoSqlTest
public class CustomerServiceNoSqlTest extends BaseCustomerServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DashboardServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DashboardServiceNoSqlTest.java
index 3234805..9712ee8 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DashboardServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DashboardServiceNoSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.nosql;
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.dao.service.BaseDashboardServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
@DaoNoSqlTest
public class DashboardServiceNoSqlTest extends BaseDashboardServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialCacheNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialCacheNoSqlTest.java
index 218993f..7c1356d 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialCacheNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialCacheNoSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.nosql;
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.dao.service.BaseDeviceCredentialsCacheTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
@DaoNoSqlTest
public class DeviceCredentialCacheNoSqlTest extends BaseDeviceCredentialsCacheTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialServiceNoSqlTest.java
index a12658e..6b8cf64 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialServiceNoSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.nosql;
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.dao.service.BaseDeviceCredentialsServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
@DaoNoSqlTest
public class DeviceCredentialServiceNoSqlTest extends BaseDeviceCredentialsServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceServiceNoSqlTest.java
index 5d01278..99e327c 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceServiceNoSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.nosql;
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.dao.service.BaseDeviceServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
@DaoNoSqlTest
public class DeviceServiceNoSqlTest extends BaseDeviceServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationServiceNoSqlTest.java
index 48d2026..74728fb 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationServiceNoSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.nosql;
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.dao.service.BaseRelationServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
@DaoNoSqlTest
public class RelationServiceNoSqlTest extends BaseRelationServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/TenantServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/TenantServiceNoSqlTest.java
index e19cd51..de474fd 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/TenantServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/TenantServiceNoSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.nosql;
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.dao.service.BaseTenantServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
@DaoNoSqlTest
public class TenantServiceNoSqlTest extends BaseTenantServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/UserServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/UserServiceNoSqlTest.java
index 0aa7175..af3c497 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/UserServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/UserServiceNoSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.nosql;
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.dao.service.BaseUserServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
@DaoNoSqlTest
public class UserServiceNoSqlTest extends BaseUserServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetsBundleServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetsBundleServiceNoSqlTest.java
index 00b131d..79b5507 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetsBundleServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetsBundleServiceNoSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.nosql;
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.dao.service.BaseWidgetsBundleServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
@DaoNoSqlTest
public class WidgetsBundleServiceNoSqlTest extends BaseWidgetsBundleServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetTypeServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetTypeServiceNoSqlTest.java
index 98d867c..90399c4 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetTypeServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetTypeServiceNoSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.nosql;
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.dao.service.BaseWidgetTypeServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
@DaoNoSqlTest
public class WidgetTypeServiceNoSqlTest extends BaseWidgetTypeServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/AdminSettingsServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AdminSettingsServiceSqlTest.java
index d5a69c0..aa2015f 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/AdminSettingsServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AdminSettingsServiceSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.sql;
-import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.service.BaseAdminSettingsServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class AdminSettingsServiceSqlTest extends BaseAdminSettingsServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/AlarmServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AlarmServiceSqlTest.java
index 38d6080..7b2d3a7 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/AlarmServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AlarmServiceSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.sql;
-import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.service.BaseAlarmServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class AlarmServiceSqlTest extends BaseAlarmServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/AssetServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AssetServiceSqlTest.java
index 3020cec..fc77fe3 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/AssetServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AssetServiceSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.sql;
-import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.service.BaseAssetServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class AssetServiceSqlTest extends BaseAssetServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/CustomerServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/CustomerServiceSqlTest.java
index c900cc4..48b4bac 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/CustomerServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/CustomerServiceSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.sql;
-import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.service.BaseCustomerServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class CustomerServiceSqlTest extends BaseCustomerServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DashboardServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DashboardServiceSqlTest.java
index 66aee43..4c6a2bb 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DashboardServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DashboardServiceSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.sql;
-import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.service.BaseDashboardServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class DashboardServiceSqlTest extends BaseDashboardServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsCacheSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsCacheSqlTest.java
index 27febf8..2f534f6 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsCacheSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsCacheSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.sql;
-import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.service.BaseDeviceCredentialsCacheTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class DeviceCredentialsCacheSqlTest extends BaseDeviceCredentialsCacheTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsServiceSqlTest.java
index f44c004..78e84eb 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsServiceSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.sql;
-import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.service.BaseDeviceCredentialsServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class DeviceCredentialsServiceSqlTest extends BaseDeviceCredentialsServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceServiceSqlTest.java
index f076237..5576a37 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceServiceSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.sql;
-import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.service.BaseDeviceServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class DeviceServiceSqlTest extends BaseDeviceServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/RelationServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/RelationServiceSqlTest.java
index b2410db..a88ecd2 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/RelationServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/RelationServiceSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.sql;
-import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.service.BaseRelationServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class RelationServiceSqlTest extends BaseRelationServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/TenantServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/TenantServiceSqlTest.java
index 30c2d8a..abd847f 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/TenantServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/TenantServiceSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.sql;
-import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.service.BaseTenantServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class TenantServiceSqlTest extends BaseTenantServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/UserServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/UserServiceSqlTest.java
index 2e9007c..b81c0be 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/UserServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/UserServiceSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.sql;
-import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.service.BaseUserServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class UserServiceSqlTest extends BaseUserServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/WidgetsBundleServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/WidgetsBundleServiceSqlTest.java
index 79a7e7b..0b78bad 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/WidgetsBundleServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/WidgetsBundleServiceSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.sql;
-import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.service.BaseWidgetsBundleServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class WidgetsBundleServiceSqlTest extends BaseWidgetsBundleServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/WidgetTypeServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/WidgetTypeServiceSqlTest.java
index de2b0e6..03b188c 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/WidgetTypeServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/WidgetTypeServiceSqlTest.java
@@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service.sql;
-import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.service.BaseWidgetTypeServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class WidgetTypeServiceSqlTest extends BaseWidgetTypeServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
index 0cb3f7f..f045bf1 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
@@ -20,7 +20,15 @@ import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+import org.thingsboard.server.common.data.kv.DoubleDataEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.service.AbstractServiceTest;
import java.util.ArrayList;
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java
index 19674f8..8139c3b 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java
@@ -35,7 +35,9 @@ import java.util.UUID;
import java.util.concurrent.ExecutionException;
import static junit.framework.TestCase.assertFalse;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
/**
* Created by Valerii Sosliuk on 5/21/2017.
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDaoTest.java
index 9874eff..cdc5515 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDaoTest.java
@@ -41,23 +41,23 @@ public class JpaBaseComponentDescriptorDaoTest extends AbstractJpaDaoTest {
@Test
public void findByType() {
for (int i = 0; i < 20; i++) {
- createComponentDescriptor(ComponentType.PLUGIN, ComponentScope.SYSTEM, i);
+ createComponentDescriptor(ComponentType.FILTER, ComponentScope.SYSTEM, i);
createComponentDescriptor(ComponentType.ACTION, ComponentScope.TENANT, i + 20);
}
TextPageLink pageLink1 = new TextPageLink(15, "COMPONENT_");
- List<ComponentDescriptor> components1 = componentDescriptorDao.findByTypeAndPageLink(ComponentType.PLUGIN, pageLink1);
+ List<ComponentDescriptor> components1 = componentDescriptorDao.findByTypeAndPageLink(ComponentType.FILTER, pageLink1);
assertEquals(15, components1.size());
TextPageLink pageLink2 = new TextPageLink(15, "COMPONENT_", components1.get(14).getId().getId(), null);
- List<ComponentDescriptor> components2 = componentDescriptorDao.findByTypeAndPageLink(ComponentType.PLUGIN, pageLink2);
+ List<ComponentDescriptor> components2 = componentDescriptorDao.findByTypeAndPageLink(ComponentType.FILTER, pageLink2);
assertEquals(5, components2.size());
}
@Test
public void findByTypeAndSocpe() {
for (int i = 0; i < 20; i++) {
- createComponentDescriptor(ComponentType.PLUGIN, ComponentScope.SYSTEM, i);
+ createComponentDescriptor(ComponentType.ENRICHMENT, ComponentScope.SYSTEM, i);
createComponentDescriptor(ComponentType.ACTION, ComponentScope.TENANT, i + 20);
createComponentDescriptor(ComponentType.FILTER, ComponentScope.SYSTEM, i + 40);
}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java
index aa80339..0d48d43 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java
@@ -20,7 +20,6 @@ import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.DashboardInfo;
-import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageLink;
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java
index 67841f5..84ae307 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java
@@ -36,7 +36,10 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import static org.thingsboard.server.common.data.DataConstants.ALARM;
import static org.thingsboard.server.common.data.DataConstants.STATS;
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java
index 43b87b4..bf36711 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java
@@ -27,7 +27,7 @@ import org.thingsboard.server.dao.tenant.TenantDao;
import java.util.List;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
/**
* Created by Valerii Sosliuk on 4/30/2017.
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserDaoTest.java
index 67491a2..bacd73d 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserDaoTest.java
@@ -34,7 +34,8 @@ import java.io.IOException;
import java.util.List;
import java.util.UUID;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
/**
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java
index 47cf034..12487a8 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java
@@ -31,7 +31,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleDao;
import java.util.List;
import java.util.UUID;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
/**
diff --git a/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java b/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java
index 67c3ce8..366cefd 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java
@@ -15,7 +15,11 @@
*/
package org.thingsboard.server.dao.util;
-import com.google.common.util.concurrent.*;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
import org.junit.Test;
import org.thingsboard.server.dao.exception.BufferLimitException;
@@ -25,7 +29,10 @@ import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
public class BufferedRateLimiterTest {
diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties
index 42b71f8..f2dab45 100644
--- a/dao/src/test/resources/application-test.properties
+++ b/dao/src/test/resources/application-test.properties
@@ -28,3 +28,5 @@ redis.connection.host=localhost
redis.connection.port=6379
redis.connection.db=0
redis.connection.password=
+
+rule.queue.type=memory
diff --git a/dao/src/test/resources/cassandra/system-test.cql b/dao/src/test/resources/cassandra/system-test.cql
index da5d1f1..de4e325 100644
--- a/dao/src/test/resources/cassandra/system-test.cql
+++ b/dao/src/test/resources/cassandra/system-test.cql
@@ -1,2 +1,27 @@
-TRUNCATE thingsboard.plugin;
-TRUNCATE thingsboard.rule;
\ No newline at end of file
+TRUNCATE thingsboard.rule_chain;
+TRUNCATE thingsboard.rule_node;
+
+-- msg_queue dataset
+
+INSERT INTO thingsboard.msg_queue (node_id, cluster_partition, ts_partition, ts, msg)
+ VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 200, 201, null);
+INSERT INTO thingsboard.msg_queue (node_id, cluster_partition, ts_partition, ts, msg)
+ VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 200, 202, null);
+INSERT INTO thingsboard.msg_queue (node_id, cluster_partition, ts_partition, ts, msg)
+ VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 300, 301, null);
+
+-- ack_queue dataset
+INSERT INTO thingsboard.msg_ack_queue (node_id, cluster_partition, ts_partition, msg_id)
+ VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 300, bebaeb60-1888-11e8-bf21-65b5d5335ba9);
+INSERT INTO thingsboard.msg_ack_queue (node_id, cluster_partition, ts_partition, msg_id)
+ VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 300, 12baeb60-1888-11e8-bf21-65b5d5335ba9);
+INSERT INTO thingsboard.msg_ack_queue (node_id, cluster_partition, ts_partition, msg_id)
+ VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 200, 32baeb60-1888-11e8-bf21-65b5d5335ba9);
+
+-- processed partition dataset
+INSERT INTO thingsboard.processed_msg_partitions (node_id, cluster_partition, ts_partition)
+ VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 100);
+INSERT INTO thingsboard.processed_msg_partitions (node_id, cluster_partition, ts_partition)
+ VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 777);
+INSERT INTO thingsboard.processed_msg_partitions (node_id, cluster_partition, ts_partition)
+ VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 202, 200);
\ No newline at end of file
diff --git a/dao/src/test/resources/cassandra-test.properties b/dao/src/test/resources/cassandra-test.properties
index 737687f..cf07b22 100644
--- a/dao/src/test/resources/cassandra-test.properties
+++ b/dao/src/test/resources/cassandra-test.properties
@@ -46,6 +46,8 @@ cassandra.query.default_fetch_size=2000
cassandra.query.ts_key_value_partitioning=HOURS
+cassandra.query.ts_key_value_ttl=0
+
cassandra.query.max_limit_per_request=1000
cassandra.query.buffer_size=100000
cassandra.query.concurrent_limit=1000
diff --git a/dao/src/test/resources/cassandra-test.yaml b/dao/src/test/resources/cassandra-test.yaml
index 6463f64..e60f248 100644
--- a/dao/src/test/resources/cassandra-test.yaml
+++ b/dao/src/test/resources/cassandra-test.yaml
@@ -103,6 +103,8 @@ commitlog_directory: target/embeddedCassandra/commitlog
hints_directory: target/embeddedCassandra/hints
+cdc_raw_directory: target/embeddedCassandra/cdc
+
# policy for data disk failures:
# stop: shut down gossip and Thrift, leaving the node effectively dead, but
# can still be inspected via JMX.
diff --git a/dao/src/test/resources/nosql-test.properties b/dao/src/test/resources/nosql-test.properties
index 482c6e7..e37e228 100644
--- a/dao/src/test/resources/nosql-test.properties
+++ b/dao/src/test/resources/nosql-test.properties
@@ -1 +1,6 @@
-database.type=cassandra
\ No newline at end of file
+database.type=cassandra
+
+cassandra.queue.partitioning=HOURS
+cassandra.queue.ack.ttl=3600
+cassandra.queue.msg.ttl=3600
+cassandra.queue.partitions.ttl=3600
\ No newline at end of file
diff --git a/dao/src/test/resources/sql/drop-all-tables.sql b/dao/src/test/resources/sql/drop-all-tables.sql
index dfdc90f..23b6a56 100644
--- a/dao/src/test/resources/sql/drop-all-tables.sql
+++ b/dao/src/test/resources/sql/drop-all-tables.sql
@@ -9,13 +9,13 @@ DROP TABLE IF EXISTS dashboard;
DROP TABLE IF EXISTS device;
DROP TABLE IF EXISTS device_credentials;
DROP TABLE IF EXISTS event;
-DROP TABLE IF EXISTS plugin;
DROP TABLE IF EXISTS relation;
-DROP TABLE IF EXISTS rule;
DROP TABLE IF EXISTS tb_user;
DROP TABLE IF EXISTS tenant;
DROP TABLE IF EXISTS ts_kv;
DROP TABLE IF EXISTS ts_kv_latest;
DROP TABLE IF EXISTS user_credentials;
DROP TABLE IF EXISTS widget_type;
-DROP TABLE IF EXISTS widgets_bundle;
\ No newline at end of file
+DROP TABLE IF EXISTS widgets_bundle;
+DROP TABLE IF EXISTS rule_node;
+DROP TABLE IF EXISTS rule_chain;
\ No newline at end of file
diff --git a/dao/src/test/resources/sql/system-test.sql b/dao/src/test/resources/sql/system-test.sql
index 57d49dd..e59afd9 100644
--- a/dao/src/test/resources/sql/system-test.sql
+++ b/dao/src/test/resources/sql/system-test.sql
@@ -1,2 +1,2 @@
-TRUNCATE TABLE plugin;
-TRUNCATE TABLE rule;
\ No newline at end of file
+TRUNCATE TABLE rule_node;
+TRUNCATE TABLE rule_chain;
\ No newline at end of file
docker/cassandra/Makefile 2(+1 -1)
diff --git a/docker/cassandra/Makefile b/docker/cassandra/Makefile
index adc9c7e..e84be0f 100644
--- a/docker/cassandra/Makefile
+++ b/docker/cassandra/Makefile
@@ -1,4 +1,4 @@
-VERSION=1.4.0
+VERSION=2.0.0
PROJECT=thingsboard
APP=cassandra
docker/cassandra-setup/Makefile 2(+1 -1)
diff --git a/docker/cassandra-setup/Makefile b/docker/cassandra-setup/Makefile
index fbe3ff9..c572987 100644
--- a/docker/cassandra-setup/Makefile
+++ b/docker/cassandra-setup/Makefile
@@ -1,4 +1,4 @@
-VERSION=1.4.0
+VERSION=2.0.0
PROJECT=thingsboard
APP=cassandra-setup
docker/cluster-mode-thirdparty.yml 45(+45 -0)
diff --git a/docker/cluster-mode-thirdparty.yml b/docker/cluster-mode-thirdparty.yml
new file mode 100644
index 0000000..3aa5bb5
--- /dev/null
+++ b/docker/cluster-mode-thirdparty.yml
@@ -0,0 +1,45 @@
+#
+# Copyright © 2016-2018 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+version: '3.3'
+services:
+ zookeeper:
+ image: wurstmeister/zookeeper
+ networks:
+ - core
+ ports:
+ - "2181:2181"
+
+ cassandra:
+ image: cassandra:3.11.2
+ networks:
+ - core
+ ports:
+ - "7199:7199"
+ - "9160:9160"
+ - "9042:9042"
+
+ redis:
+ image: redis:4.0
+ networks:
+ - core
+ command: redis-server --maxclients 2000
+ ports:
+ - "6379:6379"
+
+networks:
+ core:
+
docker/docker-compose.yml 2(+1 -1)
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 6662a8f..28323b7 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -18,7 +18,7 @@ version: '2'
services:
tb:
- image: "thingsboard/application:1.4.0"
+ image: "thingsboard/application:2.0.0"
ports:
- "8080:8080"
- "1883:1883"
docker/k8s/cassandra.yaml 2(+1 -1)
diff --git a/docker/k8s/cassandra.yaml b/docker/k8s/cassandra.yaml
index 15d0e55..d6264ae 100644
--- a/docker/k8s/cassandra.yaml
+++ b/docker/k8s/cassandra.yaml
@@ -54,7 +54,7 @@ spec:
topologyKey: "kubernetes.io/hostname"
containers:
- name: cassandra
- image: thingsboard/cassandra:1.4.0
+ image: thingsboard/cassandra:2.0.0
imagePullPolicy: Always
ports:
- containerPort: 7000
docker/k8s/cassandra-setup.yaml 2(+1 -1)
diff --git a/docker/k8s/cassandra-setup.yaml b/docker/k8s/cassandra-setup.yaml
index bf50ba9..2f07238 100644
--- a/docker/k8s/cassandra-setup.yaml
+++ b/docker/k8s/cassandra-setup.yaml
@@ -22,7 +22,7 @@ spec:
containers:
- name: cassandra-setup
imagePullPolicy: Always
- image: thingsboard/cassandra-setup:1.4.0
+ image: thingsboard/cassandra-setup:2.0.0
env:
- name: ADD_DEMO_DATA
value: "true"
docker/k8s/tb.yaml 2(+1 -1)
diff --git a/docker/k8s/tb.yaml b/docker/k8s/tb.yaml
index 1d94cb1..2a4e6f8 100644
--- a/docker/k8s/tb.yaml
+++ b/docker/k8s/tb.yaml
@@ -84,7 +84,7 @@ spec:
containers:
- name: tb
imagePullPolicy: Always
- image: thingsboard/application:1.4.0
+ image: thingsboard/application:2.0.0
ports:
- containerPort: 8080
name: ui
docker/k8s/zookeeper.yaml 2(+1 -1)
diff --git a/docker/k8s/zookeeper.yaml b/docker/k8s/zookeeper.yaml
index bea0833..b5c6dce 100644
--- a/docker/k8s/zookeeper.yaml
+++ b/docker/k8s/zookeeper.yaml
@@ -87,7 +87,7 @@ spec:
containers:
- name: zk
imagePullPolicy: Always
- image: thingsboard/zk:1.4.0
+ image: thingsboard/zk:2.0.0
ports:
- containerPort: 2181
name: client
docker/tb/Makefile 2(+1 -1)
diff --git a/docker/tb/Makefile b/docker/tb/Makefile
index 05d3f4d..cfff5a4 100644
--- a/docker/tb/Makefile
+++ b/docker/tb/Makefile
@@ -1,4 +1,4 @@
-VERSION=1.4.0
+VERSION=2.0.0
PROJECT=thingsboard
APP=application
docker/zookeeper/Makefile 2(+1 -1)
diff --git a/docker/zookeeper/Makefile b/docker/zookeeper/Makefile
index 3386ff8..52b80fb 100644
--- a/docker/zookeeper/Makefile
+++ b/docker/zookeeper/Makefile
@@ -1,4 +1,4 @@
-VERSION=1.4.0
+VERSION=2.0.0
PROJECT=thingsboard
APP=zk
netty-mqtt/.gitignore 7(+7 -0)
diff --git a/netty-mqtt/.gitignore b/netty-mqtt/.gitignore
new file mode 100644
index 0000000..4d2302b
--- /dev/null
+++ b/netty-mqtt/.gitignore
@@ -0,0 +1,7 @@
+.idea/
+*.ipr
+*.iws
+*.ids
+*.iml
+logs
+target
netty-mqtt/pom.xml 99(+99 -0)
diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml
new file mode 100644
index 0000000..411fc53
--- /dev/null
+++ b/netty-mqtt/pom.xml
@@ -0,0 +1,99 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.thingsboard</groupId>
+ <version>2.0.0-SNAPSHOT</version>
+ <artifactId>thingsboard</artifactId>
+ </parent>
+ <groupId>org.thingsboard</groupId>
+ <artifactId>netty-mqtt</artifactId>
+ <version>2.0.0-SNAPSHOT</version>
+ <packaging>jar</packaging>
+
+ <name>Netty MQTT Client</name>
+ <url>https://thingsboard.io</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <main.dir>${basedir}/..</main.dir>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-codec-mqtt</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-handler</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.code.findbugs</groupId>
+ <artifactId>jsr305</artifactId>
+ <version>3.0.1</version>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+ </dependencies>
+
+ <distributionManagement>
+ <repository>
+ <id>jk-5-maven</id>
+ <name>jk-5's maven server</name>
+ <url>sftp://10.2.1.2/opt/maven</url>
+ </repository>
+ </distributionManagement>
+
+ <build>
+ <extensions>
+ <extension>
+ <groupId>org.apache.maven.wagon</groupId>
+ <artifactId>wagon-ssh</artifactId>
+ <version>2.6</version>
+ </extension>
+ </extensions>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>3.1</version>
+ <configuration>
+ <source>1.8</source>
+ <target>1.8</target>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <version>2.4</version>
+ <configuration>
+ <archive>
+ <manifest>
+ <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+ </manifest>
+ </archive>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
\ No newline at end of file
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java
new file mode 100644
index 0000000..ef5e7a5
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java
@@ -0,0 +1,269 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import com.google.common.collect.ImmutableSet;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.mqtt.*;
+import io.netty.util.CharsetUtil;
+import io.netty.util.concurrent.Promise;
+
+final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage> {
+
+ private final MqttClientImpl client;
+ private final Promise<MqttConnectResult> connectFuture;
+
+ MqttChannelHandler(MqttClientImpl client, Promise<MqttConnectResult> connectFuture) {
+ this.client = client;
+ this.connectFuture = connectFuture;
+ }
+
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, MqttMessage msg) throws Exception {
+ switch (msg.fixedHeader().messageType()) {
+ case CONNACK:
+ handleConack(ctx.channel(), (MqttConnAckMessage) msg);
+ break;
+ case SUBACK:
+ handleSubAck((MqttSubAckMessage) msg);
+ break;
+ case PUBLISH:
+ handlePublish(ctx.channel(), (MqttPublishMessage) msg);
+ break;
+ case UNSUBACK:
+ handleUnsuback((MqttUnsubAckMessage) msg);
+ break;
+ case PUBACK:
+ handlePuback((MqttPubAckMessage) msg);
+ break;
+ case PUBREC:
+ handlePubrec(ctx.channel(), msg);
+ break;
+ case PUBREL:
+ handlePubrel(ctx.channel(), msg);
+ break;
+ case PUBCOMP:
+ handlePubcomp(msg);
+ break;
+ }
+ }
+
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) throws Exception {
+ super.channelActive(ctx);
+
+ MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.CONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0);
+ MqttConnectVariableHeader variableHeader = new MqttConnectVariableHeader(
+ this.client.getClientConfig().getProtocolVersion().protocolName(), // Protocol Name
+ this.client.getClientConfig().getProtocolVersion().protocolLevel(), // Protocol Level
+ this.client.getClientConfig().getUsername() != null, // Has Username
+ this.client.getClientConfig().getPassword() != null, // Has Password
+ this.client.getClientConfig().getLastWill() != null // Will Retain
+ && this.client.getClientConfig().getLastWill().isRetain(),
+ this.client.getClientConfig().getLastWill() != null // Will QOS
+ ? this.client.getClientConfig().getLastWill().getQos().value()
+ : 0,
+ this.client.getClientConfig().getLastWill() != null, // Has Will
+ this.client.getClientConfig().isCleanSession(), // Clean Session
+ this.client.getClientConfig().getTimeoutSeconds() // Timeout
+ );
+ MqttConnectPayload payload = new MqttConnectPayload(
+ this.client.getClientConfig().getClientId(),
+ this.client.getClientConfig().getLastWill() != null ? this.client.getClientConfig().getLastWill().getTopic() : null,
+ this.client.getClientConfig().getLastWill() != null ? this.client.getClientConfig().getLastWill().getMessage().getBytes(CharsetUtil.UTF_8) : null,
+ this.client.getClientConfig().getUsername(),
+ this.client.getClientConfig().getPassword() != null ? this.client.getClientConfig().getPassword().getBytes(CharsetUtil.UTF_8) : null
+ );
+ ctx.channel().writeAndFlush(new MqttConnectMessage(fixedHeader, variableHeader, payload));
+ }
+
+ @Override
+ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+ super.channelInactive(ctx);
+ }
+
+ private void invokeHandlersForIncomingPublish(MqttPublishMessage message) {
+ for (MqttSubscribtion subscribtion : ImmutableSet.copyOf(this.client.getSubscriptions().values())) {
+ if (subscribtion.matches(message.variableHeader().topicName())) {
+ if (subscribtion.isOnce() && subscribtion.isCalled()) {
+ continue;
+ }
+ message.payload().markReaderIndex();
+ subscribtion.setCalled(true);
+ subscribtion.getHandler().onMessage(message.variableHeader().topicName(), message.payload());
+ if (subscribtion.isOnce()) {
+ this.client.off(subscribtion.getTopic(), subscribtion.getHandler());
+ }
+ message.payload().resetReaderIndex();
+ }
+ }
+ /*Set<MqttSubscribtion> subscribtions = ImmutableSet.copyOf(this.client.getSubscriptions().get(message.variableHeader().topicName()));
+ for (MqttSubscribtion subscribtion : subscribtions) {
+ if(subscribtion.isOnce() && subscribtion.isCalled()){
+ continue;
+ }
+ message.payload().markReaderIndex();
+ subscribtion.setCalled(true);
+ subscribtion.getHandler().onMessage(message.variableHeader().topicName(), message.payload());
+ if(subscribtion.isOnce()){
+ this.client.off(subscribtion.getTopic(), subscribtion.getHandler());
+ }
+ message.payload().resetReaderIndex();
+ }*/
+ message.payload().release();
+ }
+
+ private void handleConack(Channel channel, MqttConnAckMessage message) {
+ switch (message.variableHeader().connectReturnCode()) {
+ case CONNECTION_ACCEPTED:
+ this.connectFuture.setSuccess(new MqttConnectResult(true, MqttConnectReturnCode.CONNECTION_ACCEPTED, channel.closeFuture()));
+
+ this.client.getPendingSubscribtions().entrySet().stream().filter((e) -> !e.getValue().isSent()).forEach((e) -> {
+ channel.write(e.getValue().getSubscribeMessage());
+ e.getValue().setSent(true);
+ });
+
+ this.client.getPendingPublishes().forEach((id, publish) -> {
+ if (publish.isSent()) return;
+ channel.write(publish.getMessage());
+ publish.setSent(true);
+ if (publish.getQos() == MqttQoS.AT_MOST_ONCE) {
+ publish.getFuture().setSuccess(null); //We don't get an ACK for QOS 0
+ this.client.getPendingPublishes().remove(publish.getMessageId());
+ }
+ });
+ channel.flush();
+ break;
+
+ case CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD:
+ case CONNECTION_REFUSED_IDENTIFIER_REJECTED:
+ case CONNECTION_REFUSED_NOT_AUTHORIZED:
+ case CONNECTION_REFUSED_SERVER_UNAVAILABLE:
+ case CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION:
+ this.connectFuture.setSuccess(new MqttConnectResult(false, message.variableHeader().connectReturnCode(), channel.closeFuture()));
+ channel.close();
+ // Don't start reconnect logic here
+ break;
+ }
+ }
+
+ private void handleSubAck(MqttSubAckMessage message) {
+ MqttPendingSubscribtion pendingSubscription = this.client.getPendingSubscribtions().remove(message.variableHeader().messageId());
+ if (pendingSubscription == null) {
+ return;
+ }
+ pendingSubscription.onSubackReceived();
+ for (MqttPendingSubscribtion.MqttPendingHandler handler : pendingSubscription.getHandlers()) {
+ MqttSubscribtion subscribtion = new MqttSubscribtion(pendingSubscription.getTopic(), handler.getHandler(), handler.isOnce());
+ this.client.getSubscriptions().put(pendingSubscription.getTopic(), subscribtion);
+ this.client.getHandlerToSubscribtion().put(handler.getHandler(), subscribtion);
+ }
+ this.client.getPendingSubscribeTopics().remove(pendingSubscription.getTopic());
+
+ this.client.getServerSubscribtions().add(pendingSubscription.getTopic());
+
+ if (!pendingSubscription.getFuture().isDone()) {
+ pendingSubscription.getFuture().setSuccess(null);
+ }
+ }
+
+ private void handlePublish(Channel channel, MqttPublishMessage message) {
+ switch (message.fixedHeader().qosLevel()) {
+ case AT_MOST_ONCE:
+ invokeHandlersForIncomingPublish(message);
+ break;
+
+ case AT_LEAST_ONCE:
+ invokeHandlersForIncomingPublish(message);
+ if (message.variableHeader().messageId() != -1) {
+ MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
+ MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().messageId());
+ channel.writeAndFlush(new MqttPubAckMessage(fixedHeader, variableHeader));
+ }
+ break;
+
+ case EXACTLY_ONCE:
+ if (message.variableHeader().messageId() != -1) {
+ MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBREC, false, MqttQoS.AT_MOST_ONCE, false, 0);
+ MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().messageId());
+ MqttMessage pubrecMessage = new MqttMessage(fixedHeader, variableHeader);
+
+ MqttIncomingQos2Publish incomingQos2Publish = new MqttIncomingQos2Publish(message, pubrecMessage);
+ this.client.getQos2PendingIncomingPublishes().put(message.variableHeader().messageId(), incomingQos2Publish);
+ message.payload().retain();
+ incomingQos2Publish.startPubrecRetransmitTimer(this.client.getEventLoop().next(), this.client::sendAndFlushPacket);
+
+ channel.writeAndFlush(pubrecMessage);
+ }
+ break;
+ }
+ }
+
+ private void handleUnsuback(MqttUnsubAckMessage message) {
+ MqttPendingUnsubscribtion unsubscribtion = this.client.getPendingServerUnsubscribes().get(message.variableHeader().messageId());
+ if (unsubscribtion == null) {
+ return;
+ }
+ unsubscribtion.onUnsubackReceived();
+ this.client.getServerSubscribtions().remove(unsubscribtion.getTopic());
+ unsubscribtion.getFuture().setSuccess(null);
+ this.client.getPendingServerUnsubscribes().remove(message.variableHeader().messageId());
+ }
+
+ private void handlePuback(MqttPubAckMessage message) {
+ MqttPendingPublish pendingPublish = this.client.getPendingPublishes().get(message.variableHeader().messageId());
+ pendingPublish.getFuture().setSuccess(null);
+ pendingPublish.onPubackReceived();
+ this.client.getPendingPublishes().remove(message.variableHeader().messageId());
+ pendingPublish.getPayload().release();
+ }
+
+ private void handlePubrec(Channel channel, MqttMessage message) {
+ MqttPendingPublish pendingPublish = this.client.getPendingPublishes().get(((MqttMessageIdVariableHeader) message.variableHeader()).messageId());
+ pendingPublish.onPubackReceived();
+
+ MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBREL, false, MqttQoS.AT_LEAST_ONCE, false, 0);
+ MqttMessageIdVariableHeader variableHeader = (MqttMessageIdVariableHeader) message.variableHeader();
+ MqttMessage pubrelMessage = new MqttMessage(fixedHeader, variableHeader);
+ channel.writeAndFlush(pubrelMessage);
+
+ pendingPublish.setPubrelMessage(pubrelMessage);
+ pendingPublish.startPubrelRetransmissionTimer(this.client.getEventLoop().next(), this.client::sendAndFlushPacket);
+ }
+
+ private void handlePubrel(Channel channel, MqttMessage message) {
+ if (this.client.getQos2PendingIncomingPublishes().containsKey(((MqttMessageIdVariableHeader) message.variableHeader()).messageId())) {
+ MqttIncomingQos2Publish incomingQos2Publish = this.client.getQos2PendingIncomingPublishes().get(((MqttMessageIdVariableHeader) message.variableHeader()).messageId());
+ this.invokeHandlersForIncomingPublish(incomingQos2Publish.getIncomingPublish());
+ incomingQos2Publish.onPubrelReceived();
+ this.client.getQos2PendingIncomingPublishes().remove(incomingQos2Publish.getIncomingPublish().variableHeader().messageId());
+ }
+ MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0);
+ MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(((MqttMessageIdVariableHeader) message.variableHeader()).messageId());
+ channel.writeAndFlush(new MqttMessage(fixedHeader, variableHeader));
+ }
+
+ private void handlePubcomp(MqttMessage message) {
+ MqttMessageIdVariableHeader variableHeader = (MqttMessageIdVariableHeader) message.variableHeader();
+ MqttPendingPublish pendingPublish = this.client.getPendingPublishes().get(variableHeader.messageId());
+ pendingPublish.getFuture().setSuccess(null);
+ this.client.getPendingPublishes().remove(variableHeader.messageId());
+ pendingPublish.getPayload().release();
+ pendingPublish.onPubcompReceived();
+ }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java
new file mode 100644
index 0000000..6563525
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java
@@ -0,0 +1,205 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import io.netty.util.concurrent.Future;
+
+public interface MqttClient {
+
+ /**
+ * Connect to the specified hostname/ip. By default uses port 1883.
+ * If you want to change the port number, see {@link #connect(String, int)}
+ *
+ * @param host The ip address or host to connect to
+ * @return A future which will be completed when the connection is opened and we received an CONNACK
+ */
+ Future<MqttConnectResult> connect(String host);
+
+ /**
+ * Connect to the specified hostname/ip using the specified port
+ *
+ * @param host The ip address or host to connect to
+ * @param port The tcp port to connect to
+ * @return A future which will be completed when the connection is opened and we received an CONNACK
+ */
+ Future<MqttConnectResult> connect(String host, int port);
+
+ /**
+ *
+ * @return boolean value indicating if channel is active
+ */
+ boolean isConnected();
+
+ /**
+ * Attempt reconnect to the host that was attempted with {@link #connect(String, int)} method before
+ *
+ * @return A future which will be completed when the connection is opened and we received an CONNACK
+ * @throws IllegalStateException if no previous {@link #connect(String, int)} calls were attempted
+ */
+ Future<MqttConnectResult> reconnect();
+
+ /**
+ * Retrieve the netty {@link EventLoopGroup} we are using
+ * @return The netty {@link EventLoopGroup} we use for the connection
+ */
+ EventLoopGroup getEventLoop();
+
+ /**
+ * By default we use the netty {@link NioEventLoopGroup}.
+ * If you change the EventLoopGroup to another type, make sure to change the {@link Channel} class using {@link MqttClientConfig#setChannelClass(Class)}
+ * If you want to force the MqttClient to use another {@link EventLoopGroup}, call this function before calling {@link #connect(String, int)}
+ *
+ * @param eventLoop The new eventloop to use
+ */
+ void setEventLoop(EventLoopGroup eventLoop);
+
+ /**
+ * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler
+ *
+ * @param topic The topic filter to subscribe to
+ * @param handler The handler to invoke when we receive a message
+ * @return A future which will be completed when the server acknowledges our subscribe request
+ */
+ Future<Void> on(String topic, MqttHandler handler);
+
+ /**
+ * Subscribe on the given topic, with the given qos. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler
+ *
+ * @param topic The topic filter to subscribe to
+ * @param handler The handler to invoke when we receive a message
+ * @param qos The qos to request to the server
+ * @return A future which will be completed when the server acknowledges our subscribe request
+ */
+ Future<Void> on(String topic, MqttHandler handler, MqttQoS qos);
+
+ /**
+ * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler
+ * This subscribtion is only once. If the MqttClient has received 1 message, the subscribtion will be removed
+ *
+ * @param topic The topic filter to subscribe to
+ * @param handler The handler to invoke when we receive a message
+ * @return A future which will be completed when the server acknowledges our subscribe request
+ */
+ Future<Void> once(String topic, MqttHandler handler);
+
+ /**
+ * Subscribe on the given topic, with the given qos. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler
+ * This subscribtion is only once. If the MqttClient has received 1 message, the subscribtion will be removed
+ *
+ * @param topic The topic filter to subscribe to
+ * @param handler The handler to invoke when we receive a message
+ * @param qos The qos to request to the server
+ * @return A future which will be completed when the server acknowledges our subscribe request
+ */
+ Future<Void> once(String topic, MqttHandler handler, MqttQoS qos);
+
+ /**
+ * Remove the subscribtion for the given topic and handler
+ * If you want to unsubscribe from all handlers known for this topic, use {@link #off(String)}
+ *
+ * @param topic The topic to unsubscribe for
+ * @param handler The handler to unsubscribe
+ * @return A future which will be completed when the server acknowledges our unsubscribe request
+ */
+ Future<Void> off(String topic, MqttHandler handler);
+
+ /**
+ * Remove all subscribtions for the given topic.
+ * If you want to specify which handler to unsubscribe, use {@link #off(String, MqttHandler)}
+ *
+ * @param topic The topic to unsubscribe for
+ * @return A future which will be completed when the server acknowledges our unsubscribe request
+ */
+ Future<Void> off(String topic);
+
+ /**
+ * Publish a message to the given payload
+ * @param topic The topic to publish to
+ * @param payload The payload to send
+ * @return A future which will be completed when the message is sent out of the MqttClient
+ */
+ Future<Void> publish(String topic, ByteBuf payload);
+
+ /**
+ * Publish a message to the given payload, using the given qos
+ * @param topic The topic to publish to
+ * @param payload The payload to send
+ * @param qos The qos to use while publishing
+ * @return A future which will be completed when the message is delivered to the server
+ */
+ Future<Void> publish(String topic, ByteBuf payload, MqttQoS qos);
+
+ /**
+ * Publish a message to the given payload, using optional retain
+ * @param topic The topic to publish to
+ * @param payload The payload to send
+ * @param retain true if you want to retain the message on the server, false otherwise
+ * @return A future which will be completed when the message is sent out of the MqttClient
+ */
+ Future<Void> publish(String topic, ByteBuf payload, boolean retain);
+
+ /**
+ * Publish a message to the given payload, using the given qos and optional retain
+ * @param topic The topic to publish to
+ * @param payload The payload to send
+ * @param qos The qos to use while publishing
+ * @param retain true if you want to retain the message on the server, false otherwise
+ * @return A future which will be completed when the message is delivered to the server
+ */
+ Future<Void> publish(String topic, ByteBuf payload, MqttQoS qos, boolean retain);
+
+ /**
+ * Retrieve the MqttClient configuration
+ * @return The {@link MqttClientConfig} instance we use
+ */
+ MqttClientConfig getClientConfig();
+
+ /**
+ * Construct the MqttClientImpl with default config
+ */
+ static MqttClient create(){
+ return new MqttClientImpl();
+ }
+
+ /**
+ * Construct the MqttClientImpl with additional config.
+ * This config can also be changed using the {@link #getClientConfig()} function
+ *
+ * @param config The config object to use while looking for settings
+ */
+ static MqttClient create(MqttClientConfig config){
+ return new MqttClientImpl(config);
+ }
+
+
+ /**
+ * Send disconnect and close channel
+ *
+ */
+ void disconnect();
+
+ /**
+ * Sets the {@see #MqttClientCallback} object for this MqttClient
+ * @param callback The callback to be set
+ */
+ void setCallback(MqttClientCallback callback);
+
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java
new file mode 100644
index 0000000..a59d83b
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java
@@ -0,0 +1,149 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.channel.Channel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.mqtt.MqttVersion;
+import io.netty.handler.ssl.SslContext;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.Random;
+
+@SuppressWarnings({"WeakerAccess", "unused"})
+public final class MqttClientConfig {
+
+ private final SslContext sslContext;
+ private final String randomClientId;
+
+ private String clientId;
+ private int timeoutSeconds = 60;
+ private MqttVersion protocolVersion = MqttVersion.MQTT_3_1;
+ @Nullable private String username = null;
+ @Nullable private String password = null;
+ private boolean cleanSession = true;
+ @Nullable private MqttLastWill lastWill;
+ private Class<? extends Channel> channelClass = NioSocketChannel.class;
+
+ private boolean reconnect = true;
+
+ public MqttClientConfig() {
+ this(null);
+ }
+
+ public MqttClientConfig(SslContext sslContext) {
+ this.sslContext = sslContext;
+ Random random = new Random();
+ String id = "netty-mqtt/";
+ String[] options = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split("");
+ for(int i = 0; i < 8; i++){
+ id += options[random.nextInt(options.length)];
+ }
+ this.clientId = id;
+ this.randomClientId = id;
+ }
+
+ @Nonnull
+ public String getClientId() {
+ return clientId;
+ }
+
+ public void setClientId(@Nullable String clientId) {
+ if(clientId == null){
+ this.clientId = randomClientId;
+ }else{
+ this.clientId = clientId;
+ }
+ }
+
+ public int getTimeoutSeconds() {
+ return timeoutSeconds;
+ }
+
+ public void setTimeoutSeconds(int timeoutSeconds) {
+ if(timeoutSeconds != -1 && timeoutSeconds <= 0){
+ throw new IllegalArgumentException("timeoutSeconds must be > 0 or -1");
+ }
+ this.timeoutSeconds = timeoutSeconds;
+ }
+
+ public MqttVersion getProtocolVersion() {
+ return protocolVersion;
+ }
+
+ public void setProtocolVersion(MqttVersion protocolVersion) {
+ if(protocolVersion == null){
+ throw new NullPointerException("protocolVersion");
+ }
+ this.protocolVersion = protocolVersion;
+ }
+
+ @Nullable
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(@Nullable String username) {
+ this.username = username;
+ }
+
+ @Nullable
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(@Nullable String password) {
+ this.password = password;
+ }
+
+ public boolean isCleanSession() {
+ return cleanSession;
+ }
+
+ public void setCleanSession(boolean cleanSession) {
+ this.cleanSession = cleanSession;
+ }
+
+ @Nullable
+ public MqttLastWill getLastWill() {
+ return lastWill;
+ }
+
+ public void setLastWill(@Nullable MqttLastWill lastWill) {
+ this.lastWill = lastWill;
+ }
+
+ public Class<? extends Channel> getChannelClass() {
+ return channelClass;
+ }
+
+ public void setChannelClass(Class<? extends Channel> channelClass) {
+ this.channelClass = channelClass;
+ }
+
+ public SslContext getSslContext() {
+ return sslContext;
+ }
+
+ public boolean isReconnect() {
+ return reconnect;
+ }
+
+ public void setReconnect(boolean reconnect) {
+ this.reconnect = reconnect;
+ }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java
new file mode 100644
index 0000000..3914105
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java
@@ -0,0 +1,484 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableSet;
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.*;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.handler.codec.mqtt.*;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.timeout.IdleStateHandler;
+import io.netty.util.collection.IntObjectHashMap;
+import io.netty.util.concurrent.DefaultPromise;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.Promise;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Represents an MqttClientImpl connected to a single MQTT server. Will try to keep the connection going at all times
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+final class MqttClientImpl implements MqttClient {
+
+ private final Set<String> serverSubscribtions = new HashSet<>();
+ private final IntObjectHashMap<MqttPendingUnsubscribtion> pendingServerUnsubscribes = new IntObjectHashMap<>();
+ private final IntObjectHashMap<MqttIncomingQos2Publish> qos2PendingIncomingPublishes = new IntObjectHashMap<>();
+ private final IntObjectHashMap<MqttPendingPublish> pendingPublishes = new IntObjectHashMap<>();
+ private final HashMultimap<String, MqttSubscribtion> subscriptions = HashMultimap.create();
+ private final IntObjectHashMap<MqttPendingSubscribtion> pendingSubscribtions = new IntObjectHashMap<>();
+ private final Set<String> pendingSubscribeTopics = new HashSet<>();
+ private final HashMultimap<MqttHandler, MqttSubscribtion> handlerToSubscribtion = HashMultimap.create();
+ private final AtomicInteger nextMessageId = new AtomicInteger(1);
+
+ private final MqttClientConfig clientConfig;
+
+ private EventLoopGroup eventLoop;
+
+ private Channel channel;
+
+ private boolean disconnected = false;
+ private String host;
+ private int port;
+ private MqttClientCallback callback;
+
+
+ /**
+ * Construct the MqttClientImpl with default config
+ */
+ public MqttClientImpl() {
+ this.clientConfig = new MqttClientConfig();
+ }
+
+ /**
+ * Construct the MqttClientImpl with additional config.
+ * This config can also be changed using the {@link #getClientConfig()} function
+ *
+ * @param clientConfig The config object to use while looking for settings
+ */
+ public MqttClientImpl(MqttClientConfig clientConfig) {
+ this.clientConfig = clientConfig;
+ }
+
+ /**
+ * Connect to the specified hostname/ip. By default uses port 1883.
+ * If you want to change the port number, see {@link #connect(String, int)}
+ *
+ * @param host The ip address or host to connect to
+ * @return A future which will be completed when the connection is opened and we received an CONNACK
+ */
+ @Override
+ public Future<MqttConnectResult> connect(String host) {
+ return connect(host, 1883);
+ }
+
+ /**
+ * Connect to the specified hostname/ip using the specified port
+ *
+ * @param host The ip address or host to connect to
+ * @param port The tcp port to connect to
+ * @return A future which will be completed when the connection is opened and we received an CONNACK
+ */
+ @Override
+ public Future<MqttConnectResult> connect(String host, int port) {
+ if (this.eventLoop == null) {
+ this.eventLoop = new NioEventLoopGroup();
+ }
+ this.host = host;
+ this.port = port;
+
+ Promise<MqttConnectResult> connectFuture = new DefaultPromise<>(this.eventLoop.next());
+ Bootstrap bootstrap = new Bootstrap();
+ bootstrap.group(this.eventLoop);
+ bootstrap.channel(clientConfig.getChannelClass());
+ bootstrap.remoteAddress(host, port);
+ bootstrap.handler(new MqttChannelInitializer(connectFuture, host, port, clientConfig.getSslContext()));
+ ChannelFuture future = bootstrap.connect();
+ future.addListener((ChannelFutureListener) f -> {
+ if (f.isSuccess()) {
+ MqttClientImpl.this.channel = f.channel();
+ } else if (clientConfig.isReconnect() && !disconnected) {
+ eventLoop.schedule((Runnable) () -> connect(host, port), 1L, TimeUnit.SECONDS);
+ }
+ });
+ return connectFuture;
+ }
+
+ @Override
+ public boolean isConnected() {
+ if (!disconnected) {
+ return channel == null ? false : channel.isActive();
+ };
+ return false;
+ }
+
+ @Override
+ public Future<MqttConnectResult> reconnect() {
+ if (host == null) {
+ throw new IllegalStateException("Cannot reconnect. Call connect() first");
+ }
+ return connect(host, port);
+ }
+
+ /**
+ * Retrieve the netty {@link EventLoopGroup} we are using
+ *
+ * @return The netty {@link EventLoopGroup} we use for the connection
+ */
+ @Override
+ public EventLoopGroup getEventLoop() {
+ return eventLoop;
+ }
+
+ /**
+ * By default we use the netty {@link NioEventLoopGroup}.
+ * If you change the EventLoopGroup to another type, make sure to change the {@link Channel} class using {@link MqttClientConfig#setChannelClass(Class)}
+ * If you want to force the MqttClient to use another {@link EventLoopGroup}, call this function before calling {@link #connect(String, int)}
+ *
+ * @param eventLoop The new eventloop to use
+ */
+ @Override
+ public void setEventLoop(EventLoopGroup eventLoop) {
+ this.eventLoop = eventLoop;
+ }
+
+ /**
+ * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler
+ *
+ * @param topic The topic filter to subscribe to
+ * @param handler The handler to invoke when we receive a message
+ * @return A future which will be completed when the server acknowledges our subscribe request
+ */
+ @Override
+ public Future<Void> on(String topic, MqttHandler handler) {
+ return on(topic, handler, MqttQoS.AT_MOST_ONCE);
+ }
+
+ /**
+ * Subscribe on the given topic, with the given qos. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler
+ *
+ * @param topic The topic filter to subscribe to
+ * @param handler The handler to invoke when we receive a message
+ * @param qos The qos to request to the server
+ * @return A future which will be completed when the server acknowledges our subscribe request
+ */
+ @Override
+ public Future<Void> on(String topic, MqttHandler handler, MqttQoS qos) {
+ return createSubscribtion(topic, handler, false, qos);
+ }
+
+ /**
+ * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler
+ * This subscribtion is only once. If the MqttClient has received 1 message, the subscribtion will be removed
+ *
+ * @param topic The topic filter to subscribe to
+ * @param handler The handler to invoke when we receive a message
+ * @return A future which will be completed when the server acknowledges our subscribe request
+ */
+ @Override
+ public Future<Void> once(String topic, MqttHandler handler) {
+ return once(topic, handler, MqttQoS.AT_MOST_ONCE);
+ }
+
+ /**
+ * Subscribe on the given topic, with the given qos. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler
+ * This subscribtion is only once. If the MqttClient has received 1 message, the subscribtion will be removed
+ *
+ * @param topic The topic filter to subscribe to
+ * @param handler The handler to invoke when we receive a message
+ * @param qos The qos to request to the server
+ * @return A future which will be completed when the server acknowledges our subscribe request
+ */
+ @Override
+ public Future<Void> once(String topic, MqttHandler handler, MqttQoS qos) {
+ return createSubscribtion(topic, handler, true, qos);
+ }
+
+ /**
+ * Remove the subscribtion for the given topic and handler
+ * If you want to unsubscribe from all handlers known for this topic, use {@link #off(String)}
+ *
+ * @param topic The topic to unsubscribe for
+ * @param handler The handler to unsubscribe
+ * @return A future which will be completed when the server acknowledges our unsubscribe request
+ */
+ @Override
+ public Future<Void> off(String topic, MqttHandler handler) {
+ Promise<Void> future = new DefaultPromise<>(this.eventLoop.next());
+ for (MqttSubscribtion subscribtion : this.handlerToSubscribtion.get(handler)) {
+ this.subscriptions.remove(topic, subscribtion);
+ }
+ this.handlerToSubscribtion.removeAll(handler);
+ this.checkSubscribtions(topic, future);
+ return future;
+ }
+
+ /**
+ * Remove all subscribtions for the given topic.
+ * If you want to specify which handler to unsubscribe, use {@link #off(String, MqttHandler)}
+ *
+ * @param topic The topic to unsubscribe for
+ * @return A future which will be completed when the server acknowledges our unsubscribe request
+ */
+ @Override
+ public Future<Void> off(String topic) {
+ Promise<Void> future = new DefaultPromise<>(this.eventLoop.next());
+ ImmutableSet<MqttSubscribtion> subscribtions = ImmutableSet.copyOf(this.subscriptions.get(topic));
+ for (MqttSubscribtion subscribtion : subscribtions) {
+ for (MqttSubscribtion handSub : this.handlerToSubscribtion.get(subscribtion.getHandler())) {
+ this.subscriptions.remove(topic, handSub);
+ }
+ this.handlerToSubscribtion.remove(subscribtion.getHandler(), subscribtion);
+ }
+ this.checkSubscribtions(topic, future);
+ return future;
+ }
+
+ /**
+ * Publish a message to the given payload
+ *
+ * @param topic The topic to publish to
+ * @param payload The payload to send
+ * @return A future which will be completed when the message is sent out of the MqttClient
+ */
+ @Override
+ public Future<Void> publish(String topic, ByteBuf payload) {
+ return publish(topic, payload, MqttQoS.AT_MOST_ONCE, false);
+ }
+
+ /**
+ * Publish a message to the given payload, using the given qos
+ *
+ * @param topic The topic to publish to
+ * @param payload The payload to send
+ * @param qos The qos to use while publishing
+ * @return A future which will be completed when the message is delivered to the server
+ */
+ @Override
+ public Future<Void> publish(String topic, ByteBuf payload, MqttQoS qos) {
+ return publish(topic, payload, qos, false);
+ }
+
+ /**
+ * Publish a message to the given payload, using optional retain
+ *
+ * @param topic The topic to publish to
+ * @param payload The payload to send
+ * @param retain true if you want to retain the message on the server, false otherwise
+ * @return A future which will be completed when the message is sent out of the MqttClient
+ */
+ @Override
+ public Future<Void> publish(String topic, ByteBuf payload, boolean retain) {
+ return publish(topic, payload, MqttQoS.AT_MOST_ONCE, retain);
+ }
+
+ /**
+ * Publish a message to the given payload, using the given qos and optional retain
+ *
+ * @param topic The topic to publish to
+ * @param payload The payload to send
+ * @param qos The qos to use while publishing
+ * @param retain true if you want to retain the message on the server, false otherwise
+ * @return A future which will be completed when the message is delivered to the server
+ */
+ @Override
+ public Future<Void> publish(String topic, ByteBuf payload, MqttQoS qos, boolean retain) {
+ Promise<Void> future = new DefaultPromise<>(this.eventLoop.next());
+ MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, false, qos, retain, 0);
+ MqttPublishVariableHeader variableHeader = new MqttPublishVariableHeader(topic, getNewMessageId().messageId());
+ MqttPublishMessage message = new MqttPublishMessage(fixedHeader, variableHeader, payload);
+ MqttPendingPublish pendingPublish = new MqttPendingPublish(variableHeader.messageId(), future, payload.retain(), message, qos);
+ ChannelFuture channelFuture = this.sendAndFlushPacket(message);
+
+ if (channelFuture != null) {
+ pendingPublish.setSent(channelFuture != null);
+ if (channelFuture.cause() != null) {
+ future.setFailure(channelFuture.cause());
+ return future;
+ }
+ }
+ if (pendingPublish.isSent() && pendingPublish.getQos() == MqttQoS.AT_MOST_ONCE) {
+ pendingPublish.getFuture().setSuccess(null); //We don't get an ACK for QOS 0
+ } else if (pendingPublish.isSent()) {
+ this.pendingPublishes.put(pendingPublish.getMessageId(), pendingPublish);
+ pendingPublish.startPublishRetransmissionTimer(this.eventLoop.next(), this::sendAndFlushPacket);
+ }
+ return future;
+ }
+
+ /**
+ * Retrieve the MqttClient configuration
+ *
+ * @return The {@link MqttClientConfig} instance we use
+ */
+ @Override
+ public MqttClientConfig getClientConfig() {
+ return clientConfig;
+ }
+
+ @Override
+ public void disconnect() {
+ disconnected = true;
+ if (this.channel != null) {
+ MqttMessage message = new MqttMessage(new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0));
+ this.sendAndFlushPacket(message).addListener(future1 -> channel.close());
+ }
+ }
+
+ @Override
+ public void setCallback(MqttClientCallback callback) {
+ this.callback = callback;
+ }
+
+
+ ///////////////////////////////////////////// PRIVATE API /////////////////////////////////////////////
+
+ ChannelFuture sendAndFlushPacket(Object message) {
+ if (this.channel == null) {
+ return null;
+ }
+ if (this.channel.isActive()) {
+ return this.channel.writeAndFlush(message);
+ }
+ ChannelClosedException e = new ChannelClosedException("Channel is closed");
+ if (callback != null) {
+ callback.connectionLost(e);
+ }
+ return this.channel.newFailedFuture(e);
+ }
+
+ private MqttMessageIdVariableHeader getNewMessageId() {
+ this.nextMessageId.compareAndSet(0xffff, 1);
+ return MqttMessageIdVariableHeader.from(this.nextMessageId.getAndIncrement());
+ }
+
+ private Future<Void> createSubscribtion(String topic, MqttHandler handler, boolean once, MqttQoS qos) {
+ if (this.pendingSubscribeTopics.contains(topic)) {
+ Optional<Map.Entry<Integer, MqttPendingSubscribtion>> subscribtionEntry = this.pendingSubscribtions.entrySet().stream().filter((e) -> e.getValue().getTopic().equals(topic)).findAny();
+ if (subscribtionEntry.isPresent()) {
+ subscribtionEntry.get().getValue().addHandler(handler, once);
+ return subscribtionEntry.get().getValue().getFuture();
+ }
+ }
+ if (this.serverSubscribtions.contains(topic)) {
+ MqttSubscribtion subscribtion = new MqttSubscribtion(topic, handler, once);
+ this.subscriptions.put(topic, subscribtion);
+ this.handlerToSubscribtion.put(handler, subscribtion);
+ return this.channel.newSucceededFuture();
+ }
+
+ Promise<Void> future = new DefaultPromise<>(this.eventLoop.next());
+ MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.SUBSCRIBE, false, MqttQoS.AT_LEAST_ONCE, false, 0);
+ MqttTopicSubscription subscription = new MqttTopicSubscription(topic, qos);
+ MqttMessageIdVariableHeader variableHeader = getNewMessageId();
+ MqttSubscribePayload payload = new MqttSubscribePayload(Collections.singletonList(subscription));
+ MqttSubscribeMessage message = new MqttSubscribeMessage(fixedHeader, variableHeader, payload);
+
+ final MqttPendingSubscribtion pendingSubscribtion = new MqttPendingSubscribtion(future, topic, message);
+ pendingSubscribtion.addHandler(handler, once);
+ this.pendingSubscribtions.put(variableHeader.messageId(), pendingSubscribtion);
+ this.pendingSubscribeTopics.add(topic);
+ pendingSubscribtion.setSent(this.sendAndFlushPacket(message) != null); //If not sent, we will send it when the connection is opened
+
+ pendingSubscribtion.startRetransmitTimer(this.eventLoop.next(), this::sendAndFlushPacket);
+
+ return future;
+ }
+
+ private void checkSubscribtions(String topic, Promise<Void> promise) {
+ if (!(this.subscriptions.containsKey(topic) && this.subscriptions.get(topic).size() != 0) && this.serverSubscribtions.contains(topic)) {
+ MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.UNSUBSCRIBE, false, MqttQoS.AT_LEAST_ONCE, false, 0);
+ MqttMessageIdVariableHeader variableHeader = getNewMessageId();
+ MqttUnsubscribePayload payload = new MqttUnsubscribePayload(Collections.singletonList(topic));
+ MqttUnsubscribeMessage message = new MqttUnsubscribeMessage(fixedHeader, variableHeader, payload);
+
+ MqttPendingUnsubscribtion pendingUnsubscribtion = new MqttPendingUnsubscribtion(promise, topic, message);
+ this.pendingServerUnsubscribes.put(variableHeader.messageId(), pendingUnsubscribtion);
+ pendingUnsubscribtion.startRetransmissionTimer(this.eventLoop.next(), this::sendAndFlushPacket);
+
+ this.sendAndFlushPacket(message);
+ } else {
+ promise.setSuccess(null);
+ }
+ }
+
+ IntObjectHashMap<MqttPendingSubscribtion> getPendingSubscribtions() {
+ return pendingSubscribtions;
+ }
+
+ HashMultimap<String, MqttSubscribtion> getSubscriptions() {
+ return subscriptions;
+ }
+
+ Set<String> getPendingSubscribeTopics() {
+ return pendingSubscribeTopics;
+ }
+
+ HashMultimap<MqttHandler, MqttSubscribtion> getHandlerToSubscribtion() {
+ return handlerToSubscribtion;
+ }
+
+ Set<String> getServerSubscribtions() {
+ return serverSubscribtions;
+ }
+
+ IntObjectHashMap<MqttPendingUnsubscribtion> getPendingServerUnsubscribes() {
+ return pendingServerUnsubscribes;
+ }
+
+ IntObjectHashMap<MqttPendingPublish> getPendingPublishes() {
+ return pendingPublishes;
+ }
+
+ IntObjectHashMap<MqttIncomingQos2Publish> getQos2PendingIncomingPublishes() {
+ return qos2PendingIncomingPublishes;
+ }
+
+ private class MqttChannelInitializer extends ChannelInitializer<SocketChannel> {
+
+ private final Promise<MqttConnectResult> connectFuture;
+ private final String host;
+ private final int port;
+ private final SslContext sslContext;
+
+
+ public MqttChannelInitializer(Promise<MqttConnectResult> connectFuture, String host, int port, SslContext sslContext) {
+ this.connectFuture = connectFuture;
+ this.host = host;
+ this.port = port;
+ this.sslContext = sslContext;
+ }
+
+ @Override
+ protected void initChannel(SocketChannel ch) throws Exception {
+ if (sslContext != null) {
+ ch.pipeline().addLast(sslContext.newHandler(ch.alloc(), host, port));
+ }
+
+ ch.pipeline().addLast("mqttDecoder", new MqttDecoder());
+ ch.pipeline().addLast("mqttEncoder", MqttEncoder.INSTANCE);
+ ch.pipeline().addLast("idleStateHandler", new IdleStateHandler(MqttClientImpl.this.clientConfig.getTimeoutSeconds(), MqttClientImpl.this.clientConfig.getTimeoutSeconds(), 0));
+ ch.pipeline().addLast("mqttPingHandler", new MqttPingHandler(MqttClientImpl.this.clientConfig.getTimeoutSeconds()));
+ ch.pipeline().addLast("mqttHandler", new MqttChannelHandler(MqttClientImpl.this, connectFuture));
+ }
+ }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java
new file mode 100644
index 0000000..5fa0e6d
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.channel.ChannelFuture;
+import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
+
+@SuppressWarnings({"WeakerAccess", "unused"})
+public final class MqttConnectResult {
+
+ private final boolean success;
+ private final MqttConnectReturnCode returnCode;
+ private final ChannelFuture closeFuture;
+
+ MqttConnectResult(boolean success, MqttConnectReturnCode returnCode, ChannelFuture closeFuture) {
+ this.success = success;
+ this.returnCode = returnCode;
+ this.closeFuture = closeFuture;
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public MqttConnectReturnCode getReturnCode() {
+ return returnCode;
+ }
+
+ public ChannelFuture getCloseFuture() {
+ return closeFuture;
+ }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttIncomingQos2Publish.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttIncomingQos2Publish.java
new file mode 100644
index 0000000..af84cde
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttIncomingQos2Publish.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.channel.EventLoop;
+import io.netty.handler.codec.mqtt.*;
+
+import java.util.function.Consumer;
+
+final class MqttIncomingQos2Publish {
+
+ private final MqttPublishMessage incomingPublish;
+
+ private final RetransmissionHandler<MqttMessage> retransmissionHandler = new RetransmissionHandler<>();
+
+ MqttIncomingQos2Publish(MqttPublishMessage incomingPublish, MqttMessage originalMessage) {
+ this.incomingPublish = incomingPublish;
+
+ this.retransmissionHandler.setOriginalMessage(originalMessage);
+ }
+
+ MqttPublishMessage getIncomingPublish() {
+ return incomingPublish;
+ }
+
+ void startPubrecRetransmitTimer(EventLoop eventLoop, Consumer<Object> sendPacket) {
+ this.retransmissionHandler.setHandle((fixedHeader, originalMessage) ->
+ sendPacket.accept(new MqttMessage(fixedHeader, originalMessage.variableHeader())));
+ this.retransmissionHandler.start(eventLoop);
+ }
+
+ void onPubrelReceived() {
+ this.retransmissionHandler.stop();
+ }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttLastWill.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttLastWill.java
new file mode 100644
index 0000000..1dadfcd
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttLastWill.java
@@ -0,0 +1,154 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.handler.codec.mqtt.MqttQoS;
+
+@SuppressWarnings({"WeakerAccess", "unused", "SimplifiableIfStatement", "StringBufferReplaceableByString"})
+public final class MqttLastWill {
+
+ private final String topic;
+ private final String message;
+ private final boolean retain;
+ private final MqttQoS qos;
+
+ public MqttLastWill(String topic, String message, boolean retain, MqttQoS qos) {
+ if(topic == null){
+ throw new NullPointerException("topic");
+ }
+ if(message == null){
+ throw new NullPointerException("message");
+ }
+ if(qos == null){
+ throw new NullPointerException("qos");
+ }
+ this.topic = topic;
+ this.message = message;
+ this.retain = retain;
+ this.qos = qos;
+ }
+
+ public String getTopic() {
+ return topic;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public boolean isRetain() {
+ return retain;
+ }
+
+ public MqttQoS getQos() {
+ return qos;
+ }
+
+ public static MqttLastWill.Builder builder(){
+ return new MqttLastWill.Builder();
+ }
+
+ public static final class Builder {
+
+ private String topic;
+ private String message;
+ private boolean retain;
+ private MqttQoS qos;
+
+ public String getTopic() {
+ return topic;
+ }
+
+ public Builder setTopic(String topic) {
+ if(topic == null){
+ throw new NullPointerException("topic");
+ }
+ this.topic = topic;
+ return this;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public Builder setMessage(String message) {
+ if(message == null){
+ throw new NullPointerException("message");
+ }
+ this.message = message;
+ return this;
+ }
+
+ public boolean isRetain() {
+ return retain;
+ }
+
+ public Builder setRetain(boolean retain) {
+ this.retain = retain;
+ return this;
+ }
+
+ public MqttQoS getQos() {
+ return qos;
+ }
+
+ public Builder setQos(MqttQoS qos) {
+ if(qos == null){
+ throw new NullPointerException("qos");
+ }
+ this.qos = qos;
+ return this;
+ }
+
+ public MqttLastWill build(){
+ return new MqttLastWill(topic, message, retain, qos);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ MqttLastWill that = (MqttLastWill) o;
+
+ if (retain != that.retain) return false;
+ if (!topic.equals(that.topic)) return false;
+ if (!message.equals(that.message)) return false;
+ return qos == that.qos;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = topic.hashCode();
+ result = 31 * result + message.hashCode();
+ result = 31 * result + (retain ? 1 : 0);
+ result = 31 * result + qos.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("MqttLastWill{");
+ sb.append("topic='").append(topic).append('\'');
+ sb.append(", message='").append(message).append('\'');
+ sb.append(", retain=").append(retain);
+ sb.append(", qos=").append(qos.name());
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingPublish.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingPublish.java
new file mode 100644
index 0000000..c656e84
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingPublish.java
@@ -0,0 +1,101 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.EventLoop;
+import io.netty.handler.codec.mqtt.MqttMessage;
+import io.netty.handler.codec.mqtt.MqttPublishMessage;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import io.netty.util.concurrent.Promise;
+
+import java.util.function.Consumer;
+
+final class MqttPendingPublish {
+
+ private final int messageId;
+ private final Promise<Void> future;
+ private final ByteBuf payload;
+ private final MqttPublishMessage message;
+ private final MqttQoS qos;
+
+ private final RetransmissionHandler<MqttPublishMessage> publishRetransmissionHandler = new RetransmissionHandler<>();
+ private final RetransmissionHandler<MqttMessage> pubrelRetransmissionHandler = new RetransmissionHandler<>();
+
+ private boolean sent = false;
+
+ MqttPendingPublish(int messageId, Promise<Void> future, ByteBuf payload, MqttPublishMessage message, MqttQoS qos) {
+ this.messageId = messageId;
+ this.future = future;
+ this.payload = payload;
+ this.message = message;
+ this.qos = qos;
+
+ this.publishRetransmissionHandler.setOriginalMessage(message);
+ }
+
+ int getMessageId() {
+ return messageId;
+ }
+
+ Promise<Void> getFuture() {
+ return future;
+ }
+
+ ByteBuf getPayload() {
+ return payload;
+ }
+
+ boolean isSent() {
+ return sent;
+ }
+
+ void setSent(boolean sent) {
+ this.sent = sent;
+ }
+
+ MqttPublishMessage getMessage() {
+ return message;
+ }
+
+ MqttQoS getQos() {
+ return qos;
+ }
+
+ void startPublishRetransmissionTimer(EventLoop eventLoop, Consumer<Object> sendPacket) {
+ this.publishRetransmissionHandler.setHandle(((fixedHeader, originalMessage) ->
+ sendPacket.accept(new MqttPublishMessage(fixedHeader, originalMessage.variableHeader(), this.payload.retain()))));
+ this.publishRetransmissionHandler.start(eventLoop);
+ }
+
+ void onPubackReceived() {
+ this.publishRetransmissionHandler.stop();
+ }
+
+ void setPubrelMessage(MqttMessage pubrelMessage) {
+ this.pubrelRetransmissionHandler.setOriginalMessage(pubrelMessage);
+ }
+
+ void startPubrelRetransmissionTimer(EventLoop eventLoop, Consumer<Object> sendPacket) {
+ this.pubrelRetransmissionHandler.setHandle((fixedHeader, originalMessage) ->
+ sendPacket.accept(new MqttMessage(fixedHeader, originalMessage.variableHeader())));
+ this.pubrelRetransmissionHandler.start(eventLoop);
+ }
+
+ void onPubcompReceived() {
+ this.pubrelRetransmissionHandler.stop();
+ }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscribtion.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscribtion.java
new file mode 100644
index 0000000..782aef1
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscribtion.java
@@ -0,0 +1,102 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.channel.EventLoop;
+import io.netty.handler.codec.mqtt.MqttSubscribeMessage;
+import io.netty.util.concurrent.Promise;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+
+final class MqttPendingSubscribtion {
+
+ private final Promise<Void> future;
+ private final String topic;
+ private final Set<MqttPendingHandler> handlers = new HashSet<>();
+ private final MqttSubscribeMessage subscribeMessage;
+
+ private final RetransmissionHandler<MqttSubscribeMessage> retransmissionHandler = new RetransmissionHandler<>();
+
+ private boolean sent = false;
+
+ MqttPendingSubscribtion(Promise<Void> future, String topic, MqttSubscribeMessage message) {
+ this.future = future;
+ this.topic = topic;
+ this.subscribeMessage = message;
+
+ this.retransmissionHandler.setOriginalMessage(message);
+ }
+
+ Promise<Void> getFuture() {
+ return future;
+ }
+
+ String getTopic() {
+ return topic;
+ }
+
+ boolean isSent() {
+ return sent;
+ }
+
+ void setSent(boolean sent) {
+ this.sent = sent;
+ }
+
+ MqttSubscribeMessage getSubscribeMessage() {
+ return subscribeMessage;
+ }
+
+ void addHandler(MqttHandler handler, boolean once){
+ this.handlers.add(new MqttPendingHandler(handler, once));
+ }
+
+ Set<MqttPendingHandler> getHandlers() {
+ return handlers;
+ }
+
+ void startRetransmitTimer(EventLoop eventLoop, Consumer<Object> sendPacket) {
+ if(this.sent){ //If the packet is sent, we can start the retransmit timer
+ this.retransmissionHandler.setHandle((fixedHeader, originalMessage) ->
+ sendPacket.accept(new MqttSubscribeMessage(fixedHeader, originalMessage.variableHeader(), originalMessage.payload())));
+ this.retransmissionHandler.start(eventLoop);
+ }
+ }
+
+ void onSubackReceived(){
+ this.retransmissionHandler.stop();
+ }
+
+ final class MqttPendingHandler {
+ private final MqttHandler handler;
+ private final boolean once;
+
+ MqttPendingHandler(MqttHandler handler, boolean once) {
+ this.handler = handler;
+ this.once = once;
+ }
+
+ MqttHandler getHandler() {
+ return handler;
+ }
+
+ boolean isOnce() {
+ return once;
+ }
+ }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscribtion.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscribtion.java
new file mode 100644
index 0000000..a626a81
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscribtion.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.channel.EventLoop;
+import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage;
+import io.netty.util.concurrent.Promise;
+
+import java.util.function.Consumer;
+
+final class MqttPendingUnsubscribtion {
+
+ private final Promise<Void> future;
+ private final String topic;
+
+ private final RetransmissionHandler<MqttUnsubscribeMessage> retransmissionHandler = new RetransmissionHandler<>();
+
+ MqttPendingUnsubscribtion(Promise<Void> future, String topic, MqttUnsubscribeMessage unsubscribeMessage) {
+ this.future = future;
+ this.topic = topic;
+
+ this.retransmissionHandler.setOriginalMessage(unsubscribeMessage);
+ }
+
+ Promise<Void> getFuture() {
+ return future;
+ }
+
+ String getTopic() {
+ return topic;
+ }
+
+ void startRetransmissionTimer(EventLoop eventLoop, Consumer<Object> sendPacket) {
+ this.retransmissionHandler.setHandle((fixedHeader, originalMessage) ->
+ sendPacket.accept(new MqttUnsubscribeMessage(fixedHeader, originalMessage.variableHeader(), originalMessage.payload())));
+ this.retransmissionHandler.start(eventLoop);
+ }
+
+ void onUnsubackReceived(){
+ this.retransmissionHandler.stop();
+ }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPingHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPingHandler.java
new file mode 100644
index 0000000..d0fd998
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPingHandler.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.mqtt.MqttFixedHeader;
+import io.netty.handler.codec.mqtt.MqttMessage;
+import io.netty.handler.codec.mqtt.MqttMessageType;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import io.netty.handler.timeout.IdleStateEvent;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.concurrent.ScheduledFuture;
+
+import java.util.concurrent.TimeUnit;
+
+final class MqttPingHandler extends ChannelInboundHandlerAdapter {
+
+ private final int keepaliveSeconds;
+
+ private ScheduledFuture<?> pingRespTimeout;
+
+ MqttPingHandler(int keepaliveSeconds) {
+ this.keepaliveSeconds = keepaliveSeconds;
+ }
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+ if (!(msg instanceof MqttMessage)) {
+ ctx.fireChannelRead(msg);
+ return;
+ }
+ MqttMessage message = (MqttMessage) msg;
+ if(message.fixedHeader().messageType() == MqttMessageType.PINGREQ){
+ this.handlePingReq(ctx.channel());
+ } else if(message.fixedHeader().messageType() == MqttMessageType.PINGRESP){
+ this.handlePingResp();
+ }else{
+ ctx.fireChannelRead(ReferenceCountUtil.retain(msg));
+ }
+ }
+
+ @Override
+ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+ super.userEventTriggered(ctx, evt);
+
+ if(evt instanceof IdleStateEvent){
+ IdleStateEvent event = (IdleStateEvent) evt;
+ switch(event.state()){
+ case READER_IDLE:
+ break;
+ case WRITER_IDLE:
+ this.sendPingReq(ctx.channel());
+ break;
+ }
+ }
+ }
+
+ private void sendPingReq(Channel channel){
+ MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PINGREQ, false, MqttQoS.AT_MOST_ONCE, false, 0);
+ channel.writeAndFlush(new MqttMessage(fixedHeader));
+
+ if(this.pingRespTimeout != null){
+ this.pingRespTimeout = channel.eventLoop().schedule(() -> {
+ MqttFixedHeader fixedHeader2 = new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0);
+ channel.writeAndFlush(new MqttMessage(fixedHeader2)).addListener(ChannelFutureListener.CLOSE);
+ //TODO: what do when the connection is closed ?
+ }, this.keepaliveSeconds, TimeUnit.SECONDS);
+ }
+ }
+
+ private void handlePingReq(Channel channel){
+ MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PINGRESP, false, MqttQoS.AT_MOST_ONCE, false, 0);
+ channel.writeAndFlush(new MqttMessage(fixedHeader));
+ }
+
+ private void handlePingResp(){
+ if(this.pingRespTimeout != null && !this.pingRespTimeout.isCancelled() && !this.pingRespTimeout.isDone()){
+ this.pingRespTimeout.cancel(true);
+ this.pingRespTimeout = null;
+ }
+ }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscribtion.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscribtion.java
new file mode 100644
index 0000000..27f4cb9
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscribtion.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import java.util.regex.Pattern;
+
+final class MqttSubscribtion {
+
+ private final String topic;
+ private final Pattern topicRegex;
+ private final MqttHandler handler;
+
+ private final boolean once;
+
+ private boolean called;
+
+ MqttSubscribtion(String topic, MqttHandler handler, boolean once) {
+ if(topic == null){
+ throw new NullPointerException("topic");
+ }
+ if(handler == null){
+ throw new NullPointerException("handler");
+ }
+ this.topic = topic;
+ this.handler = handler;
+ this.once = once;
+ this.topicRegex = Pattern.compile(topic.replace("+", "[^/]+").replace("#", ".+") + "$");
+ }
+
+ String getTopic() {
+ return topic;
+ }
+
+ public MqttHandler getHandler() {
+ return handler;
+ }
+
+ boolean isOnce() {
+ return once;
+ }
+
+ boolean isCalled() {
+ return called;
+ }
+
+ boolean matches(String topic){
+ return this.topicRegex.matcher(topic).matches();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ MqttSubscribtion that = (MqttSubscribtion) o;
+
+ return once == that.once && topic.equals(that.topic) && handler.equals(that.handler);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = topic.hashCode();
+ result = 31 * result + handler.hashCode();
+ result = 31 * result + (once ? 1 : 0);
+ return result;
+ }
+
+ void setCalled(boolean called) {
+ this.called = called;
+ }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java
new file mode 100644
index 0000000..36e91e5
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.channel.EventLoop;
+import io.netty.handler.codec.mqtt.MqttFixedHeader;
+import io.netty.handler.codec.mqtt.MqttMessage;
+import io.netty.util.concurrent.ScheduledFuture;
+
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+
+final class RetransmissionHandler<T extends MqttMessage> {
+
+ private ScheduledFuture<?> timer;
+ private int timeout = 10;
+ private BiConsumer<MqttFixedHeader, T> handler;
+ private T originalMessage;
+
+ void start(EventLoop eventLoop){
+ if(eventLoop == null){
+ throw new NullPointerException("eventLoop");
+ }
+ if(this.handler == null){
+ throw new NullPointerException("handler");
+ }
+ this.timeout = 10;
+ this.startTimer(eventLoop);
+ }
+
+ private void startTimer(EventLoop eventLoop){
+ this.timer = eventLoop.schedule(() -> {
+ this.timeout += 5;
+ MqttFixedHeader fixedHeader = new MqttFixedHeader(this.originalMessage.fixedHeader().messageType(), true, this.originalMessage.fixedHeader().qosLevel(), this.originalMessage.fixedHeader().isRetain(), this.originalMessage.fixedHeader().remainingLength());
+ handler.accept(fixedHeader, originalMessage);
+ startTimer(eventLoop);
+ }, timeout, TimeUnit.SECONDS);
+ }
+
+ void stop(){
+ if(this.timer != null){
+ this.timer.cancel(true);
+ }
+ }
+
+ void setHandle(BiConsumer<MqttFixedHeader, T> runnable) {
+ this.handler = runnable;
+ }
+
+ void setOriginalMessage(T originalMessage) {
+ this.originalMessage = originalMessage;
+ }
+}
pom.xml 97(+31 -66)
diff --git a/pom.xml b/pom.xml
index 15950ec..a767696 100755
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.thingsboard</groupId>
<artifactId>thingsboard</artifactId>
- <version>1.4.1-SNAPSHOT</version>
+ <version>2.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Thingsboard</name>
@@ -35,16 +35,16 @@
<spring-data-redis.version>1.8.10.RELEASE</spring-data-redis.version>
<jedis.version>2.9.0</jedis.version>
<jjwt.version>0.7.0</jjwt.version>
- <json-path.version>2.2.0</json-path.version>
+ <json-path.version>2.2.0</json-path.version>
<junit.version>4.12</junit.version>
<slf4j.version>1.7.7</slf4j.version>
<logback.version>1.2.3</logback.version>
<mockito.version>1.9.5</mockito.version>
<rat.version>0.10</rat.version>
- <cassandra.version>3.0.7</cassandra.version>
- <cassandra-unit.version>3.0.0.1</cassandra-unit.version>
+ <cassandra.version>3.5.0</cassandra.version>
+ <cassandra-unit.version>3.3.0.2</cassandra-unit.version>
<takari-cpsuite.version>1.2.7</takari-cpsuite.version>
- <guava.version>18.0</guava.version>
+ <guava.version>21.0</guava.version>
<caffeine.version>2.6.1</caffeine.version>
<commons-lang3.version>3.4</commons-lang3.version>
<commons-validator.version>1.5.0</commons-validator.version>
@@ -59,17 +59,15 @@
<velocity.version>1.7</velocity.version>
<velocity-tools.version>2.0</velocity-tools.version>
<mail.version>1.4.3</mail.version>
- <curator.version>2.11.0</curator.version>
+ <curator.version>4.0.1</curator.version>
<protobuf.version>3.0.2</protobuf.version>
- <grpc.version>1.0.0</grpc.version>
+ <grpc.version>1.12.0</grpc.version>
<lombok.version>1.16.18</lombok.version>
<paho.client.version>1.1.0</paho.client.version>
<netty.version>4.1.22.Final</netty.version>
<os-maven-plugin.version>1.5.0</os-maven-plugin.version>
<rabbitmq.version>3.6.5</rabbitmq.version>
<kafka.version>0.9.0.0</kafka.version>
- <hazelcast.version>3.6.6</hazelcast.version>
- <hazelcast-zookeeper.version>3.6.1</hazelcast-zookeeper.version>
<surfire.version>2.19.1</surfire.version>
<jar-plugin.version>3.0.2</jar-plugin.version>
<springfox-swagger.version>2.6.1</springfox-swagger.version>
@@ -79,16 +77,18 @@
<dbunit.version>2.5.3</dbunit.version>
<spring-test-dbunit.version>1.2.1</spring-test-dbunit.version>
<postgresql.driver.version>9.4.1211</postgresql.driver.version>
- <sonar.exclusions>org/thingsboard/server/gen/**/*, org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/*</sonar.exclusions>
+ <sonar.exclusions>org/thingsboard/server/gen/**/*,
+ org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/*
+ </sonar.exclusions>
<elasticsearch.version>5.0.2</elasticsearch.version>
+ <delight-nashorn-sandbox.version>0.1.14</delight-nashorn-sandbox.version>
</properties>
<modules>
+ <module>netty-mqtt</module>
<module>common</module>
+ <module>rule-engine</module>
<module>dao</module>
- <module>extensions-api</module>
- <module>extensions-core</module>
- <module>extensions</module>
<module>transport</module>
<module>ui</module>
<module>tools</module>
@@ -281,6 +281,7 @@
<exclude>src/sh/**</exclude>
<exclude>src/main/scripts/control/**</exclude>
<exclude>src/main/scripts/windows/**</exclude>
+ <exclude>src/main/resources/public/static/rulenode/**</exclude>
</excludes>
<mapping>
<proto>JAVADOC_STYLE</proto>
@@ -321,53 +322,22 @@
<dependencies>
<dependency>
<groupId>org.thingsboard</groupId>
- <artifactId>extensions-api</artifactId>
+ <artifactId>netty-mqtt</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>org.thingsboard</groupId>
- <artifactId>extensions-core</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.thingsboard.extensions</groupId>
- <artifactId>extension-rabbitmq</artifactId>
- <classifier>extension</classifier>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.thingsboard.extensions</groupId>
- <artifactId>extension-rest-api-call</artifactId>
- <classifier>extension</classifier>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.thingsboard.extensions</groupId>
- <artifactId>extension-kafka</artifactId>
- <classifier>extension</classifier>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.thingsboard.extensions</groupId>
- <artifactId>extension-mqtt</artifactId>
- <classifier>extension</classifier>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.thingsboard.extensions</groupId>
- <artifactId>extension-sqs</artifactId>
- <classifier>extension</classifier>
+ <groupId>org.thingsboard.common</groupId>
+ <artifactId>data</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>org.thingsboard.extensions</groupId>
- <artifactId>extension-sns</artifactId>
- <classifier>extension</classifier>
+ <groupId>org.thingsboard.rule-engine</groupId>
+ <artifactId>rule-engine-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>org.thingsboard.common</groupId>
- <artifactId>data</artifactId>
+ <groupId>org.thingsboard.rule-engine</groupId>
+ <artifactId>rule-engine-components</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
@@ -554,6 +524,11 @@
<version>${netty.version}</version>
</dependency>
<dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-codec-mqtt</artifactId>
+ <version>${netty.version}</version>
+ </dependency>
+ <dependency>
<groupId>com.datastax.cassandra</groupId>
<artifactId>cassandra-driver-core</artifactId>
<version>${cassandra.version}</version>
@@ -736,26 +711,11 @@
<version>${paho.client.version}</version>
</dependency>
<dependency>
- <groupId>com.hazelcast</groupId>
- <artifactId>hazelcast-spring</artifactId>
- <version>${hazelcast.version}</version>
- </dependency>
- <dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-x-discovery</artifactId>
<version>${curator.version}</version>
</dependency>
<dependency>
- <groupId>com.hazelcast</groupId>
- <artifactId>hazelcast-zookeeper</artifactId>
- <version>${hazelcast-zookeeper.version}</version>
- </dependency>
- <dependency>
- <groupId>com.hazelcast</groupId>
- <artifactId>hazelcast</artifactId>
- <version>${hazelcast.version}</version>
- </dependency>
- <dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox-swagger.version}</version>
@@ -809,6 +769,11 @@
<artifactId>rest</artifactId>
<version>${elasticsearch.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.javadelight</groupId>
+ <artifactId>delight-nashorn-sandbox</artifactId>
+ <version>${delight-nashorn-sandbox.version}</version>
+ </dependency>
</dependencies>
</dependencyManagement>
rule-engine/pom.xml 41(+41 -0)
diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml
new file mode 100644
index 0000000..0ee609c
--- /dev/null
+++ b/rule-engine/pom.xml
@@ -0,0 +1,41 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.thingsboard</groupId>
+ <version>2.0.0-SNAPSHOT</version>
+ <artifactId>thingsboard</artifactId>
+ </parent>
+ <artifactId>rule-engine</artifactId>
+ <packaging>pom</packaging>
+
+ <name>Thingsboard Extensions</name>
+ <url>https://thingsboard.io</url>
+
+ <properties>
+ <main.dir>${basedir}/..</main.dir>
+ </properties>
+
+ <modules>
+ <module>rule-engine-api</module>
+ <module>rule-engine-components</module>
+ </modules>
+
+</project>
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/EmptyNodeConfiguration.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/EmptyNodeConfiguration.java
new file mode 100644
index 0000000..9553b13
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/EmptyNodeConfiguration.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.api;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+@Data
+public class EmptyNodeConfiguration implements NodeConfiguration<EmptyNodeConfiguration> {
+
+ private int version;
+
+ @Override
+ public EmptyNodeConfiguration defaultConfiguration() {
+ EmptyNodeConfiguration configuration = new EmptyNodeConfiguration();
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java
new file mode 100644
index 0000000..7852715
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.thingsboard.rule.engine.api;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+
+@Data
+public class NodeDefinition {
+
+ private String details;
+ private String description;
+ private boolean inEnabled;
+ private boolean outEnabled;
+ String[] relationTypes;
+ boolean customRelations;
+ JsonNode defaultConfiguration;
+ String[] uiResources;
+ String configDirective;
+ String icon;
+ String iconUrl;
+ String docUrl;
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceRpcRequest.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceRpcRequest.java
new file mode 100644
index 0000000..5ea481a
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceRpcRequest.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.api;
+
+import lombok.Builder;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.DeviceId;
+
+/**
+ * Created by ashvayka on 02.04.18.
+ */
+@Data
+@Builder
+public final class RuleEngineDeviceRpcRequest {
+
+ private final DeviceId deviceId;
+ private final int requestId;
+ private final boolean oneway;
+ private final String method;
+ private final String body;
+ private final long timeout;
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java
new file mode 100644
index 0000000..aa57a02
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.api;
+
+import com.google.common.util.concurrent.FutureCallback;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+
+import java.util.List;
+
+/**
+ * Created by ashvayka on 02.04.18.
+ */
+public interface RuleEngineTelemetryService {
+
+ void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback);
+
+ void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, long ttl, FutureCallback<Void> callback);
+
+ void saveAndNotify(EntityId entityId, String scope, List<AttributeKvEntry> attributes, FutureCallback<Void> callback);
+
+ void saveAttrAndNotify(EntityId entityId, String scope, String key, long value, FutureCallback<Void> callback);
+
+ void saveAttrAndNotify(EntityId entityId, String scope, String key, String value, FutureCallback<Void> callback);
+
+ void saveAttrAndNotify(EntityId entityId, String scope, String key, double value, FutureCallback<Void> callback);
+
+ void saveAttrAndNotify(EntityId entityId, String scope, String key, boolean value, FutureCallback<Void> callback);
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
new file mode 100644
index 0000000..a3d6db4
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
@@ -0,0 +1,95 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.api;
+
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.alarm.AlarmService;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.dao.user.UserService;
+
+import java.util.Set;
+
+/**
+ * Created by ashvayka on 13.01.18.
+ */
+public interface TbContext {
+
+ void tellNext(TbMsg msg, String relationType);
+
+ void tellNext(TbMsg msg, String relationType, Throwable th);
+
+ void tellNext(TbMsg msg, Set<String> relationTypes);
+
+ void tellSelf(TbMsg msg, long delayMs);
+
+ void tellFailure(TbMsg msg, Throwable th);
+
+ void updateSelf(RuleNode self);
+
+ TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data);
+
+ TbMsg transformMsg(TbMsg origMsg, String type, EntityId originator, TbMsgMetaData metaData, String data);
+
+ RuleNodeId getSelfId();
+
+ TenantId getTenantId();
+
+ AttributesService getAttributesService();
+
+ CustomerService getCustomerService();
+
+ UserService getUserService();
+
+ AssetService getAssetService();
+
+ DeviceService getDeviceService();
+
+ AlarmService getAlarmService();
+
+ RuleChainService getRuleChainService();
+
+ RuleEngineRpcService getRpcService();
+
+ RuleEngineTelemetryService getTelemetryService();
+
+ TimeseriesService getTimeseriesService();
+
+ RelationService getRelationService();
+
+ ListeningExecutor getJsExecutor();
+
+ ListeningExecutor getMailExecutor();
+
+ ListeningExecutor getDbCallbackExecutor();
+
+ ListeningExecutor getExternalCallExecutor();
+
+ MailService getMailService();
+
+ ScriptEngine createJsScriptEngine(String script, String... argNames);
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/DonAsynchron.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/DonAsynchron.java
new file mode 100644
index 0000000..81220f6
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/DonAsynchron.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.api.util;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import javax.annotation.Nullable;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+public class DonAsynchron {
+
+ public static <T> void withCallback(ListenableFuture<T> future, Consumer<T> onSuccess,
+ Consumer<Throwable> onFailure) {
+ withCallback(future, onSuccess, onFailure, null);
+ }
+
+ public static <T> void withCallback(ListenableFuture<T> future, Consumer<T> onSuccess,
+ Consumer<Throwable> onFailure, Executor executor) {
+ FutureCallback<T> callback = new FutureCallback<T>() {
+ @Override
+ public void onSuccess(@Nullable T result) {
+ try {
+ onSuccess.accept(result);
+ } catch (Throwable th) {
+ onFailure(th);
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ onFailure.accept(t);
+ }
+ };
+ if (executor != null) {
+ Futures.addCallback(future, callback, executor);
+ } else {
+ Futures.addCallback(future, callback);
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java
new file mode 100644
index 0000000..dff7cf4
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.api.util;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.util.Map;
+
+/**
+ * Created by ashvayka on 19.01.18.
+ */
+public class TbNodeUtils {
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ private static final String VARIABLE_TEMPLATE = "${%s}";
+
+
+ public static <T> T convert(TbNodeConfiguration configuration, Class<T> clazz) throws TbNodeException {
+ try {
+ return mapper.treeToValue(configuration.getData(), clazz);
+ } catch (JsonProcessingException e) {
+ throw new TbNodeException(e);
+ }
+ }
+
+ public static String processPattern(String pattern, TbMsgMetaData metaData) {
+ String result = new String(pattern);
+ for (Map.Entry<String,String> keyVal : metaData.values().entrySet()) {
+ result = processVar(result, keyVal.getKey(), keyVal.getValue());
+ }
+ return result;
+ }
+
+ private static String processVar(String pattern, String key, String val) {
+ String varPattern = String.format(VARIABLE_TEMPLATE, key);
+ return pattern.replace(varPattern, val);
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java
new file mode 100644
index 0000000..1fa2350
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java
@@ -0,0 +1,117 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.action;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+
+@Slf4j
+public abstract class TbAbstractAlarmNode<C extends TbAbstractAlarmNodeConfiguration> implements TbNode {
+
+ static final String PREV_ALARM_DETAILS = "prevAlarmDetails";
+
+ static final String IS_NEW_ALARM = "isNewAlarm";
+ static final String IS_EXISTING_ALARM = "isExistingAlarm";
+ static final String IS_CLEARED_ALARM = "isClearedAlarm";
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ protected C config;
+ private ScriptEngine buildDetailsJsEngine;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = loadAlarmNodeConfig(configuration);
+ this.buildDetailsJsEngine = ctx.createJsScriptEngine(config.getAlarmDetailsBuildJs());
+ }
+
+ protected abstract C loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException;
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ withCallback(processAlarm(ctx, msg),
+ alarmResult -> {
+ if (alarmResult.alarm == null) {
+ ctx.tellNext(msg, "False");
+ } else if (alarmResult.isCreated) {
+ ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), "Created");
+ } else if (alarmResult.isUpdated) {
+ ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), "Updated");
+ } else if (alarmResult.isCleared) {
+ ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), "Cleared");
+ }
+ },
+ t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
+ }
+
+ protected abstract ListenableFuture<AlarmResult> processAlarm(TbContext ctx, TbMsg msg);
+
+ protected ListenableFuture<JsonNode> buildAlarmDetails(TbContext ctx, TbMsg msg, JsonNode previousDetails) {
+ return ctx.getJsExecutor().executeAsync(() -> {
+ TbMsg dummyMsg = msg;
+ if (previousDetails != null) {
+ TbMsgMetaData metaData = msg.getMetaData().copy();
+ metaData.putValue(PREV_ALARM_DETAILS, mapper.writeValueAsString(previousDetails));
+ dummyMsg = ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), metaData, msg.getData());
+ }
+ return buildDetailsJsEngine.executeJson(dummyMsg);
+ });
+ }
+
+ private TbMsg toAlarmMsg(TbContext ctx, AlarmResult alarmResult, TbMsg originalMsg) {
+ JsonNode jsonNodes = mapper.valueToTree(alarmResult.alarm);
+ String data = jsonNodes.toString();
+ TbMsgMetaData metaData = originalMsg.getMetaData().copy();
+ if (alarmResult.isCreated) {
+ metaData.putValue(IS_NEW_ALARM, Boolean.TRUE.toString());
+ } else if (alarmResult.isUpdated) {
+ metaData.putValue(IS_EXISTING_ALARM, Boolean.TRUE.toString());
+ } else if (alarmResult.isCleared) {
+ metaData.putValue(IS_CLEARED_ALARM, Boolean.TRUE.toString());
+ }
+ return ctx.transformMsg(originalMsg, "ALARM", originalMsg.getOriginator(), metaData, data);
+ }
+
+
+ @Override
+ public void destroy() {
+ if (buildDetailsJsEngine != null) {
+ buildDetailsJsEngine.destroy();
+ }
+ }
+
+ protected static class AlarmResult {
+ boolean isCreated;
+ boolean isUpdated;
+ boolean isCleared;
+ Alarm alarm;
+
+ AlarmResult(boolean isCreated, boolean isUpdated, boolean isCleared, Alarm alarm) {
+ this.isCreated = isCreated;
+ this.isUpdated = isUpdated;
+ this.isCleared = isCleared;
+ this.alarm = alarm;
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java
new file mode 100644
index 0000000..2d722f5
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.action;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.ACTION,
+ name = "clear alarm", relationTypes = {"Cleared", "False"},
+ configClazz = TbClearAlarmNodeConfiguration.class,
+ nodeDescription = "Clear Alarm",
+ nodeDetails =
+ "Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.\n" +
+ "Node output:\n" +
+ "If alarm was not cleared, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'matadata' will contains 'isClearedAlarm' property. " +
+ "Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>. " +
+ "Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbActionNodeClearAlarmConfig",
+ icon = "notifications_off"
+)
+public class TbClearAlarmNode extends TbAbstractAlarmNode<TbClearAlarmNodeConfiguration> {
+
+ @Override
+ protected TbClearAlarmNodeConfiguration loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException {
+ return TbNodeUtils.convert(configuration, TbClearAlarmNodeConfiguration.class);
+ }
+
+ @Override
+ protected ListenableFuture<AlarmResult> processAlarm(TbContext ctx, TbMsg msg) {
+ ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
+ return Futures.transformAsync(latest, a -> {
+ if (a != null && !a.getStatus().isCleared()) {
+ return clearAlarm(ctx, msg, a);
+ }
+ return Futures.immediateFuture(new AlarmResult(false, false, false, null));
+ }, ctx.getDbCallbackExecutor());
+ }
+
+ private ListenableFuture<AlarmResult> clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
+ ListenableFuture<JsonNode> asyncDetails = buildAlarmDetails(ctx, msg, alarm.getDetails());
+ return Futures.transformAsync(asyncDetails, details -> {
+ ListenableFuture<Boolean> clearFuture = ctx.getAlarmService().clearAlarm(alarm.getId(), details, System.currentTimeMillis());
+ return Futures.transformAsync(clearFuture, cleared -> {
+ if (cleared && details != null) {
+ alarm.setDetails(details);
+ }
+ alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK);
+ return Futures.immediateFuture(new AlarmResult(false, false, true, alarm));
+ });
+ }, ctx.getDbCallbackExecutor());
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java
new file mode 100644
index 0000000..f3f3072
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.action;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+
+@Data
+public class TbClearAlarmNodeConfiguration extends TbAbstractAlarmNodeConfiguration implements NodeConfiguration<TbClearAlarmNodeConfiguration> {
+
+ @Override
+ public TbClearAlarmNodeConfiguration defaultConfiguration() {
+ TbClearAlarmNodeConfiguration configuration = new TbClearAlarmNodeConfiguration();
+ configuration.setAlarmDetailsBuildJs("var details = {};\n" +
+ "if (metadata.prevAlarmDetails) {\n" +
+ " details = JSON.parse(metadata.prevAlarmDetails);\n" +
+ "}\n" +
+ "return details;");
+ configuration.setAlarmType("General Alarm");
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java
new file mode 100644
index 0000000..a660c93
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.action;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.ACTION,
+ name = "create alarm", relationTypes = {"Created", "Updated", "False"},
+ configClazz = TbCreateAlarmNodeConfiguration.class,
+ nodeDescription = "Create or Update Alarm",
+ nodeDetails =
+ "Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.\n" +
+ "Node output:\n" +
+ "If alarm was not created, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'matadata' will contains one of those properties 'isNewAlarm/isExistingAlarm'. " +
+ "Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>. " +
+ "Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbActionNodeCreateAlarmConfig",
+ icon = "notifications_active"
+)
+public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConfiguration> {
+
+ @Override
+ protected TbCreateAlarmNodeConfiguration loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException {
+ return TbNodeUtils.convert(configuration, TbCreateAlarmNodeConfiguration.class);
+ }
+
+ @Override
+ protected ListenableFuture<AlarmResult> processAlarm(TbContext ctx, TbMsg msg) {
+ ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
+ return Futures.transformAsync(latest, a -> {
+ if (a == null || a.getStatus().isCleared()) {
+ return createNewAlarm(ctx, msg);
+ } else {
+ return updateAlarm(ctx, msg, a);
+ }
+ }, ctx.getDbCallbackExecutor());
+
+ }
+
+ private ListenableFuture<AlarmResult> createNewAlarm(TbContext ctx, TbMsg msg) {
+ ListenableFuture<Alarm> asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg, null),
+ details -> buildAlarm(msg, details, ctx.getTenantId()));
+ ListenableFuture<Alarm> asyncCreated = Futures.transform(asyncAlarm,
+ alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor());
+ return Futures.transform(asyncCreated, alarm -> new AlarmResult(true, false, false, alarm));
+ }
+
+ private ListenableFuture<AlarmResult> updateAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
+ ListenableFuture<Alarm> asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg, alarm.getDetails()), (Function<JsonNode, Alarm>) details -> {
+ alarm.setSeverity(config.getSeverity());
+ alarm.setPropagate(config.isPropagate());
+ alarm.setDetails(details);
+ alarm.setEndTs(System.currentTimeMillis());
+ return ctx.getAlarmService().createOrUpdateAlarm(alarm);
+ }, ctx.getDbCallbackExecutor());
+
+ return Futures.transform(asyncUpdated, a -> new AlarmResult(false, true, false, a));
+ }
+
+ private Alarm buildAlarm(TbMsg msg, JsonNode details, TenantId tenantId) {
+ return Alarm.builder()
+ .tenantId(tenantId)
+ .originator(msg.getOriginator())
+ .status(AlarmStatus.ACTIVE_UNACK)
+ .severity(config.getSeverity())
+ .propagate(config.isPropagate())
+ .type(config.getAlarmType())
+ //todo-vp: alarm date should be taken from Message or current Time should be used?
+// .startTs(System.currentTimeMillis())
+// .endTs(System.currentTimeMillis())
+ .details(details)
+ .build();
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java
new file mode 100644
index 0000000..b424794
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.action;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+
+@Data
+public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfiguration implements NodeConfiguration<TbCreateAlarmNodeConfiguration> {
+
+ private AlarmSeverity severity;
+ private boolean propagate;
+
+ @Override
+ public TbCreateAlarmNodeConfiguration defaultConfiguration() {
+ TbCreateAlarmNodeConfiguration configuration = new TbCreateAlarmNodeConfiguration();
+ configuration.setAlarmDetailsBuildJs("var details = {};\n" +
+ "if (metadata.prevAlarmDetails) {\n" +
+ " details = JSON.parse(metadata.prevAlarmDetails);\n" +
+ "}\n"+
+ "return details;");
+ configuration.setAlarmType("General Alarm");
+ configuration.setSeverity(AlarmSeverity.CRITICAL);
+ configuration.setPropagate(false);
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
new file mode 100644
index 0000000..7cab0c2
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.action;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.ACTION,
+ name = "log",
+ configClazz = TbLogNodeConfiguration.class,
+ nodeDescription = "Log incoming messages using JS script for transformation Message into String",
+ nodeDetails = "Transform incoming Message with configured JS function to String and log final value into Thingsboard log file. " +
+ "Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>. " +
+ "Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbActionNodeLogConfig",
+ icon = "menu"
+)
+
+public class TbLogNode implements TbNode {
+
+ private TbLogNodeConfiguration config;
+ private ScriptEngine jsEngine;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbLogNodeConfiguration.class);
+ this.jsEngine = ctx.createJsScriptEngine(config.getJsScript());
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ ListeningExecutor jsExecutor = ctx.getJsExecutor();
+ withCallback(jsExecutor.executeAsync(() -> jsEngine.executeToString(msg)),
+ toString -> {
+ log.info(toString);
+ ctx.tellNext(msg, SUCCESS);
+ },
+ t -> ctx.tellFailure(msg, t));
+ }
+
+ @Override
+ public void destroy() {
+ if (jsEngine != null) {
+ jsEngine.destroy();
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java
new file mode 100644
index 0000000..7393099
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java
@@ -0,0 +1,120 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.aws.sns;
+
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.services.sns.AmazonSNS;
+import com.amazonaws.services.sns.AmazonSNSClient;
+import com.amazonaws.services.sns.model.PublishRequest;
+import com.amazonaws.services.sns.model.PublishResult;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.util.concurrent.ExecutionException;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.EXTERNAL,
+ name = "aws sns",
+ configClazz = TbSnsNodeConfiguration.class,
+ nodeDescription = "Publish message to the AWS SNS",
+ nodeDetails = "Will publish message payload to the AWS SNS topic. Outbound message will contain response fields " +
+ "(<code>messageId</code>, <code>requestId</code>) in the Message Metadata from the AWS SNS. " +
+ "For example <b>requestId</b> field can be accessed with <code>metadata.requestId</code>.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbActionNodeSnsConfig",
+ iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+"
+)
+public class TbSnsNode implements TbNode {
+
+ private static final String MESSAGE_ID = "messageId";
+ private static final String REQUEST_ID = "requestId";
+ private static final String ERROR = "error";
+
+ private TbSnsNodeConfiguration config;
+ private AmazonSNS snsClient;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbSnsNodeConfiguration.class);
+ AWSCredentials awsCredentials = new BasicAWSCredentials(this.config.getAccessKeyId(), this.config.getSecretAccessKey());
+ AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
+ try {
+ this.snsClient = AmazonSNSClient.builder()
+ .withCredentials(credProvider)
+ .withRegion(this.config.getRegion())
+ .build();
+ } catch (Exception e) {
+ throw new TbNodeException(e);
+ }
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
+ withCallback(publishMessageAsync(ctx, msg),
+ m -> ctx.tellNext(m, TbRelationTypes.SUCCESS),
+ t -> {
+ TbMsg next = processException(ctx, msg, t);
+ ctx.tellFailure(next, t);
+ });
+ }
+
+ private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {
+ return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(ctx, msg));
+ }
+
+ private TbMsg publishMessage(TbContext ctx, TbMsg msg) {
+ String topicArn = TbNodeUtils.processPattern(this.config.getTopicArnPattern(), msg.getMetaData());
+ PublishRequest publishRequest = new PublishRequest()
+ .withTopicArn(topicArn)
+ .withMessage(msg.getData());
+ PublishResult result = this.snsClient.publish(publishRequest);
+ return processPublishResult(ctx, msg, result);
+ }
+
+ private TbMsg processPublishResult(TbContext ctx, TbMsg origMsg, PublishResult result) {
+ TbMsgMetaData metaData = origMsg.getMetaData().copy();
+ metaData.putValue(MESSAGE_ID, result.getMessageId());
+ metaData.putValue(REQUEST_ID, result.getSdkResponseMetadata().getRequestId());
+ return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+ }
+
+ private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) {
+ TbMsgMetaData metaData = origMsg.getMetaData().copy();
+ metaData.putValue(ERROR, t.getClass() + ": " + t.getMessage());
+ return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+ }
+
+ @Override
+ public void destroy() {
+ if (this.snsClient != null) {
+ try {
+ this.snsClient.shutdown();
+ } catch (Exception e) {
+ log.error("Failed to shutdown SNS client during destroy()", e);
+ }
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeConfiguration.java
new file mode 100644
index 0000000..a124778
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeConfiguration.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.thingsboard.rule.engine.aws.sns;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbSnsNodeConfiguration implements NodeConfiguration<TbSnsNodeConfiguration> {
+
+ private String topicArnPattern;
+ private String accessKeyId;
+ private String secretAccessKey;
+ private String region;
+
+ @Override
+ public TbSnsNodeConfiguration defaultConfiguration() {
+ TbSnsNodeConfiguration configuration = new TbSnsNodeConfiguration();
+ configuration.setTopicArnPattern("arn:aws:sns:us-east-1:123456789012:MyNewTopic");
+ configuration.setRegion("us-east-1");
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java
new file mode 100644
index 0000000..fd67605
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java
@@ -0,0 +1,151 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.thingsboard.rule.engine.aws.sqs;
+
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.services.sqs.AmazonSQS;
+import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
+import com.amazonaws.services.sqs.model.MessageAttributeValue;
+import com.amazonaws.services.sqs.model.SendMessageRequest;
+import com.amazonaws.services.sqs.model.SendMessageResult;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.EXTERNAL,
+ name = "aws sqs",
+ configClazz = TbSqsNodeConfiguration.class,
+ nodeDescription = "Publish messages to the AWS SQS",
+ nodeDetails = "Will publish message payload and metadata attributes to the AWS SQS queue. Outbound message will contain " +
+ "response fields (<code>messageId</code>, <code>requestId</code>, <code>messageBodyMd5</code>, <code>messageAttributesMd5</code>" +
+ ", <code>sequenceNumber</code>) in the Message Metadata from the AWS SQS." +
+ " For example <b>requestId</b> field can be accessed with <code>metadata.requestId</code>.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbActionNodeSqsConfig",
+ iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+"
+)
+public class TbSqsNode implements TbNode {
+
+ private static final String MESSAGE_ID = "messageId";
+ private static final String REQUEST_ID = "requestId";
+ private static final String MESSAGE_BODY_MD5 = "messageBodyMd5";
+ private static final String MESSAGE_ATTRIBUTES_MD5 = "messageAttributesMd5";
+ private static final String SEQUENCE_NUMBER = "sequenceNumber";
+ private static final String ERROR = "error";
+
+ private TbSqsNodeConfiguration config;
+ private AmazonSQS sqsClient;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbSqsNodeConfiguration.class);
+ AWSCredentials awsCredentials = new BasicAWSCredentials(this.config.getAccessKeyId(), this.config.getSecretAccessKey());
+ AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
+ try {
+ this.sqsClient = AmazonSQSClientBuilder.standard()
+ .withCredentials(credProvider)
+ .withRegion(this.config.getRegion())
+ .build();
+ } catch (Exception e) {
+ throw new TbNodeException(e);
+ }
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
+ withCallback(publishMessageAsync(ctx, msg),
+ m -> ctx.tellNext(m, TbRelationTypes.SUCCESS),
+ t -> {
+ TbMsg next = processException(ctx, msg, t);
+ ctx.tellFailure(next, t);
+ });
+ }
+
+ private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {
+ return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(ctx, msg));
+ }
+
+ private TbMsg publishMessage(TbContext ctx, TbMsg msg) {
+ String queueUrl = TbNodeUtils.processPattern(this.config.getQueueUrlPattern(), msg.getMetaData());
+ SendMessageRequest sendMsgRequest = new SendMessageRequest();
+ sendMsgRequest.withQueueUrl(queueUrl);
+ sendMsgRequest.withMessageBody(msg.getData());
+ Map<String, MessageAttributeValue> messageAttributes = new HashMap<>();
+ this.config.getMessageAttributes().forEach((k,v) -> {
+ String name = TbNodeUtils.processPattern(k, msg.getMetaData());
+ String val = TbNodeUtils.processPattern(v, msg.getMetaData());
+ messageAttributes.put(name, new MessageAttributeValue().withDataType("String").withStringValue(val));
+ });
+ sendMsgRequest.setMessageAttributes(messageAttributes);
+ if (this.config.getQueueType() == TbSqsNodeConfiguration.QueueType.STANDARD) {
+ sendMsgRequest.withDelaySeconds(this.config.getDelaySeconds());
+ } else {
+ sendMsgRequest.withMessageDeduplicationId(msg.getId().toString());
+ sendMsgRequest.withMessageGroupId(msg.getOriginator().toString());
+ }
+ SendMessageResult result = this.sqsClient.sendMessage(sendMsgRequest);
+ return processSendMessageResult(ctx, msg, result);
+ }
+
+ private TbMsg processSendMessageResult(TbContext ctx, TbMsg origMsg, SendMessageResult result) {
+ TbMsgMetaData metaData = origMsg.getMetaData().copy();
+ metaData.putValue(MESSAGE_ID, result.getMessageId());
+ metaData.putValue(REQUEST_ID, result.getSdkResponseMetadata().getRequestId());
+ if (!StringUtils.isEmpty(result.getMD5OfMessageBody())) {
+ metaData.putValue(MESSAGE_BODY_MD5, result.getMD5OfMessageBody());
+ }
+ if (!StringUtils.isEmpty(result.getMD5OfMessageAttributes())) {
+ metaData.putValue(MESSAGE_ATTRIBUTES_MD5, result.getMD5OfMessageAttributes());
+ }
+ if (!StringUtils.isEmpty(result.getSequenceNumber())) {
+ metaData.putValue(SEQUENCE_NUMBER, result.getSequenceNumber());
+ }
+ return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+ }
+
+ private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) {
+ TbMsgMetaData metaData = origMsg.getMetaData().copy();
+ metaData.putValue(ERROR, t.getClass() + ": " + t.getMessage());
+ return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+ }
+
+ @Override
+ public void destroy() {
+ if (this.sqsClient != null) {
+ try {
+ this.sqsClient.shutdown();
+ } catch (Exception e) {
+ log.error("Failed to shutdown SQS client during destroy()", e);
+ }
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeConfiguration.java
new file mode 100644
index 0000000..d27af36
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeConfiguration.java
@@ -0,0 +1,51 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.thingsboard.rule.engine.aws.sqs;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Collections;
+import java.util.Map;
+
+@Data
+public class TbSqsNodeConfiguration implements NodeConfiguration<TbSqsNodeConfiguration> {
+
+ private QueueType queueType;
+ private String queueUrlPattern;
+ private int delaySeconds;
+ private Map<String, String> messageAttributes;
+ private String accessKeyId;
+ private String secretAccessKey;
+ private String region;
+
+ @Override
+ public TbSqsNodeConfiguration defaultConfiguration() {
+ TbSqsNodeConfiguration configuration = new TbSqsNodeConfiguration();
+ configuration.setQueueType(QueueType.STANDARD);
+ configuration.setQueueUrlPattern("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue-name");
+ configuration.setDelaySeconds(0);
+ configuration.setMessageAttributes(Collections.emptyMap());
+ configuration.setRegion("us-east-1");
+ return configuration;
+ }
+
+ public enum QueueType {
+ STANDARD,
+ FIFO
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
new file mode 100644
index 0000000..5a30dcb
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.debug;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.ACTION,
+ name = "generator",
+ configClazz = TbMsgGeneratorNodeConfiguration.class,
+ nodeDescription = "Periodically generates messages",
+ nodeDetails = "Generates messages with configurable period. Javascript function used for message generation.",
+ inEnabled = false,
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbActionNodeGeneratorConfig",
+ icon = "repeat"
+)
+
+public class TbMsgGeneratorNode implements TbNode {
+
+ public static final String TB_MSG_GENERATOR_NODE_MSG = "TbMsgGeneratorNodeMsg";
+
+ private TbMsgGeneratorNodeConfiguration config;
+ private ScriptEngine jsEngine;
+ private long delay;
+ private EntityId originatorId;
+ private UUID nextTickId;
+ private TbMsg prevMsg;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbMsgGeneratorNodeConfiguration.class);
+ this.delay = TimeUnit.SECONDS.toMillis(config.getPeriodInSeconds());
+ if (!StringUtils.isEmpty(config.getOriginatorId())) {
+ originatorId = EntityIdFactory.getByTypeAndUuid(config.getOriginatorType(), config.getOriginatorId());
+ } else {
+ originatorId = ctx.getSelfId();
+ }
+ this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType");
+ sentTickMsg(ctx);
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ if (msg.getType().equals(TB_MSG_GENERATOR_NODE_MSG) && msg.getId().equals(nextTickId)) {
+ withCallback(generate(ctx),
+ m -> {ctx.tellNext(m, SUCCESS); sentTickMsg(ctx);},
+ t -> {ctx.tellFailure(msg, t); sentTickMsg(ctx);});
+ }
+ }
+
+ private void sentTickMsg(TbContext ctx) {
+ TbMsg tickMsg = ctx.newMsg(TB_MSG_GENERATOR_NODE_MSG, ctx.getSelfId(), new TbMsgMetaData(), "");
+ nextTickId = tickMsg.getId();
+ ctx.tellSelf(tickMsg, delay);
+ }
+
+ private ListenableFuture<TbMsg> generate(TbContext ctx) {
+ return ctx.getJsExecutor().executeAsync(() -> {
+ if (prevMsg == null) {
+ prevMsg = ctx.newMsg( "", originatorId, new TbMsgMetaData(), "{}");
+ }
+ TbMsg generated = jsEngine.executeGenerate(prevMsg);
+ prevMsg = ctx.newMsg(generated.getType(), originatorId, generated.getMetaData(), generated.getData());
+ return prevMsg;
+ });
+ }
+
+ @Override
+ public void destroy() {
+ prevMsg = null;
+ if (jsEngine != null) {
+ jsEngine.destroy();
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java
new file mode 100644
index 0000000..c568e3d
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.debug;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.data.EntityType;
+
+import java.util.Map;
+
+@Data
+public class TbMsgGeneratorNodeConfiguration implements NodeConfiguration<TbMsgGeneratorNodeConfiguration> {
+
+ private int msgCount;
+ private int periodInSeconds;
+ private String originatorId;
+ private EntityType originatorType;
+ private String jsScript;
+
+ @Override
+ public TbMsgGeneratorNodeConfiguration defaultConfiguration() {
+ TbMsgGeneratorNodeConfiguration configuration = new TbMsgGeneratorNodeConfiguration();
+ configuration.setMsgCount(0);
+ configuration.setPeriodInSeconds(1);
+ configuration.setJsScript("var msg = { temp: 42, humidity: 77 };\n" +
+ "var metadata = { data: 40 };\n" +
+ "var msgType = \"DebugMsg\";\n\n" +
+ "return { msg: msg, metadata: metadata, msgType: msgType };");
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
new file mode 100644
index 0000000..73a9aad
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.FILTER,
+ name = "script", relationTypes = {"True", "False"},
+ configClazz = TbJsFilterNodeConfiguration.class,
+ nodeDescription = "Filter incoming messages using JS script",
+ nodeDetails = "Evaluate incoming Message with configured JS condition. " +
+ "If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." +
+ "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code><br/>" +
+ "Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code><br/>" +
+ "Message type can be accessed via <code>msgType</code> property.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbFilterNodeScriptConfig")
+
+public class TbJsFilterNode implements TbNode {
+
+ private TbJsFilterNodeConfiguration config;
+ private ScriptEngine jsEngine;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class);
+ this.jsEngine = ctx.createJsScriptEngine(config.getJsScript());
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ ListeningExecutor jsExecutor = ctx.getJsExecutor();
+ withCallback(jsExecutor.executeAsync(() -> jsEngine.executeFilter(msg)),
+ filterResult -> ctx.tellNext(msg, filterResult ? "True" : "False"),
+ t -> ctx.tellFailure(msg, t));
+ }
+
+ @Override
+ public void destroy() {
+ if (jsEngine != null) {
+ jsEngine.destroy();
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
new file mode 100644
index 0000000..c8f8c06
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.Set;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.FILTER,
+ name = "switch", customRelations = true,
+ relationTypes = {},
+ configClazz = TbJsSwitchNodeConfiguration.class,
+ nodeDescription = "Route incoming Message to one or multiple output chains",
+ nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " +
+ "If Array is empty - message not routed to next Node. " +
+ "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code><br/>" +
+ "Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code><br/>" +
+ "Message type can be accessed via <code>msgType</code> property.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbFilterNodeSwitchConfig")
+public class TbJsSwitchNode implements TbNode {
+
+ private TbJsSwitchNodeConfiguration config;
+ private ScriptEngine jsEngine;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class);
+ this.jsEngine = ctx.createJsScriptEngine(config.getJsScript());
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ ListeningExecutor jsExecutor = ctx.getJsExecutor();
+ withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(msg)),
+ result -> processSwitch(ctx, msg, result),
+ t -> ctx.tellFailure(msg, t));
+ }
+
+ private void processSwitch(TbContext ctx, TbMsg msg, Set<String> nextRelations) {
+ ctx.tellNext(msg, nextRelations);
+ }
+
+ @Override
+ public void destroy() {
+ if (jsEngine != null) {
+ jsEngine.destroy();
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java
new file mode 100644
index 0000000..d1dba1b
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.filter;
+
+import com.google.common.collect.Sets;
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Set;
+
+@Data
+public class TbJsSwitchNodeConfiguration implements NodeConfiguration<TbJsSwitchNodeConfiguration> {
+
+ private String jsScript;
+
+ @Override
+ public TbJsSwitchNodeConfiguration defaultConfiguration() {
+ TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration();
+ configuration.setJsScript("function nextRelation(metadata, msg) {\n" +
+ " return ['one','nine'];\n" +
+ "}\n" +
+ "if(msgType === 'POST_TELEMETRY_REQUEST') {\n" +
+ " return ['two'];\n" +
+ "}\n" +
+ "return nextRelation(metadata, msg);");
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java
new file mode 100644
index 0000000..161abb1
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 19.01.18.
+ */
+@Slf4j
+@RuleNode(
+ type = ComponentType.FILTER,
+ name = "message type",
+ configClazz = TbMsgTypeFilterNodeConfiguration.class,
+ relationTypes = {"True", "False"},
+ nodeDescription = "Filter incoming messages by Message Type",
+ nodeDetails = "If incoming MessageType is expected - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used.",
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbFilterNodeMessageTypeConfig")
+public class TbMsgTypeFilterNode implements TbNode {
+
+ TbMsgTypeFilterNodeConfiguration config;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbMsgTypeFilterNodeConfiguration.class);
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
+ ctx.tellNext(msg, config.getMessageTypes().contains(msg.getType()) ? "True" : "False");
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java
new file mode 100644
index 0000000..100d876
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.filter;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Created by ashvayka on 19.01.18.
+ */
+@Data
+public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration<TbMsgTypeFilterNodeConfiguration> {
+
+ private List<String> messageTypes;
+
+ @Override
+ public TbMsgTypeFilterNodeConfiguration defaultConfiguration() {
+ TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration();
+ configuration.setMessageTypes(Arrays.asList(
+ SessionMsgType.POST_ATTRIBUTES_REQUEST.name(),
+ SessionMsgType.POST_TELEMETRY_REQUEST.name(),
+ SessionMsgType.TO_SERVER_RPC_REQUEST.name()));
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java
new file mode 100644
index 0000000..5426278
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java
@@ -0,0 +1,88 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.FILTER,
+ name = "message type switch",
+ configClazz = EmptyNodeConfiguration.class,
+ relationTypes = {"Post attributes", "Post telemetry", "RPC Request", "Activity Event", "Inactivity Event",
+ "Connect Event", "Disconnect Event", "Entity Created", "Entity Updated", "Entity Deleted", "Entity Assigned",
+ "Entity Unassigned", "Attributes Updated", "Attributes Deleted", "Other"},
+ nodeDescription = "Route incoming messages by Message Type",
+ nodeDetails = "Sends messages with message types <b>\"Post attributes\", \"Post telemetry\", \"RPC Request\"</b> etc. via corresponding chain, otherwise <b>Other</b> chain is used.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbNodeEmptyConfig")
+public class TbMsgTypeSwitchNode implements TbNode {
+
+ EmptyNodeConfiguration config;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class);
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
+ String relationType;
+ if (msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name())) {
+ relationType = "Post attributes";
+ } else if (msg.getType().equals(SessionMsgType.POST_TELEMETRY_REQUEST.name())) {
+ relationType = "Post telemetry";
+ } else if (msg.getType().equals(SessionMsgType.TO_SERVER_RPC_REQUEST.name())) {
+ relationType = "RPC Request";
+ } else if (msg.getType().equals(DataConstants.ACTIVITY_EVENT)) {
+ relationType = "Activity Event";
+ } else if (msg.getType().equals(DataConstants.INACTIVITY_EVENT)) {
+ relationType = "Inactivity Event";
+ } else if (msg.getType().equals(DataConstants.CONNECT_EVENT)) {
+ relationType = "Connect Event";
+ } else if (msg.getType().equals(DataConstants.DISCONNECT_EVENT)) {
+ relationType = "Disconnect Event";
+ } else if (msg.getType().equals(DataConstants.ENTITY_CREATED)) {
+ relationType = "Entity Created";
+ } else if (msg.getType().equals(DataConstants.ENTITY_UPDATED)) {
+ relationType = "Entity Updated";
+ } else if (msg.getType().equals(DataConstants.ENTITY_DELETED)) {
+ relationType = "Entity Deleted";
+ } else if (msg.getType().equals(DataConstants.ENTITY_ASSIGNED)) {
+ relationType = "Entity Assigned";
+ } else if (msg.getType().equals(DataConstants.ENTITY_UNASSIGNED)) {
+ relationType = "Entity Unassigned";
+ } else if (msg.getType().equals(DataConstants.ATTRIBUTES_UPDATED)) {
+ relationType = "Attributes Updated";
+ } else if (msg.getType().equals(DataConstants.ATTRIBUTES_DELETED)) {
+ relationType = "Attributes Deleted";
+ } else {
+ relationType = "Other";
+ }
+ ctx.tellNext(msg, relationType);
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java
new file mode 100644
index 0000000..e4a54bd
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.FILTER,
+ name = "originator type switch",
+ configClazz = EmptyNodeConfiguration.class,
+ relationTypes = {"Device", "Asset", "Tenant", "Customer", "User", "Dashboard", "Rule chain", "Rule node"},
+ nodeDescription = "Route incoming messages by Message Originator Type",
+ nodeDetails = "Routes messages to chain according to the originator type ('Device', 'Asset', etc.).",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbNodeEmptyConfig")
+public class TbOriginatorTypeSwitchNode implements TbNode {
+
+ EmptyNodeConfiguration config;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class);
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
+ String relationType;
+ EntityType originatorType = msg.getOriginator().getEntityType();
+ switch (originatorType) {
+ case TENANT:
+ relationType = "Tenant";
+ break;
+ case CUSTOMER:
+ relationType = "Customer";
+ break;
+ case USER:
+ relationType = "User";
+ break;
+ case DASHBOARD:
+ relationType = "Dashboard";
+ break;
+ case ASSET:
+ relationType = "Asset";
+ break;
+ case DEVICE:
+ relationType = "Device";
+ break;
+ case RULE_CHAIN:
+ relationType = "Rule chain";
+ break;
+ case RULE_NODE:
+ relationType = "Rule node";
+ break;
+ default:
+ throw new TbNodeException("Unsupported originator type: " + originatorType);
+ }
+ ctx.tellNext(msg, relationType);
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java
new file mode 100644
index 0000000..aa8bf5f
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java
@@ -0,0 +1,120 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.kafka;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.kafka.clients.producer.*;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.util.Properties;
+import java.util.concurrent.ExecutionException;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.EXTERNAL,
+ name = "kafka",
+ configClazz = TbKafkaNodeConfiguration.class,
+ nodeDescription = "Publish messages to Kafka server",
+ nodeDetails = "Will send record via Kafka producer to Kafka server. " +
+ "Outbound message will contain response fields (<code>offset</code>, <code>partition</code>, <code>topic</code>)" +
+ " from the Kafka in the Message Metadata. For example <b>partition</b> field can be accessed with <code>metadata.partition</code>.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbActionNodeKafkaConfig",
+ iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUzOCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDQxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTIwMS44MTYgMjMwLjIxNmMtMTYuMTg2IDAtMzAuNjk3IDcuMTcxLTQwLjYzNCAxOC40NjFsLTI1LjQ2My0xOC4wMjZjMi43MDMtNy40NDIgNC4yNTUtMTUuNDMzIDQuMjU1LTIzLjc5NyAwLTguMjE5LTEuNDk4LTE2LjA3Ni00LjExMi0yMy40MDhsMjUuNDA2LTE3LjgzNWM5LjkzNiAxMS4yMzMgMjQuNDA5IDE4LjM2NSA0MC41NDggMTguMzY1IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI5Ljg3OS0yNC4zMDktNTQuMTg0LTU0LjE4NC01NC4xODQtMjkuODc1IDAtNTQuMTg0IDI0LjMwNS01NC4xODQgNTQuMTg0IDAgNS4zNDguODA4IDEwLjUwNSAyLjI1OCAxNS4zODlsLTI1LjQyMyAxNy44NDRjLTEwLjYyLTEzLjE3NS0yNS45MTEtMjIuMzc0LTQzLjMzMy0yNS4xODJ2LTMwLjY0YzI0LjU0NC01LjE1NSA0My4wMzctMjYuOTYyIDQzLjAzNy01My4wMTlDMTI0LjE3MSAyNC4zMDUgOTkuODYyIDAgNjkuOTg3IDAgNDAuMTEyIDAgMTUuODAzIDI0LjMwNSAxNS44MDMgNTQuMTg0YzAgMjUuNzA4IDE4LjAxNCA0Ny4yNDYgNDIuMDY3IDUyLjc2OXYzMS4wMzhDMjUuMDQ0IDE0My43NTMgMCAxNzIuNDAxIDAgMjA2Ljg1NGMwIDM0LjYyMSAyNS4yOTIgNjMuMzc0IDU4LjM1NSA2OC45NHYzMi43NzRjLTI0LjI5OSA1LjM0MS00Mi41NTIgMjcuMDExLTQyLjU1MiA1Mi44OTQgMCAyOS44NzkgMjQuMzA5IDU0LjE4NCA1NC4xODQgNTQuMTg0IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI1Ljg4My0xOC4yNTMtNDcuNTUzLTQyLjU1Mi01Mi44OTR2LTMyLjc3NWE2OS45NjUgNjkuOTY1IDAgMCAwIDQyLjYtMjQuNzc2bDI1LjYzMyAxOC4xNDNjLTEuNDIzIDQuODQtMi4yMiA5Ljk0Ni0yLjIyIDE1LjI0IDAgMjkuODc5IDI0LjMwOSA1NC4xODQgNTQuMTg0IDU0LjE4NCAyOS44NzUgMCA1NC4xODQtMjQuMzA1IDU0LjE4NC01NC4xODQgMC0yOS44NzktMjQuMzA5LTU0LjE4NC01NC4xODQtNTQuMTg0em0wLTEyNi42OTVjMTQuNDg3IDAgMjYuMjcgMTEuNzg4IDI2LjI3IDI2LjI3MXMtMTEuNzgzIDI2LjI3LTI2LjI3IDI2LjI3LTI2LjI3LTExLjc4Ny0yNi4yNy0yNi4yN2MwLTE0LjQ4MyAxMS43ODMtMjYuMjcxIDI2LjI3LTI2LjI3MXptLTE1OC4xLTQ5LjMzN2MwLTE0LjQ4MyAxMS43ODQtMjYuMjcgMjYuMjcxLTI2LjI3czI2LjI3IDExLjc4NyAyNi4yNyAyNi4yN2MwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3em01Mi41NDEgMzA3LjI3OGMwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3YzAtMTQuNDgzIDExLjc4NC0yNi4yNyAyNi4yNzEtMjYuMjdzMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3em0tMjYuMjcyLTExNy45N2MtMjAuMjA1IDAtMzYuNjQyLTE2LjQzNC0zNi42NDItMzYuNjM4IDAtMjAuMjA1IDE2LjQzNy0zNi42NDIgMzYuNjQyLTM2LjY0MiAyMC4yMDQgMCAzNi42NDEgMTYuNDM3IDM2LjY0MSAzNi42NDIgMCAyMC4yMDQtMTYuNDM3IDM2LjYzOC0zNi42NDEgMzYuNjM4em0xMzEuODMxIDY3LjE3OWMtMTQuNDg3IDAtMjYuMjctMTEuNzg4LTI2LjI3LTI2LjI3MXMxMS43ODMtMjYuMjcgMjYuMjctMjYuMjcgMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3YzAgMTQuNDgzLTExLjc4MyAyNi4yNzEtMjYuMjcgMjYuMjcxeiIvPjwvc3ZnPg=="
+)
+public class TbKafkaNode implements TbNode {
+
+ private static final String OFFSET = "offset";
+ private static final String PARTITION = "partition";
+ private static final String TOPIC = "topic";
+ private static final String ERROR = "error";
+
+ private TbKafkaNodeConfiguration config;
+
+ private Producer<?, String> producer;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbKafkaNodeConfiguration.class);
+ Properties properties = new Properties();
+ properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, config.getBootstrapServers());
+ properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, config.getValueSerializer());
+ properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, config.getKeySerializer());
+ properties.put(ProducerConfig.ACKS_CONFIG, config.getAcks());
+ properties.put(ProducerConfig.RETRIES_CONFIG, config.getRetries());
+ properties.put(ProducerConfig.BATCH_SIZE_CONFIG, config.getBatchSize());
+ properties.put(ProducerConfig.LINGER_MS_CONFIG, config.getLinger());
+ properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, config.getBufferMemory());
+ if (config.getOtherProperties() != null) {
+ config.getOtherProperties()
+ .forEach((k,v) -> properties.put(k, v));
+ }
+ try {
+ this.producer = new KafkaProducer<>(properties);
+ } catch (Exception e) {
+ throw new TbNodeException(e);
+ }
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
+ String topic = TbNodeUtils.processPattern(config.getTopicPattern(), msg.getMetaData());
+ try {
+ producer.send(new ProducerRecord<>(topic, msg.getData()),
+ (metadata, e) -> {
+ if (metadata != null) {
+ TbMsg next = processResponse(ctx, msg, metadata);
+ ctx.tellNext(next, TbRelationTypes.SUCCESS);
+ } else {
+ TbMsg next = processException(ctx, msg, e);
+ ctx.tellFailure(next, e);
+ }
+ });
+ } catch (Exception e) {
+ ctx.tellFailure(msg, e);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ if (this.producer != null) {
+ try {
+ this.producer.close();
+ } catch (Exception e) {
+ log.error("Failed to close producer during destroy()", e);
+ }
+ }
+ }
+
+ private TbMsg processResponse(TbContext ctx, TbMsg origMsg, RecordMetadata recordMetadata) {
+ TbMsgMetaData metaData = origMsg.getMetaData().copy();
+ metaData.putValue(OFFSET, String.valueOf(recordMetadata.offset()));
+ metaData.putValue(PARTITION, String.valueOf(recordMetadata.partition()));
+ metaData.putValue(TOPIC, recordMetadata.topic());
+ return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+ }
+
+ private TbMsg processException(TbContext ctx, TbMsg origMsg, Exception e) {
+ TbMsgMetaData metaData = origMsg.getMetaData().copy();
+ metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage());
+ return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java
new file mode 100644
index 0000000..2c16e56
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.thingsboard.rule.engine.kafka;
+
+import lombok.Data;
+import org.apache.kafka.common.serialization.StringSerializer;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Collections;
+import java.util.Map;
+
+@Data
+public class TbKafkaNodeConfiguration implements NodeConfiguration<TbKafkaNodeConfiguration> {
+
+ private String topicPattern;
+ private String bootstrapServers;
+ private int retries;
+ private int batchSize;
+ private int linger;
+ private int bufferMemory;
+ private String acks;
+ private String keySerializer;
+ private String valueSerializer;
+ private Map<String, String> otherProperties;
+
+ @Override
+ public TbKafkaNodeConfiguration defaultConfiguration() {
+ TbKafkaNodeConfiguration configuration = new TbKafkaNodeConfiguration();
+ configuration.setTopicPattern("my-topic");
+ configuration.setBootstrapServers("localhost:9092");
+ configuration.setRetries(0);
+ configuration.setBatchSize(16384);
+ configuration.setLinger(0);
+ configuration.setBufferMemory(33554432);
+ configuration.setAcks("-1");
+ configuration.setKeySerializer(StringSerializer.class.getName());
+ configuration.setValueSerializer(StringSerializer.class.getName());
+ configuration.setOtherProperties(Collections.emptyMap());
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java
new file mode 100644
index 0000000..7dece25
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.mail;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.io.IOException;
+
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+import static org.thingsboard.rule.engine.mail.TbSendEmailNode.SEND_EMAIL_TYPE;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.TRANSFORMATION,
+ name = "to email",
+ configClazz = TbMsgToEmailNodeConfiguration.class,
+ nodeDescription = "Transforms message to email message",
+ nodeDetails = "Transforms message to email message by populating email fields using values derived from message metadata. " +
+ "Set 'SEND_EMAIL' output message type.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbTransformationNodeToEmailConfig",
+ icon = "email"
+)
+public class TbMsgToEmailNode implements TbNode {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private TbMsgToEmailNodeConfiguration config;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbMsgToEmailNodeConfiguration.class);
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ try {
+ EmailPojo email = convert(msg);
+ TbMsg emailMsg = buildEmailMsg(ctx, msg, email);
+ ctx.tellNext(emailMsg, SUCCESS);
+ } catch (Exception ex) {
+ log.warn("Can not convert message to email " + ex.getMessage());
+ ctx.tellFailure(msg, ex);
+ }
+ }
+
+ private TbMsg buildEmailMsg(TbContext ctx, TbMsg msg, EmailPojo email) throws JsonProcessingException {
+ String emailJson = MAPPER.writeValueAsString(email);
+ return ctx.transformMsg(msg, SEND_EMAIL_TYPE, msg.getOriginator(), msg.getMetaData().copy(), emailJson);
+ }
+
+ private EmailPojo convert(TbMsg msg) throws IOException {
+ EmailPojo.EmailPojoBuilder builder = EmailPojo.builder();
+ builder.from(fromTemplate(this.config.getFromTemplate(), msg.getMetaData()));
+ builder.to(fromTemplate(this.config.getToTemplate(), msg.getMetaData()));
+ builder.cc(fromTemplate(this.config.getCcTemplate(), msg.getMetaData()));
+ builder.bcc(fromTemplate(this.config.getBccTemplate(), msg.getMetaData()));
+ builder.subject(fromTemplate(this.config.getSubjectTemplate(), msg.getMetaData()));
+ builder.body(fromTemplate(this.config.getBodyTemplate(), msg.getMetaData()));
+ return builder.build();
+ }
+
+ private String fromTemplate(String template, TbMsgMetaData metaData) {
+ if (!StringUtils.isEmpty(template)) {
+ return TbNodeUtils.processPattern(template, metaData);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java
new file mode 100644
index 0000000..f99259f
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.mail;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbMsgToEmailNodeConfiguration implements NodeConfiguration {
+
+ private String fromTemplate;
+ private String toTemplate;
+ private String ccTemplate;
+ private String bccTemplate;
+ private String subjectTemplate;
+ private String bodyTemplate;
+
+ @Override
+ public TbMsgToEmailNodeConfiguration defaultConfiguration() {
+ TbMsgToEmailNodeConfiguration configuration = new TbMsgToEmailNodeConfiguration();
+ configuration.fromTemplate = "info@testmail.org";
+ configuration.toTemplate = "${userEmail}";
+ configuration.subjectTemplate = "Device ${deviceType} temperature high";
+ configuration.bodyTemplate = "Device ${deviceName} has high temperature ${temp}";
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java
new file mode 100644
index 0000000..9862d02
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java
@@ -0,0 +1,146 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.mail;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.mail.javamail.JavaMailSenderImpl;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import javax.mail.internet.MimeMessage;
+import java.io.IOException;
+import java.util.Properties;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.EXTERNAL,
+ name = "send email",
+ configClazz = TbSendEmailNodeConfiguration.class,
+ nodeDescription = "Sends email message via SMTP server.",
+ nodeDetails = "Expects messages with <b>SEND_EMAIL</b> type. Node works only with messages that " +
+ " where created using <code>to Email</code> transformation Node, please connect this Node " +
+ "with <code>to Email</code> Node using <code>Successful</code> chain.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbActionNodeSendEmailConfig",
+ icon = "send"
+)
+public class TbSendEmailNode implements TbNode {
+
+ private static final String MAIL_PROP = "mail.";
+ static final String SEND_EMAIL_TYPE = "SEND_EMAIL";
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private TbSendEmailNodeConfiguration config;
+ private JavaMailSenderImpl mailSender;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ try {
+ this.config = TbNodeUtils.convert(configuration, TbSendEmailNodeConfiguration.class);
+ if (!this.config.isUseSystemSmtpSettings()) {
+ mailSender = createMailSender();
+ }
+ } catch (Exception e) {
+ throw new TbNodeException(e);
+ }
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ try {
+ validateType(msg.getType());
+ EmailPojo email = getEmail(msg);
+ withCallback(ctx.getMailExecutor().executeAsync(() -> {
+ sendEmail(ctx, email);
+ return null;
+ }),
+ ok -> ctx.tellNext(msg, SUCCESS),
+ fail -> ctx.tellFailure(msg, fail));
+ } catch (Exception ex) {
+ ctx.tellFailure(msg, ex);
+ }
+ }
+
+ private void sendEmail(TbContext ctx, EmailPojo email) throws Exception {
+ if (this.config.isUseSystemSmtpSettings()) {
+ ctx.getMailService().send(email.getFrom(), email.getTo(), email.getCc(),
+ email.getBcc(), email.getSubject(), email.getBody());
+ } else {
+ MimeMessage mailMsg = mailSender.createMimeMessage();
+ MimeMessageHelper helper = new MimeMessageHelper(mailMsg, "UTF-8");
+ helper.setFrom(email.getFrom());
+ helper.setTo(email.getTo().split("\\s*,\\s*"));
+ if (!StringUtils.isBlank(email.getCc())) {
+ helper.setCc(email.getCc().split("\\s*,\\s*"));
+ }
+ if (!StringUtils.isBlank(email.getBcc())) {
+ helper.setBcc(email.getBcc().split("\\s*,\\s*"));
+ }
+ helper.setSubject(email.getSubject());
+ helper.setText(email.getBody());
+ mailSender.send(helper.getMimeMessage());
+ }
+ }
+
+ private EmailPojo getEmail(TbMsg msg) throws IOException {
+ EmailPojo email = MAPPER.readValue(msg.getData(), EmailPojo.class);
+ if (StringUtils.isBlank(email.getTo())) {
+ throw new IllegalStateException("Email destination can not be blank [" + email.getTo() + "]");
+ }
+ return email;
+ }
+
+ private void validateType(String type) {
+ if (!SEND_EMAIL_TYPE.equals(type)) {
+ log.warn("Not expected msg type [{}] for SendEmail Node", type);
+ throw new IllegalStateException("Not expected msg type " + type + " for SendEmail Node");
+ }
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ private JavaMailSenderImpl createMailSender() {
+ JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
+ mailSender.setHost(this.config.getSmtpHost());
+ mailSender.setPort(this.config.getSmtpPort());
+ mailSender.setUsername(this.config.getUsername());
+ mailSender.setPassword(this.config.getPassword());
+ mailSender.setJavaMailProperties(createJavaMailProperties());
+ return mailSender;
+ }
+
+ private Properties createJavaMailProperties() {
+ Properties javaMailProperties = new Properties();
+ String protocol = this.config.getSmtpProtocol();
+ javaMailProperties.put("mail.transport.protocol", protocol);
+ javaMailProperties.put(MAIL_PROP + protocol + ".host", this.config.getSmtpHost());
+ javaMailProperties.put(MAIL_PROP + protocol + ".port", this.config.getSmtpPort()+"");
+ javaMailProperties.put(MAIL_PROP + protocol + ".timeout", this.config.getTimeout()+"");
+ javaMailProperties.put(MAIL_PROP + protocol + ".auth", String.valueOf(StringUtils.isNotEmpty(this.config.getUsername())));
+ javaMailProperties.put(MAIL_PROP + protocol + ".starttls.enable", Boolean.valueOf(this.config.isEnableTls()).toString());
+ return javaMailProperties;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNodeConfiguration.java
new file mode 100644
index 0000000..ea8a9a3
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNodeConfiguration.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.mail;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbSendEmailNodeConfiguration implements NodeConfiguration {
+
+ private boolean useSystemSmtpSettings;
+ private String smtpHost;
+ private int smtpPort;
+ private String username;
+ private String password;
+ private String smtpProtocol;
+ private int timeout;
+ private boolean enableTls;
+
+ @Override
+ public TbSendEmailNodeConfiguration defaultConfiguration() {
+ TbSendEmailNodeConfiguration configuration = new TbSendEmailNodeConfiguration();
+ configuration.setUseSystemSmtpSettings(true);
+ configuration.setSmtpHost("localhost");
+ configuration.setSmtpProtocol("smtp");
+ configuration.setSmtpPort(25);
+ configuration.setTimeout(10000);
+ configuration.setEnableTls(false);
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java
new file mode 100644
index 0000000..0f50eee
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.apache.commons.collections.CollectionUtils;
+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.EntityId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.List;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE;
+import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE;
+import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
+
+public abstract class TbAbstractGetAttributesNode<C extends TbGetAttributesNodeConfiguration, T extends EntityId> implements TbNode {
+
+ protected C config;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = loadGetAttributesNodeConfig(configuration);
+ }
+
+ protected abstract C loadGetAttributesNodeConfig(TbNodeConfiguration configuration) throws TbNodeException;
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
+ try {
+ withCallback(
+ findEntityAsync(ctx, msg.getOriginator()),
+ entityId -> safePutAttributes(ctx, msg, entityId),
+ t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
+ } catch (Throwable th) {
+ ctx.tellFailure(msg, th);
+ }
+ }
+
+ private void safePutAttributes(TbContext ctx, TbMsg msg, T entityId) {
+ if (entityId == null || entityId.isNullUid()) {
+ ctx.tellNext(msg, FAILURE);
+ return;
+ }
+ ListenableFuture<List<Void>> allFutures = Futures.allAsList(
+ putLatestTelemetry(ctx, entityId, msg, config.getLatestTsKeyNames()),
+ putAttrAsync(ctx, entityId, msg, CLIENT_SCOPE, config.getClientAttributeNames(), "cs_"),
+ putAttrAsync(ctx, entityId, msg, SHARED_SCOPE, config.getSharedAttributeNames(), "shared_"),
+ putAttrAsync(ctx, entityId, msg, SERVER_SCOPE, config.getServerAttributeNames(), "ss_")
+ );
+ withCallback(allFutures, i -> ctx.tellNext(msg, SUCCESS), t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
+ }
+
+ private ListenableFuture<Void> putAttrAsync(TbContext ctx, EntityId entityId, TbMsg msg, String scope, List<String> keys, String prefix) {
+ if (CollectionUtils.isEmpty(keys)) {
+ return Futures.immediateFuture(null);
+ }
+ ListenableFuture<List<AttributeKvEntry>> latest = ctx.getAttributesService().find(entityId, scope, keys);
+ return Futures.transform(latest, l -> {
+ l.forEach(r -> {
+ if (r.getValue() != null) {
+ msg.getMetaData().putValue(prefix + r.getKey(), r.getValueAsString());
+ } else {
+ throw new RuntimeException("[" + scope + "][" + r.getKey() + "] attribute value is not present in the DB!");
+ }
+ });
+ return null;
+ });
+ }
+
+ private ListenableFuture<Void> putLatestTelemetry(TbContext ctx, EntityId entityId, TbMsg msg, List<String> keys) {
+ if (CollectionUtils.isEmpty(keys)) {
+ return Futures.immediateFuture(null);
+ }
+ ListenableFuture<List<TsKvEntry>> latest = ctx.getTimeseriesService().findLatest(entityId, keys);
+ return Futures.transform(latest, l -> {
+ l.forEach(r -> {
+ if (r.getValue() != null) {
+ msg.getMetaData().putValue(r.getKey(), r.getValueAsString());
+ } else {
+ throw new RuntimeException("[" + r.getKey() + "] telemetry value is not present in the DB!");
+ }
+ });
+ return null;
+ });
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+
+ protected abstract ListenableFuture<T> findEntityAsync(TbContext ctx, EntityId originator);
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java
new file mode 100644
index 0000000..6f651f1
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+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.EntityId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE;
+
+@Slf4j
+public abstract class TbEntityGetAttrNode<T extends EntityId> implements TbNode {
+
+ private TbGetEntityAttrNodeConfiguration config;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbGetEntityAttrNodeConfiguration.class);
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ try {
+ withCallback(
+ findEntityAsync(ctx, msg.getOriginator()),
+ entityId -> safeGetAttributes(ctx, msg, entityId),
+ t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
+ } catch (Throwable th) {
+ ctx.tellFailure(msg, th);
+ }
+ }
+
+ private void safeGetAttributes(TbContext ctx, TbMsg msg, T entityId) {
+ if(entityId == null || entityId.isNullUid()) {
+ ctx.tellNext(msg, FAILURE);
+ return;
+ }
+
+ withCallback(config.isTelemetry() ? getLatestTelemetry(ctx, entityId) : getAttributesAsync(ctx, entityId),
+ attributes -> putAttributesAndTell(ctx, msg, attributes),
+ t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
+ }
+
+ private ListenableFuture<List<KvEntry>> getAttributesAsync(TbContext ctx, EntityId entityId) {
+ ListenableFuture<List<AttributeKvEntry>> latest = ctx.getAttributesService().find(entityId, SERVER_SCOPE, config.getAttrMapping().keySet());
+ return Futures.transform(latest, l ->
+ l.stream().map(i -> (KvEntry) i).collect(Collectors.toList()));
+ }
+
+ private ListenableFuture<List<KvEntry>> getLatestTelemetry(TbContext ctx, EntityId entityId) {
+ ListenableFuture<List<TsKvEntry>> latest = ctx.getTimeseriesService().findLatest(entityId, config.getAttrMapping().keySet());
+ return Futures.transform(latest, l ->
+ l.stream().map(i -> (KvEntry) i).collect(Collectors.toList()));
+ }
+
+
+ private void putAttributesAndTell(TbContext ctx, TbMsg msg, List<? extends KvEntry> attributes) {
+ attributes.forEach(r -> {
+ String attrName = config.getAttrMapping().get(r.getKey());
+ msg.getMetaData().putValue(attrName, r.getValueAsString());
+ });
+ ctx.tellNext(msg, SUCCESS);
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+
+ protected abstract ListenableFuture<T> findEntityAsync(TbContext ctx, EntityId originator);
+
+ public void setConfig(TbGetEntityAttrNodeConfiguration config) {
+ this.config = config;
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
new file mode 100644
index 0000000..4908b1c
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+/**
+ * Created by ashvayka on 19.01.18.
+ */
+@Slf4j
+@RuleNode(type = ComponentType.ENRICHMENT,
+ name = "originator attributes",
+ configClazz = TbGetAttributesNodeConfiguration.class,
+ nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata",
+ nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
+ "with specific prefix: <i>cs/shared/ss</i>. Latest telemetry value added into metadata without prefix. " +
+ "To access those attributes in other nodes this template can be used " +
+ "<code>metadata.cs_temperature</code> or <code>metadata.shared_limit</code> ",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbEnrichmentNodeOriginatorAttributesConfig")
+public class TbGetAttributesNode extends TbAbstractGetAttributesNode<TbGetAttributesNodeConfiguration, EntityId> {
+
+ @Override
+ protected TbGetAttributesNodeConfiguration loadGetAttributesNodeConfig(TbNodeConfiguration configuration) throws TbNodeException {
+ return TbNodeUtils.convert(configuration, TbGetAttributesNodeConfiguration.class);
+ }
+
+ @Override
+ protected ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator) {
+ return Futures.immediateFuture(originator);
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
new file mode 100644
index 0000000..6cd2247
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Created by ashvayka on 19.01.18.
+ */
+@Data
+public class TbGetAttributesNodeConfiguration implements NodeConfiguration<TbGetAttributesNodeConfiguration> {
+
+ private List<String> clientAttributeNames;
+ private List<String> sharedAttributeNames;
+ private List<String> serverAttributeNames;
+
+ private List<String> latestTsKeyNames;
+
+ @Override
+ public TbGetAttributesNodeConfiguration defaultConfiguration() {
+ TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
+ configuration.setClientAttributeNames(Collections.emptyList());
+ configuration.setSharedAttributeNames(Collections.emptyList());
+ configuration.setServerAttributeNames(Collections.emptyList());
+ configuration.setLatestTsKeyNames(Collections.emptyList());
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
new file mode 100644
index 0000000..60b6b84
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+@RuleNode(
+ type = ComponentType.ENRICHMENT,
+ name="customer attributes",
+ configClazz = TbGetEntityAttrNodeConfiguration.class,
+ nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata",
+ nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
+ "If Latest Telemetry enrichment configured, latest telemetry added into metadata. " +
+ "To access those attributes in other nodes this template can be used " +
+ "<code>metadata.temperature</code>.",
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbEnrichmentNodeCustomerAttributesConfig")
+public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> {
+
+ @Override
+ protected ListenableFuture<CustomerId> findEntityAsync(TbContext ctx, EntityId originator) {
+ return EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, originator);
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java
new file mode 100644
index 0000000..327d91a
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.rule.engine.util.EntitiesRelatedDeviceIdAsyncLoader;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+@Slf4j
+@RuleNode(type = ComponentType.ENRICHMENT,
+ name = "device attributes",
+ configClazz = TbGetDeviceAttrNodeConfiguration.class,
+ nodeDescription = "Add Originators Related Device Attributes and Latest Telemetry value into Message Metadata",
+ nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
+ "with specific prefix: <i>cs/shared/ss</i>. Latest telemetry value added into metadata without prefix. " +
+ "To access those attributes in other nodes this template can be used " +
+ "<code>metadata.cs_temperature</code> or <code>metadata.shared_limit</code> ",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbEnrichmentNodeDeviceAttributesConfig")
+public class TbGetDeviceAttrNode extends TbAbstractGetAttributesNode<TbGetDeviceAttrNodeConfiguration, DeviceId> {
+
+ @Override
+ protected TbGetDeviceAttrNodeConfiguration loadGetAttributesNodeConfig(TbNodeConfiguration configuration) throws TbNodeException {
+ return TbNodeUtils.convert(configuration, TbGetDeviceAttrNodeConfiguration.class);
+ }
+
+ @Override
+ protected ListenableFuture<DeviceId> findEntityAsync(TbContext ctx, EntityId originator) {
+ return EntitiesRelatedDeviceIdAsyncLoader.findDeviceAsync(ctx, originator, config.getDeviceRelationsQuery());
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java
new file mode 100644
index 0000000..4d8307c
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.data.DeviceRelationsQuery;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+
+import java.util.Collections;
+
+@Data
+public class TbGetDeviceAttrNodeConfiguration extends TbGetAttributesNodeConfiguration {
+
+ private DeviceRelationsQuery deviceRelationsQuery;
+
+ @Override
+ public TbGetDeviceAttrNodeConfiguration defaultConfiguration() {
+ TbGetDeviceAttrNodeConfiguration configuration = new TbGetDeviceAttrNodeConfiguration();
+ configuration.setClientAttributeNames(Collections.emptyList());
+ configuration.setSharedAttributeNames(Collections.emptyList());
+ configuration.setServerAttributeNames(Collections.emptyList());
+ configuration.setLatestTsKeyNames(Collections.emptyList());
+
+ DeviceRelationsQuery deviceRelationsQuery = new DeviceRelationsQuery();
+ deviceRelationsQuery.setDirection(EntitySearchDirection.FROM);
+ deviceRelationsQuery.setMaxLevel(1);
+ deviceRelationsQuery.setRelationType(EntityRelation.CONTAINS_TYPE);
+ deviceRelationsQuery.setDeviceTypes(Collections.singletonList("default"));
+
+ configuration.setDeviceRelationsQuery(deviceRelationsQuery);
+
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
new file mode 100644
index 0000000..66a1648
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader;
+
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+@RuleNode(
+ type = ComponentType.ENRICHMENT,
+ name="related attributes",
+ configClazz = TbGetRelatedAttrNodeConfiguration.class,
+ nodeDescription = "Add Originators Related Entity Attributes or Latest Telemetry into Message Metadata",
+ nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
+ "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " +
+ "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
+ "If Latest Telemetry enrichment configured, latest telemetry added into metadata. " +
+ "To access those attributes in other nodes this template can be used " +
+ "<code>metadata.temperature</code>.",
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbEnrichmentNodeRelatedAttributesConfig")
+
+public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
+
+ private TbGetRelatedAttrNodeConfiguration config;
+
+ @Override
+ public void init(TbContext context, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbGetRelatedAttrNodeConfiguration.class);
+ setConfig(config);
+ }
+
+ @Override
+ protected ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator) {
+ return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, config.getRelationsQuery());
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java
new file mode 100644
index 0000000..63186ca
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.data.RelationsQuery;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.EntityTypeFilter;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+@Data
+public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration {
+
+ private RelationsQuery relationsQuery;
+
+ @Override
+ public TbGetRelatedAttrNodeConfiguration defaultConfiguration() {
+ TbGetRelatedAttrNodeConfiguration configuration = new TbGetRelatedAttrNodeConfiguration();
+ Map<String, String> attrMapping = new HashMap<>();
+ attrMapping.putIfAbsent("temperature", "tempo");
+ configuration.setAttrMapping(attrMapping);
+ configuration.setTelemetry(false);
+
+ RelationsQuery relationsQuery = new RelationsQuery();
+ relationsQuery.setDirection(EntitySearchDirection.FROM);
+ relationsQuery.setMaxLevel(1);
+ EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
+ relationsQuery.setFilters(Collections.singletonList(entityTypeFilter));
+ configuration.setRelationsQuery(relationsQuery);
+
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java
new file mode 100644
index 0000000..1ae6c68
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.util.EntitiesTenantIdAsyncLoader;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.ENRICHMENT,
+ name="tenant attributes",
+ configClazz = TbGetEntityAttrNodeConfiguration.class,
+ nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata",
+ nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
+ "If Latest Telemetry enrichment configured, latest telemetry added into metadata. " +
+ "To access those attributes in other nodes this template can be used " +
+ "<code>metadata.temperature</code>.",
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbEnrichmentNodeTenantAttributesConfig")
+public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> {
+
+ @Override
+ protected ListenableFuture<TenantId> findEntityAsync(TbContext ctx, EntityId originator) {
+ return EntitiesTenantIdAsyncLoader.findEntityIdAsync(ctx, originator);
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java
new file mode 100644
index 0000000..a462839
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java
@@ -0,0 +1,154 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.thingsboard.rule.engine.mqtt.credentials;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import io.netty.handler.ssl.ClientAuth;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.mqtt.MqttClientConfig;
+import org.apache.commons.codec.binary.Base64;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openssl.PEMDecryptorProvider;
+import org.bouncycastle.openssl.PEMEncryptedKeyPair;
+import org.bouncycastle.openssl.PEMKeyPair;
+import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
+import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
+import org.springframework.util.StringUtils;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.ByteArrayInputStream;
+import java.security.*;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Optional;
+
+@Data
+@Slf4j
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class CertPemClientCredentials implements MqttClientCredentials {
+
+ private static final String TLS_VERSION = "TLSv1.2";
+
+ private String caCert;
+ private String cert;
+ private String privateKey;
+ private String password;
+
+ @Override
+ public Optional<SslContext> initSslContext() {
+ try {
+ Security.addProvider(new BouncyCastleProvider());
+ return Optional.of(SslContextBuilder.forClient()
+ .keyManager(createAndInitKeyManagerFactory())
+ .trustManager(createAndInitTrustManagerFactory())
+ .clientAuth(ClientAuth.REQUIRE)
+ .build());
+ } catch (Exception e) {
+ log.error("[{}:{}] Creating TLS factory failed!", caCert, cert, e);
+ throw new RuntimeException("Creating TLS factory failed!", e);
+ }
+ }
+
+ @Override
+ public void configure(MqttClientConfig config) {
+
+ }
+
+ private KeyManagerFactory createAndInitKeyManagerFactory() throws Exception {
+ X509Certificate certHolder = readCertFile(cert);
+ Object keyObject = readPrivateKeyFile(privateKey);
+ char[] passwordCharArray = "".toCharArray();
+ if (!StringUtils.isEmpty(password)) {
+ passwordCharArray = password.toCharArray();
+ }
+
+ JcaPEMKeyConverter keyConverter = new JcaPEMKeyConverter().setProvider("BC");
+
+ PrivateKey privateKey;
+ if (keyObject instanceof PEMEncryptedKeyPair) {
+ PEMDecryptorProvider provider = new JcePEMDecryptorProviderBuilder().build(passwordCharArray);
+ KeyPair key = keyConverter.getKeyPair(((PEMEncryptedKeyPair) keyObject).decryptKeyPair(provider));
+ privateKey = key.getPrivate();
+ } else if (keyObject instanceof PEMKeyPair) {
+ KeyPair key = keyConverter.getKeyPair((PEMKeyPair) keyObject);
+ privateKey = key.getPrivate();
+ } else if (keyObject instanceof PrivateKey) {
+ privateKey = (PrivateKey)keyObject;
+ } else {
+ throw new RuntimeException("Unable to get private key from object: " + keyObject.getClass());
+ }
+
+ KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ clientKeyStore.load(null, null);
+ clientKeyStore.setCertificateEntry("cert", certHolder);
+ clientKeyStore.setKeyEntry("private-key",
+ privateKey,
+ passwordCharArray,
+ new Certificate[]{certHolder});
+
+ KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ keyManagerFactory.init(clientKeyStore, passwordCharArray);
+ return keyManagerFactory;
+ }
+
+ private TrustManagerFactory createAndInitTrustManagerFactory() throws Exception {
+ X509Certificate caCertHolder;
+ caCertHolder = readCertFile(caCert);
+
+ KeyStore caKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ caKeyStore.load(null, null);
+ caKeyStore.setCertificateEntry("caCert-cert", caCertHolder);
+
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(caKeyStore);
+ return trustManagerFactory;
+ }
+
+ private X509Certificate readCertFile(String fileContent) throws Exception {
+ X509Certificate certificate = null;
+ if (fileContent != null && !fileContent.trim().isEmpty()) {
+ fileContent = fileContent.replace("-----BEGIN CERTIFICATE-----", "")
+ .replace("-----END CERTIFICATE-----", "")
+ .replaceAll("\\s", "");
+ byte[] decoded = Base64.decodeBase64(fileContent);
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ certificate = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(decoded));
+ }
+ return certificate;
+ }
+
+ private PrivateKey readPrivateKeyFile(String fileContent) throws Exception {
+ RSAPrivateKey privateKey = null;
+ if (fileContent != null && !fileContent.isEmpty()) {
+ fileContent = fileContent.replaceAll(".*BEGIN.*PRIVATE KEY.*", "")
+ .replaceAll(".*END.*PRIVATE KEY.*", "")
+ .replaceAll("\\s", "");
+ byte[] decoded = Base64.decodeBase64(fileContent);
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ privateKey = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(decoded));
+ }
+ return privateKey;
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
new file mode 100644
index 0000000..dc16a1e
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
@@ -0,0 +1,144 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.thingsboard.rule.engine.mqtt;
+
+import io.netty.buffer.Unpooled;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.util.concurrent.Future;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.mqtt.MqttClient;
+import org.thingsboard.mqtt.MqttClientConfig;
+import org.thingsboard.mqtt.MqttConnectResult;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.net.ssl.SSLException;
+import java.nio.charset.Charset;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.EXTERNAL,
+ name = "mqtt",
+ configClazz = TbMqttNodeConfiguration.class,
+ nodeDescription = "Publish messages to the MQTT broker",
+ nodeDetails = "Will publish message payload to the MQTT broker with QoS <b>AT_LEAST_ONCE</b>.",
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbActionNodeMqttConfig",
+ icon = "call_split"
+)
+public class TbMqttNode implements TbNode {
+
+ private static final Charset UTF8 = Charset.forName("UTF-8");
+
+ private static final String ERROR = "error";
+
+ private TbMqttNodeConfiguration config;
+
+ private EventLoopGroup eventLoopGroup;
+ private MqttClient mqttClient;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ try {
+ this.config = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class);
+ this.eventLoopGroup = new NioEventLoopGroup();
+ this.mqttClient = initClient();
+ } catch (Exception e) {
+ throw new TbNodeException(e);
+ }
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
+ String topic = TbNodeUtils.processPattern(this.config.getTopicPattern(), msg.getMetaData());
+ this.mqttClient.publish(topic, Unpooled.wrappedBuffer(msg.getData().getBytes(UTF8)), MqttQoS.AT_LEAST_ONCE)
+ .addListener(future -> {
+ if (future.isSuccess()) {
+ ctx.tellNext(msg, TbRelationTypes.SUCCESS);
+ } else {
+ TbMsg next = processException(ctx, msg, future.cause());
+ ctx.tellFailure(next, future.cause());
+ }
+ }
+ );
+ }
+
+ private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) {
+ TbMsgMetaData metaData = origMsg.getMetaData().copy();
+ metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage());
+ return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+ }
+
+ @Override
+ public void destroy() {
+ if (this.mqttClient != null) {
+ this.mqttClient.disconnect();
+ }
+ if (this.eventLoopGroup != null) {
+ this.eventLoopGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS);
+ }
+ }
+
+ private MqttClient initClient() throws Exception {
+ Optional<SslContext> sslContextOpt = initSslContext();
+ MqttClientConfig config = sslContextOpt.isPresent() ? new MqttClientConfig(sslContextOpt.get()) : new MqttClientConfig();
+ if (!StringUtils.isEmpty(this.config.getClientId())) {
+ config.setClientId(this.config.getClientId());
+ }
+ this.config.getCredentials().configure(config);
+ MqttClient client = MqttClient.create(config);
+ client.setEventLoop(this.eventLoopGroup);
+ Future<MqttConnectResult> connectFuture = client.connect(this.config.getHost(), this.config.getPort());
+ MqttConnectResult result;
+ try {
+ result = connectFuture.get(this.config.getConnectTimeoutSec(), TimeUnit.SECONDS);
+ } catch (TimeoutException ex) {
+ connectFuture.cancel(true);
+ client.disconnect();
+ String hostPort = this.config.getHost() + ":" + this.config.getPort();
+ throw new RuntimeException(String.format("Failed to connect to MQTT broker at %s.", hostPort));
+ }
+ if (!result.isSuccess()) {
+ connectFuture.cancel(true);
+ client.disconnect();
+ String hostPort = this.config.getHost() + ":" + this.config.getPort();
+ throw new RuntimeException(String.format("Failed to connect to MQTT broker at %s. Result code is: %s", hostPort, result.getReturnCode()));
+ }
+ return client;
+ }
+
+ private Optional<SslContext> initSslContext() throws SSLException {
+ Optional<SslContext> result = this.config.getCredentials().initSslContext();
+ if (this.config.isSsl() && !result.isPresent()) {
+ result = Optional.of(SslContextBuilder.forClient().build());
+ }
+ return result;
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java
new file mode 100644
index 0000000..f63e201
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.thingsboard.rule.engine.mqtt;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.rule.engine.mqtt.credentials.AnonymousCredentials;
+import org.thingsboard.rule.engine.mqtt.credentials.MqttClientCredentials;
+
+@Data
+public class TbMqttNodeConfiguration implements NodeConfiguration<TbMqttNodeConfiguration> {
+
+ private String topicPattern;
+ private String host;
+ private int port;
+ private int connectTimeoutSec;
+ private String clientId;
+
+ private boolean ssl;
+ private MqttClientCredentials credentials;
+
+ @Override
+ public TbMqttNodeConfiguration defaultConfiguration() {
+ TbMqttNodeConfiguration configuration = new TbMqttNodeConfiguration();
+ configuration.setTopicPattern("my-topic");
+ configuration.setHost("localhost");
+ configuration.setPort(1883);
+ configuration.setConnectTimeoutSec(10);
+ configuration.setSsl(false);
+ configuration.setCredentials(new AnonymousCredentials());
+ return configuration;
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java
new file mode 100644
index 0000000..839eec7
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java
@@ -0,0 +1,148 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.thingsboard.rule.engine.rabbitmq;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.rabbitmq.client.*;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.nio.charset.Charset;
+import java.util.concurrent.ExecutionException;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.EXTERNAL,
+ name = "rabbitmq",
+ configClazz = TbRabbitMqNodeConfiguration.class,
+ nodeDescription = "Publish messages to the RabbitMQ",
+ nodeDetails = "Will publish message payload to RabbitMQ queue.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbActionNodeRabbitMqConfig",
+ iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZlcnNpb249IjEuMSIgeT0iMHB4IiB4PSIwcHgiIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxwYXRoIHN0cm9rZS13aWR0aD0iLjg0OTU2IiBkPSJtODYwLjQ3IDQxNi4zMmgtMjYyLjAxYy0xMi45MTMgMC0yMy42MTgtMTAuNzA0LTIzLjYxOC0yMy42MTh2LTI3Mi43MWMwLTIwLjMwNS0xNi4yMjctMzYuMjc2LTM2LjI3Ni0zNi4yNzZoLTkzLjc5MmMtMjAuMzA1IDAtMzYuMjc2IDE2LjIyNy0zNi4yNzYgMzYuMjc2djI3MC44NGMtMC4yNTQ4NyAxNC4xMDMtMTEuNDY5IDI1LjU3Mi0yNS43NDIgMjUuNTcybC04NS42MzYgMC42Nzk2NWMtMTQuMTAzIDAtMjUuNTcyLTExLjQ2OS0yNS41NzItMjUuNTcybDAuNjc5NjUtMjcxLjUyYzAtMjAuMzA1LTE2LjIyNy0zNi4yNzYtMzYuMjc2LTM2LjI3NmgtOTMuNTM3Yy0yMC4zMDUgMC0zNi4yNzYgMTYuMjI3LTM2LjI3NiAzNi4yNzZ2NzYzLjg0YzAgMTguMDk2IDE0Ljc4MiAzMi40NTMgMzIuNDUzIDMyLjQ1M2g3MjIuODFjMTguMDk2IDAgMzIuNDUzLTE0Ljc4MiAzMi40NTMtMzIuNDUzdi00MzUuMzFjLTEuMTg5NC0xOC4xODEtMTUuMjkyLTMyLjE5OC0zMy4zODgtMzIuMTk4em0tMTIyLjY4IDI4Ny4wN2MwIDIzLjYxOC0xOC44NiA0Mi40NzgtNDIuNDc4IDQyLjQ3OGgtNzMuOTk3Yy0yMy42MTggMC00Mi40NzgtMTguODYtNDIuNDc4LTQyLjQ3OHYtNzQuMjUyYzAtMjMuNjE4IDE4Ljg2LTQyLjQ3OCA0Mi40NzgtNDIuNDc4aDczLjk5N2MyMy42MTggMCA0Mi40NzggMTguODYgNDIuNDc4IDQyLjQ3OHoiLz48L3N2Zz4="
+)
+public class TbRabbitMqNode implements TbNode {
+
+ private static final Charset UTF8 = Charset.forName("UTF-8");
+
+ private static final String ERROR = "error";
+
+ private TbRabbitMqNodeConfiguration config;
+
+ private Connection connection;
+ private Channel channel;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbRabbitMqNodeConfiguration.class);
+ ConnectionFactory factory = new ConnectionFactory();
+ factory.setHost(this.config.getHost());
+ factory.setPort(this.config.getPort());
+ factory.setVirtualHost(this.config.getVirtualHost());
+ factory.setUsername(this.config.getUsername());
+ factory.setPassword(this.config.getPassword());
+ factory.setAutomaticRecoveryEnabled(this.config.isAutomaticRecoveryEnabled());
+ factory.setConnectionTimeout(this.config.getConnectionTimeout());
+ factory.setHandshakeTimeout(this.config.getHandshakeTimeout());
+ this.config.getClientProperties().forEach((k,v) -> factory.getClientProperties().put(k,v));
+ try {
+ this.connection = factory.newConnection();
+ this.channel = this.connection.createChannel();
+ } catch (Exception e) {
+ throw new TbNodeException(e);
+ }
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
+ withCallback(publishMessageAsync(ctx, msg),
+ m -> ctx.tellNext(m, TbRelationTypes.SUCCESS),
+ t -> {
+ TbMsg next = processException(ctx, msg, t);
+ ctx.tellFailure(next, t);
+ });
+ }
+
+ private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {
+ return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(ctx, msg));
+ }
+
+ private TbMsg publishMessage(TbContext ctx, TbMsg msg) throws Exception {
+ String exchangeName = "";
+ if (!StringUtils.isEmpty(this.config.getExchangeNamePattern())) {
+ exchangeName = TbNodeUtils.processPattern(this.config.getExchangeNamePattern(), msg.getMetaData());
+ }
+ String routingKey = "";
+ if (!StringUtils.isEmpty(this.config.getRoutingKeyPattern())) {
+ routingKey = TbNodeUtils.processPattern(this.config.getRoutingKeyPattern(), msg.getMetaData());
+ }
+ AMQP.BasicProperties properties = null;
+ if (!StringUtils.isEmpty(this.config.getMessageProperties())) {
+ properties = convert(this.config.getMessageProperties());
+ }
+ channel.basicPublish(
+ exchangeName,
+ routingKey,
+ properties,
+ msg.getData().getBytes(UTF8));
+ return msg;
+ }
+
+ private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) {
+ TbMsgMetaData metaData = origMsg.getMetaData().copy();
+ metaData.putValue(ERROR, t.getClass() + ": " + t.getMessage());
+ return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+ }
+
+ @Override
+ public void destroy() {
+ if (this.connection != null) {
+ try {
+ this.connection.close();
+ } catch (Exception e) {
+ log.error("Failed to close connection during destroy()", e);
+ }
+ }
+ }
+
+ private static AMQP.BasicProperties convert(String name) throws TbNodeException {
+ switch (name) {
+ case "BASIC":
+ return MessageProperties.BASIC;
+ case "TEXT_PLAIN":
+ return MessageProperties.TEXT_PLAIN;
+ case "MINIMAL_BASIC":
+ return MessageProperties.MINIMAL_BASIC;
+ case "MINIMAL_PERSISTENT_BASIC":
+ return MessageProperties.MINIMAL_PERSISTENT_BASIC;
+ case "PERSISTENT_BASIC":
+ return MessageProperties.PERSISTENT_BASIC;
+ case "PERSISTENT_TEXT_PLAIN":
+ return MessageProperties.PERSISTENT_TEXT_PLAIN;
+ default:
+ throw new TbNodeException("Message Properties: '" + name + "' is undefined!");
+ }
+ }
+}
+
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNodeConfiguration.java
new file mode 100644
index 0000000..1a0a00a
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNodeConfiguration.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.thingsboard.rule.engine.rabbitmq;
+
+import com.rabbitmq.client.ConnectionFactory;
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Collections;
+import java.util.Map;
+
+@Data
+public class TbRabbitMqNodeConfiguration implements NodeConfiguration<TbRabbitMqNodeConfiguration> {
+
+ private String exchangeNamePattern;
+ private String routingKeyPattern;
+ private String messageProperties;
+ private String host;
+ private int port;
+ private String virtualHost;
+ private String username;
+ private String password;
+ private boolean automaticRecoveryEnabled;
+ private int connectionTimeout;
+ private int handshakeTimeout;
+ private Map<String, String> clientProperties;
+
+ @Override
+ public TbRabbitMqNodeConfiguration defaultConfiguration() {
+ TbRabbitMqNodeConfiguration configuration = new TbRabbitMqNodeConfiguration();
+ configuration.setExchangeNamePattern("");
+ configuration.setRoutingKeyPattern("");
+ configuration.setMessageProperties(null);
+ configuration.setHost(ConnectionFactory.DEFAULT_HOST);
+ configuration.setPort(ConnectionFactory.DEFAULT_AMQP_PORT);
+ configuration.setVirtualHost(ConnectionFactory.DEFAULT_VHOST);
+ configuration.setUsername(ConnectionFactory.DEFAULT_USER);
+ configuration.setPassword(ConnectionFactory.DEFAULT_PASS);
+ configuration.setAutomaticRecoveryEnabled(false);
+ configuration.setConnectionTimeout(ConnectionFactory.DEFAULT_CONNECTION_TIMEOUT);
+ configuration.setHandshakeTimeout(ConnectionFactory.DEFAULT_HANDSHAKE_TIMEOUT);
+ configuration.setClientProperties(Collections.emptyMap());
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
new file mode 100644
index 0000000..f7f2d2d
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
@@ -0,0 +1,158 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.rest;
+
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.handler.ssl.SslContextBuilder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.Netty4ClientHttpRequestFactory;
+import org.springframework.util.concurrent.ListenableFuture;
+import org.springframework.util.concurrent.ListenableFutureCallback;
+import org.springframework.web.client.AsyncRestTemplate;
+import org.springframework.web.client.HttpClientErrorException;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.net.ssl.SSLException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.EXTERNAL,
+ name = "rest api call",
+ configClazz = TbRestApiCallNodeConfiguration.class,
+ nodeDescription = "Invoke REST API calls to external REST server",
+ nodeDetails = "Will invoke REST API call <code>GET | POST | PUT | DELETE</code> to external REST server. " +
+ "Message payload added into Request body. Configured attributes can be added into Headers from Message Metadata." +
+ " Outbound message will contain response fields " +
+ "(<code>status</code>, <code>statusCode</code>, <code>statusReason</code> and response <code>headers</code>) in the Message Metadata." +
+ " Response body saved in outbound Message payload. " +
+ "For example <b>statusCode</b> field can be accessed with <code>metadata.statusCode</code>.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbActionNodeRestApiCallConfig",
+ iconUrl = "data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB2ZXJzaW9uPSIxLjEiIHk9IjBweCIgeD0iMHB4Ij48ZyB0cmFuc2Zvcm09Im1hdHJpeCguOTQ5NzUgMCAwIC45NDk3NSAxNy4xMiAyNi40OTIpIj48cGF0aCBkPSJtMTY5LjExIDEwOC41NGMtOS45MDY2IDAuMDczNC0xOS4wMTQgNi41NzI0LTIyLjAxNCAxNi40NjlsLTY5Ljk5MyAyMzEuMDhjLTMuNjkwNCAxMi4xODEgMy4yODkyIDI1LjIyIDE1LjQ2OSAyOC45MSAyLjIyNTkgMC42NzQ4MSA0LjQ5NjkgMSA2LjcyODUgMSA5Ljk3MjEgMCAxOS4xNjUtNi41MTUzIDIyLjE4Mi0xNi40NjdhNi41MjI0IDYuNTIyNCAwIDAgMCAwLjAwMiAtMC4wMDJsNjkuOTktMjMxLjA3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMCAtMC4wMDJjMy42ODU1LTEyLjE4MS0zLjI4Ny0yNS4yMjUtMTUuNDcxLTI4LjkxMi0yLjI4MjUtMC42OTE0NS00LjYxMTYtMS4wMTY5LTYuODk4NC0xem04NC45ODggMGMtOS45MDQ4IDAuMDczNC0xOS4wMTggNi41Njc1LTIyLjAxOCAxNi40NjlsLTY5Ljk4NiAyMzEuMDhjLTMuNjg5OCAxMi4xNzkgMy4yODUzIDI1LjIxNyAxNS40NjUgMjguOTA4IDIuMjI5NyAwLjY3NjQ3IDQuNTAwOCAxLjAwMiA2LjczMjQgMS4wMDIgOS45NzIxIDAgMTkuMTY1LTYuNTE1MyAyMi4xODItMTYuNDY3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMC4wMDIgLTAuMDAybDY5Ljk4OC0yMzEuMDdjMy42OTA4LTEyLjE4MS0zLjI4NTItMjUuMjIzLTE1LjQ2Ny0yOC45MTItMi4yODE0LTAuNjkyMzEtNC42MTA4LTEuMDE4OS02Ljg5ODQtMS4wMDJ6bS0yMTcuMjkgNDIuMjNjLTEyLjcyOS0wLjAwMDg3LTIzLjE4OCAxMC40NTYtMjMuMTg4IDIzLjE4NiAwLjAwMSAxMi43MjggMTAuNDU5IDIzLjE4NiAyMy4xODggMjMuMTg2IDEyLjcyNy0wLjAwMSAyMy4xODMtMTAuNDU5IDIzLjE4NC0yMy4xODYgMC4wMDA4NzYtMTIuNzI4LTEwLjQ1Ni0yMy4xODUtMjMuMTg0LTIzLjE4NnptMCAxNDYuNjRjLTEyLjcyNy0wLjAwMDg3LTIzLjE4NiAxMC40NTUtMjMuMTg4IDIzLjE4NC0wLjAwMDg3MyAxMi43MjkgMTAuNDU4IDIzLjE4OCAyMy4xODggMjMuMTg4IDEyLjcyOC0wLjAwMSAyMy4xODQtMTAuNDYgMjMuMTg0LTIzLjE4OC0wLjAwMS0xMi43MjYtMTAuNDU3LTIzLjE4My0yMy4xODQtMjMuMTg0em0yNzAuNzkgNDIuMjExYy0xMi43MjcgMC0yMy4xODQgMTAuNDU3LTIzLjE4NCAyMy4xODRzMTAuNDU1IDIzLjE4OCAyMy4xODQgMjMuMTg4aDE1NC45OGMxMi43MjkgMCAyMy4xODYtMTAuNDYgMjMuMTg2LTIzLjE4OCAwLjAwMS0xMi43MjgtMTAuNDU4LTIzLjE4NC0yMy4xODYtMjMuMTg0eiIgdHJhbnNmb3JtPSJtYXRyaXgoMS4wMzc2IDAgMCAxLjAzNzYgLTcuNTY3NiAtMTQuOTI1KSIgc3Ryb2tlLXdpZHRoPSIxLjI2OTMiLz48L2c+PC9zdmc+"
+)
+public class TbRestApiCallNode implements TbNode {
+
+ private static final String STATUS = "status";
+ private static final String STATUS_CODE = "statusCode";
+ private static final String STATUS_REASON = "statusReason";
+ private static final String ERROR = "error";
+ private static final String ERROR_BODY = "error_body";
+
+ private TbRestApiCallNodeConfiguration config;
+
+ private EventLoopGroup eventLoopGroup;
+ private AsyncRestTemplate httpClient;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ try {
+ this.config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class);
+ this.eventLoopGroup = new NioEventLoopGroup();
+ Netty4ClientHttpRequestFactory nettyFactory = new Netty4ClientHttpRequestFactory(this.eventLoopGroup);
+ nettyFactory.setSslContext(SslContextBuilder.forClient().build());
+ httpClient = new AsyncRestTemplate(nettyFactory);
+ } catch (SSLException e) {
+ throw new TbNodeException(e);
+ }
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
+ String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg.getMetaData());
+ HttpHeaders headers = prepareHeaders(msg.getMetaData());
+ HttpMethod method = HttpMethod.valueOf(config.getRequestMethod());
+ HttpEntity<String> entity = new HttpEntity<>(msg.getData(), headers);
+
+ ListenableFuture<ResponseEntity<String>> future = httpClient.exchange(
+ endpointUrl, method, entity, String.class);
+
+ future.addCallback(new ListenableFutureCallback<ResponseEntity<String>>() {
+ @Override
+ public void onFailure(Throwable throwable) {
+ TbMsg next = processException(ctx, msg, throwable);
+ ctx.tellFailure(next, throwable);
+ }
+
+ @Override
+ public void onSuccess(ResponseEntity<String> responseEntity) {
+ if (responseEntity.getStatusCode().is2xxSuccessful()) {
+ TbMsg next = processResponse(ctx, msg, responseEntity);
+ ctx.tellNext(next, TbRelationTypes.SUCCESS);
+ } else {
+ TbMsg next = processFailureResponse(ctx, msg, responseEntity);
+ ctx.tellNext(next, TbRelationTypes.FAILURE);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void destroy() {
+ if (this.eventLoopGroup != null) {
+ this.eventLoopGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS);
+ }
+ }
+
+ private TbMsg processResponse(TbContext ctx, TbMsg origMsg, ResponseEntity<String> response) {
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue(STATUS, response.getStatusCode().name());
+ metaData.putValue(STATUS_CODE, response.getStatusCode().value()+"");
+ metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase());
+ response.getHeaders().toSingleValueMap().forEach((k,v) -> metaData.putValue(k,v) );
+ return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, response.getBody());
+ }
+
+ private TbMsg processFailureResponse(TbContext ctx, TbMsg origMsg, ResponseEntity<String> response) {
+ TbMsgMetaData metaData = origMsg.getMetaData().copy();
+ metaData.putValue(STATUS, response.getStatusCode().name());
+ metaData.putValue(STATUS_CODE, response.getStatusCode().value()+"");
+ metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase());
+ metaData.putValue(ERROR_BODY, response.getBody());
+ return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+ }
+
+ private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) {
+ TbMsgMetaData metaData = origMsg.getMetaData().copy();
+ metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage());
+ if (e instanceof HttpClientErrorException) {
+ HttpClientErrorException httpClientErrorException = (HttpClientErrorException)e;
+ metaData.putValue(STATUS, httpClientErrorException.getStatusText());
+ metaData.putValue(STATUS_CODE, httpClientErrorException.getRawStatusCode()+"");
+ metaData.putValue(ERROR_BODY, httpClientErrorException.getResponseBodyAsString());
+ }
+ return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+ }
+
+ private HttpHeaders prepareHeaders(TbMsgMetaData metaData) {
+ HttpHeaders headers = new HttpHeaders();
+ config.getHeaders().forEach((k,v) -> {
+ headers.add(TbNodeUtils.processPattern(k, metaData), TbNodeUtils.processPattern(v, metaData));
+ });
+ return headers;
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java
new file mode 100644
index 0000000..812eb77
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.rest;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Collections;
+import java.util.Map;
+
+@Data
+public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestApiCallNodeConfiguration> {
+
+ private String restEndpointUrlPattern;
+ private String requestMethod;
+ private Map<String, String> headers;
+
+ @Override
+ public TbRestApiCallNodeConfiguration defaultConfiguration() {
+ TbRestApiCallNodeConfiguration configuration = new TbRestApiCallNodeConfiguration();
+ configuration.setRestEndpointUrlPattern("http://localhost/api");
+ configuration.setRequestMethod("POST");
+ configuration.setHeaders(Collections.emptyMap());
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java
new file mode 100644
index 0000000..288c2ea
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.rpc;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.RuleNode;
+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.EntityType;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.ACTION,
+ name = "rpc call reply",
+ configClazz = TbSendRpcReplyNodeConfiguration.class,
+ nodeDescription = "Sends one-way RPC call to device",
+ nodeDetails = "Expects messages with any message type. Will forward message body to the device.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbActionNodeRpcReplyConfig",
+ icon = "call_merge"
+)
+public class TbSendRPCReplyNode implements TbNode {
+
+ private TbSendRpcReplyNodeConfiguration config;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbSendRpcReplyNodeConfiguration.class);
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ String requestIdStr = msg.getMetaData().getValue(config.getRequestIdMetaDataAttribute());
+ if (msg.getOriginator().getEntityType() != EntityType.DEVICE) {
+ ctx.tellFailure(msg, new RuntimeException("Message originator is not a device entity!"));
+ } else if (StringUtils.isEmpty(requestIdStr)) {
+ ctx.tellFailure(msg, new RuntimeException("Request id is not present in the metadata!"));
+ } else if (StringUtils.isEmpty(msg.getData())) {
+ ctx.tellFailure(msg, new RuntimeException("Request body is empty!"));
+ } else {
+ ctx.getRpcService().sendRpcReply(new DeviceId(msg.getOriginator().getId()), Integer.parseInt(requestIdStr), msg.getData());
+ }
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRpcReplyNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRpcReplyNodeConfiguration.java
new file mode 100644
index 0000000..402a33b
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRpcReplyNodeConfiguration.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.rpc;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.data.DataConstants;
+
+@Data
+public class TbSendRpcReplyNodeConfiguration implements NodeConfiguration<TbSendRpcReplyNodeConfiguration> {
+
+ private String requestIdMetaDataAttribute;
+
+ @Override
+ public TbSendRpcReplyNodeConfiguration defaultConfiguration() {
+ TbSendRpcReplyNodeConfiguration configuration = new TbSendRpcReplyNodeConfiguration();
+ configuration.setRequestIdMetaDataAttribute("requestId");
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java
new file mode 100644
index 0000000..c1165ca
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java
@@ -0,0 +1,103 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.rpc;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest;
+import org.thingsboard.rule.engine.api.RuleNode;
+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.rule.engine.api.TbRelationTypes;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.ACTION,
+ name = "rpc call request",
+ configClazz = TbSendRpcRequestNodeConfiguration.class,
+ nodeDescription = "Sends two-way RPC call to device",
+ nodeDetails = "Expects messages with \"method\" and \"params\". Will forward response from device to next nodes.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbActionNodeRpcRequestConfig",
+ icon = "call_made"
+)
+public class TbSendRPCRequestNode implements TbNode {
+
+ private Random random = new Random();
+ private Gson gson = new Gson();
+ private JsonParser jsonParser = new JsonParser();
+ private TbSendRpcRequestNodeConfiguration config;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbSendRpcRequestNodeConfiguration.class);
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ JsonObject json = jsonParser.parse(msg.getData()).getAsJsonObject();
+
+ if (msg.getOriginator().getEntityType() != EntityType.DEVICE) {
+ ctx.tellFailure(msg, new RuntimeException("Message originator is not a device entity!"));
+ } else if (!json.has("method")) {
+ ctx.tellFailure(msg, new RuntimeException("Method is not present in the message!"));
+ } else if (!json.has("params")) {
+ ctx.tellFailure(msg, new RuntimeException("Params are not present in the message!"));
+ } else {
+ int requestId = json.has("requestId") ? json.get("requestId").getAsInt() : random.nextInt();
+ RuleEngineDeviceRpcRequest request = RuleEngineDeviceRpcRequest.builder()
+ .method(json.get("method").getAsString())
+ .body(gson.toJson(json.get("params")))
+ .deviceId(new DeviceId(msg.getOriginator().getId()))
+ .requestId(requestId)
+ .timeout(TimeUnit.SECONDS.toMillis(config.getTimeoutInSeconds()))
+ .build();
+
+ ctx.getRpcService().sendRpcRequest(request, ruleEngineDeviceRpcResponse -> {
+ if (!ruleEngineDeviceRpcResponse.getError().isPresent()) {
+ TbMsg next = ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), ruleEngineDeviceRpcResponse.getResponse().get());
+ ctx.tellNext(next, TbRelationTypes.SUCCESS);
+ } else {
+ TbMsg next = ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), wrap("error", ruleEngineDeviceRpcResponse.getError().get().name()));
+ ctx.tellFailure(next, new RuntimeException(ruleEngineDeviceRpcResponse.getError().get().name()));
+ }
+ });
+ }
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ private String wrap(String name, String body) {
+ JsonObject json = new JsonObject();
+ json.addProperty(name, body);
+ return gson.toJson(json);
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java
new file mode 100644
index 0000000..4f82884
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.telemetry;
+
+import com.google.gson.JsonParser;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.RuleNode;
+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.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+
+import java.util.ArrayList;
+import java.util.Set;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.ACTION,
+ name = "save attributes",
+ configClazz = TbMsgAttributesNodeConfiguration.class,
+ nodeDescription = "Saves attributes data",
+ nodeDetails = "Saves entity attributes based on configurable scope parameter. Expects messages with 'POST_ATTRIBUTES_REQUEST' message type",
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbActionNodeAttributesConfig",
+ icon = "file_upload"
+)
+public class TbMsgAttributesNode implements TbNode {
+
+ private TbMsgAttributesNodeConfiguration config;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbMsgAttributesNodeConfiguration.class);
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ if (!msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name())) {
+ ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType()));
+ return;
+ }
+
+ String src = msg.getData();
+ Set<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(new JsonParser().parse(src)).getAttributes();
+ ctx.getTelemetryService().saveAndNotify(msg.getOriginator(), config.getScope(), new ArrayList<>(attributes), new TelemetryNodeCallback(ctx, msg));
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java
new file mode 100644
index 0000000..c633917
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.telemetry;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.data.DataConstants;
+
+@Data
+public class TbMsgAttributesNodeConfiguration implements NodeConfiguration<TbMsgAttributesNodeConfiguration> {
+
+ private String scope;
+
+ @Override
+ public TbMsgAttributesNodeConfiguration defaultConfiguration() {
+ TbMsgAttributesNodeConfiguration configuration = new TbMsgAttributesNodeConfiguration();
+ configuration.setScope(DataConstants.SERVER_SCOPE);
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java
new file mode 100644
index 0000000..efdf4af
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.telemetry;
+
+import com.google.gson.JsonParser;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.RuleNode;
+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.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.ACTION,
+ name = "save timeseries",
+ configClazz = TbMsgTimeseriesNodeConfiguration.class,
+ nodeDescription = "Saves timeseries data",
+ nodeDetails = "Saves timeseries telemetry data based on configurable TTL parameter. Expects messages with 'POST_TELEMETRY_REQUEST' message type",
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbActionNodeTimeseriesConfig",
+ icon = "file_upload"
+)
+public class TbMsgTimeseriesNode implements TbNode {
+
+ private TbMsgTimeseriesNodeConfiguration config;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbMsgTimeseriesNodeConfiguration.class);
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ if (!msg.getType().equals(SessionMsgType.POST_TELEMETRY_REQUEST.name())) {
+ ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType()));
+ return;
+ }
+ long ts = -1;
+ String tsStr = msg.getMetaData().getValue("ts");
+ if (!StringUtils.isEmpty(tsStr)) {
+ try {
+ ts = Long.parseLong(tsStr);
+ } catch (NumberFormatException e) {}
+ } else {
+ ts = System.currentTimeMillis();
+ }
+ String src = msg.getData();
+ TelemetryUploadRequest telemetryUploadRequest = JsonConverter.convertToTelemetry(new JsonParser().parse(src), ts);
+ Map<Long, List<KvEntry>> tsKvMap = telemetryUploadRequest.getData();
+ if (tsKvMap == null) {
+ ctx.tellFailure(msg, new IllegalArgumentException("Msg body is empty: " + src));
+ return;
+ }
+ List<TsKvEntry> tsKvEntryList = new ArrayList<>();
+ for (Map.Entry<Long, List<KvEntry>> tsKvEntry : tsKvMap.entrySet()) {
+ for (KvEntry kvEntry : tsKvEntry.getValue()) {
+ tsKvEntryList.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry));
+ }
+ }
+ String ttlValue = msg.getMetaData().getValue("TTL");
+ long ttl = !StringUtils.isEmpty(ttlValue) ? Long.valueOf(ttlValue) : config.getDefaultTTL();
+ ctx.getTelemetryService().saveAndNotify(msg.getOriginator(), tsKvEntryList, ttl, new TelemetryNodeCallback(ctx, msg));
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java
new file mode 100644
index 0000000..679745c
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.transform;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+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.msg.TbMsg;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+
+/**
+ * Created by ashvayka on 19.01.18.
+ */
+@Slf4j
+public abstract class TbAbstractTransformNode implements TbNode {
+
+ private TbTransformNodeConfiguration config;
+
+ @Override
+ public void init(TbContext context, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbTransformNodeConfiguration.class);
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ withCallback(transform(ctx, msg),
+ m -> {
+ if (m != null) {
+ ctx.tellNext(m, SUCCESS);
+ } else {
+ ctx.tellNext(msg, FAILURE);
+ }
+ },
+ t -> ctx.tellFailure(msg, t));
+ }
+
+ protected abstract ListenableFuture<TbMsg> transform(TbContext ctx, TbMsg msg);
+
+ public void setConfig(TbTransformNodeConfiguration config) {
+ this.config = config;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
new file mode 100644
index 0000000..a68cc82
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
@@ -0,0 +1,109 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.transform;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader;
+import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader;
+import org.thingsboard.rule.engine.util.EntitiesTenantIdAsyncLoader;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.HashSet;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.TRANSFORMATION,
+ name = "change originator",
+ configClazz = TbChangeOriginatorNodeConfiguration.class,
+ nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity",
+ nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
+ "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ",
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbTransformationNodeChangeOriginatorConfig",
+ icon = "find_replace"
+)
+public class TbChangeOriginatorNode extends TbAbstractTransformNode {
+
+ protected static final String CUSTOMER_SOURCE = "CUSTOMER";
+ protected static final String TENANT_SOURCE = "TENANT";
+ protected static final String RELATED_SOURCE = "RELATED";
+
+ private TbChangeOriginatorNodeConfiguration config;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbChangeOriginatorNodeConfiguration.class);
+ validateConfig(config);
+ setConfig(config);
+ }
+
+ @Override
+ protected ListenableFuture<TbMsg> transform(TbContext ctx, TbMsg msg) {
+ ListenableFuture<? extends EntityId> newOriginator = getNewOriginator(ctx, msg.getOriginator());
+ return Futures.transform(newOriginator, (Function<EntityId, TbMsg>) n -> {
+ if (n == null || n.isNullUid()) {
+ return null;
+ }
+ return ctx.transformMsg(msg, msg.getType(), n, msg.getMetaData(), msg.getData());
+ }, ctx.getDbCallbackExecutor());
+ }
+
+ private ListenableFuture<? extends EntityId> getNewOriginator(TbContext ctx, EntityId original) {
+ switch (config.getOriginatorSource()) {
+ case CUSTOMER_SOURCE:
+ return EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, original);
+ case TENANT_SOURCE:
+ return EntitiesTenantIdAsyncLoader.findEntityIdAsync(ctx, original);
+ case RELATED_SOURCE:
+ return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, original, config.getRelationsQuery());
+ default:
+ return Futures.immediateFailedFuture(new IllegalStateException("Unexpected originator source " + config.getOriginatorSource()));
+ }
+ }
+
+ private void validateConfig(TbChangeOriginatorNodeConfiguration conf) {
+ HashSet<String> knownSources = Sets.newHashSet(CUSTOMER_SOURCE, TENANT_SOURCE, RELATED_SOURCE);
+ if (!knownSources.contains(conf.getOriginatorSource())) {
+ log.error("Unsupported source [{}] for TbChangeOriginatorNode", conf.getOriginatorSource());
+ throw new IllegalArgumentException("Unsupported source TbChangeOriginatorNode" + conf.getOriginatorSource());
+ }
+
+ if (conf.getOriginatorSource().equals(RELATED_SOURCE)) {
+ if (conf.getRelationsQuery() == null) {
+ log.error("Related source for TbChangeOriginatorNode should have relations query. Actual [{}]",
+ conf.getRelationsQuery());
+ throw new IllegalArgumentException("Wrong config for RElated Source in TbChangeOriginatorNode" + conf.getOriginatorSource());
+ }
+ }
+
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java
new file mode 100644
index 0000000..32e9119
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.transform;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.rule.engine.data.RelationsQuery;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.EntityTypeFilter;
+
+import java.util.Collections;
+
+@Data
+public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
+
+ private String originatorSource;
+
+ private RelationsQuery relationsQuery;
+
+ @Override
+ public TbChangeOriginatorNodeConfiguration defaultConfiguration() {
+ TbChangeOriginatorNodeConfiguration configuration = new TbChangeOriginatorNodeConfiguration();
+ configuration.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE);
+
+ RelationsQuery relationsQuery = new RelationsQuery();
+ relationsQuery.setDirection(EntitySearchDirection.FROM);
+ relationsQuery.setMaxLevel(1);
+ EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
+ relationsQuery.setFilters(Collections.singletonList(entityTypeFilter));
+ configuration.setRelationsQuery(relationsQuery);
+
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
new file mode 100644
index 0000000..ab73d7c
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.transform;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+@RuleNode(
+ type = ComponentType.TRANSFORMATION,
+ name = "script",
+ configClazz = TbTransformMsgNodeConfiguration.class,
+ nodeDescription = "Change Message payload, Metadata or Message type using JavaScript",
+ nodeDetails = "JavaScript function receive 3 input parameters <br/> " +
+ "<code>metadata</code> - is a Message metadata.<br/>" +
+ "<code>msg</code> - is a Message payload.<br/>" +
+ "<code>msgType</code> - is a Message type.<br/>" +
+ "Should return the following structure:<br/>" +
+ "<code>{ msg: <i style=\"color: #666;\">new payload</i>,<br/>   metadata: <i style=\"color: #666;\">new metadata</i>,<br/>   msgType: <i style=\"color: #666;\">new msgType</i> }</code><br/>" +
+ "All fields in resulting object are optional and will be taken from original message if not specified.",
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbTransformationNodeScriptConfig")
+public class TbTransformMsgNode extends TbAbstractTransformNode {
+
+ private TbTransformMsgNodeConfiguration config;
+ private ScriptEngine jsEngine;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class);
+ this.jsEngine = ctx.createJsScriptEngine(config.getJsScript());
+ setConfig(config);
+ }
+
+ @Override
+ protected ListenableFuture<TbMsg> transform(TbContext ctx, TbMsg msg) {
+ return ctx.getJsExecutor().executeAsync(() -> jsEngine.executeUpdate(msg));
+ }
+
+ @Override
+ public void destroy() {
+ if (jsEngine != null) {
+ jsEngine.destroy();
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java
new file mode 100644
index 0000000..3e112ad
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.transform;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
+
+ private String jsScript;
+
+ @Override
+ public TbTransformMsgNodeConfiguration defaultConfiguration() {
+ TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration();
+ configuration.setJsScript("return {msg: msg, metadata: metadata, msgType: msgType};");
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java
new file mode 100644
index 0000000..be72833
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.util;
+
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.HasCustomerId;
+import org.thingsboard.server.common.data.id.*;
+
+public class EntitiesCustomerIdAsyncLoader {
+
+
+ public static ListenableFuture<CustomerId> findEntityIdAsync(TbContext ctx, EntityId original) {
+
+ switch (original.getEntityType()) {
+ case CUSTOMER:
+ return Futures.immediateFuture((CustomerId) original);
+ case USER:
+ return getCustomerAsync(ctx.getUserService().findUserByIdAsync((UserId) original));
+ case ASSET:
+ return getCustomerAsync(ctx.getAssetService().findAssetByIdAsync((AssetId) original));
+ case DEVICE:
+ return getCustomerAsync(ctx.getDeviceService().findDeviceByIdAsync((DeviceId) original));
+ default:
+ return Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original));
+ }
+ }
+
+ private static <T extends HasCustomerId> ListenableFuture<CustomerId> getCustomerAsync(ListenableFuture<T> future) {
+ return Futures.transformAsync(future, in -> in != null ? Futures.immediateFuture(in.getCustomerId())
+ : Futures.immediateFuture(null));
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java
new file mode 100644
index 0000000..9e3a639
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.util;
+
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.apache.commons.collections.CollectionUtils;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.data.DeviceRelationsQuery;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.device.DeviceSearchQuery;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
+import org.thingsboard.server.dao.device.DeviceService;
+
+import java.util.List;
+
+public class EntitiesRelatedDeviceIdAsyncLoader {
+
+ public static ListenableFuture<DeviceId> findDeviceAsync(TbContext ctx, EntityId originator,
+ DeviceRelationsQuery deviceRelationsQuery) {
+ DeviceService deviceService = ctx.getDeviceService();
+ DeviceSearchQuery query = buildQuery(originator, deviceRelationsQuery);
+
+ ListenableFuture<List<Device>> asyncDevices = deviceService.findDevicesByQuery(query);
+
+ return Futures.transformAsync(asyncDevices, d -> CollectionUtils.isNotEmpty(d) ? Futures.immediateFuture(d.get(0).getId())
+ : Futures.immediateFuture(null));
+ }
+
+ private static DeviceSearchQuery buildQuery(EntityId originator, DeviceRelationsQuery deviceRelationsQuery) {
+ DeviceSearchQuery query = new DeviceSearchQuery();
+ RelationsSearchParameters parameters = new RelationsSearchParameters(originator,
+ deviceRelationsQuery.getDirection(), deviceRelationsQuery.getMaxLevel());
+ query.setParameters(parameters);
+ query.setRelationType(deviceRelationsQuery.getRelationType());
+ query.setDeviceTypes(deviceRelationsQuery.getDeviceTypes());
+ return query;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java
new file mode 100644
index 0000000..f4de8fc
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.util;
+
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.apache.commons.collections.CollectionUtils;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.data.RelationsQuery;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
+import org.thingsboard.server.dao.relation.RelationService;
+
+import java.util.List;
+
+public class EntitiesRelatedEntityIdAsyncLoader {
+
+ public static ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator,
+ RelationsQuery relationsQuery) {
+ RelationService relationService = ctx.getRelationService();
+ EntityRelationsQuery query = buildQuery(originator, relationsQuery);
+ ListenableFuture<List<EntityRelation>> asyncRelation = relationService.findByQuery(query);
+ if (relationsQuery.getDirection() == EntitySearchDirection.FROM) {
+ return Futures.transformAsync(asyncRelation, r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getTo())
+ : Futures.immediateFuture(null));
+ } else if (relationsQuery.getDirection() == EntitySearchDirection.TO) {
+ return Futures.transformAsync(asyncRelation, r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getFrom())
+ : Futures.immediateFuture(null));
+ }
+ return Futures.immediateFailedFuture(new IllegalStateException("Unknown direction"));
+ }
+
+ private static EntityRelationsQuery buildQuery(EntityId originator, RelationsQuery relationsQuery) {
+ EntityRelationsQuery query = new EntityRelationsQuery();
+ RelationsSearchParameters parameters = new RelationsSearchParameters(originator,
+ relationsQuery.getDirection(), relationsQuery.getMaxLevel());
+ query.setParameters(parameters);
+ query.setFilters(relationsQuery.getFilters());
+ return query;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java
new file mode 100644
index 0000000..774c269
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.util;
+
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.HasTenantId;
+import org.thingsboard.server.common.data.alarm.AlarmId;
+import org.thingsboard.server.common.data.id.*;
+
+public class EntitiesTenantIdAsyncLoader {
+
+ public static ListenableFuture<TenantId> findEntityIdAsync(TbContext ctx, EntityId original) {
+
+ switch (original.getEntityType()) {
+ case TENANT:
+ return Futures.immediateFuture((TenantId) original);
+ case CUSTOMER:
+ return getTenantAsync(ctx.getCustomerService().findCustomerByIdAsync((CustomerId) original));
+ case USER:
+ return getTenantAsync(ctx.getUserService().findUserByIdAsync((UserId) original));
+ case ASSET:
+ return getTenantAsync(ctx.getAssetService().findAssetByIdAsync((AssetId) original));
+ case DEVICE:
+ return getTenantAsync(ctx.getDeviceService().findDeviceByIdAsync((DeviceId) original));
+ case ALARM:
+ return getTenantAsync(ctx.getAlarmService().findAlarmByIdAsync((AlarmId) original));
+ case RULE_CHAIN:
+ return getTenantAsync(ctx.getRuleChainService().findRuleChainByIdAsync((RuleChainId) original));
+ default:
+ return Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original));
+ }
+ }
+
+ private static <T extends HasTenantId> ListenableFuture<TenantId> getTenantAsync(ListenableFuture<T> future) {
+ return Futures.transformAsync(future, in -> {
+ return in != null ? Futures.immediateFuture(in.getTenantId())
+ : Futures.immediateFuture(null);});
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css
new file mode 100644
index 0000000..b5109b5
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css
@@ -0,0 +1,2 @@
+.tb-message-type-autocomplete .tb-not-found{display:block;line-height:1.5;height:48px}.tb-message-type-autocomplete .tb-not-found .tb-no-entries{line-height:48px}.tb-message-type-autocomplete li{height:auto!important;white-space:normal!important}.tb-generator-config tb-json-content.tb-message-body,.tb-generator-config tb-json-object-edit.tb-metadata-json{height:200px;display:block}.tb-mqtt-config .tb-credentials-panel-group .tb-panel-title{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;min-width:90px}@media (min-width:960px){.tb-mqtt-config .tb-credentials-panel-group .tb-panel-title{min-width:180px}}.tb-mqtt-config .tb-credentials-panel-group .tb-panel-prompt{font-size:14px;color:rgba(0,0,0,.87);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tb-mqtt-config .tb-credentials-panel-group.disabled .tb-panel-prompt,.tb-mqtt-config .tb-credentials-panel-group.disabled .tb-panel-title{color:rgba(0,0,0,.38)}.tb-mqtt-config .tb-credentials-panel-group md-icon.md-expansion-panel-icon{margin-right:0}.tb-mqtt-config .tb-container{width:100%}.tb-mqtt-config .dropdown-messages .tb-error-message{padding:5px 0 0}.tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}.tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:rgba(0,0,0,.54);font-size:12px;font-weight:700;white-space:nowrap}.tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}.tb-kv-map-config .body .row{padding-top:5px;max-height:40px}.tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}.tb-kv-map-config .body md-input-container.cell{margin:0;max-height:40px}.tb-kv-map-config .body .md-button{margin:0}
+/*# sourceMappingURL=rulenode-core-config.css.map*/
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
new file mode 100644
index 0000000..11dc000
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
@@ -0,0 +1,4 @@
+!function(e){function t(a){if(n[a])return n[a].exports;var r=n[a]={exports:{},id:a,loaded:!1};return e[a].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}(function(e){for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t))switch(typeof e[t]){case"function":break;case"object":e[t]=function(t){var n=t.slice(1),a=e[t[0]];return function(e,t,r){a.apply(this,[e,t,r].concat(n))}}(e[t]);break;default:e[t]=e[e[t]]}return e}([function(e,t,n){e.exports=n(68)},function(e,t){},1,1,1,function(e,t){e.exports=' <section ng-form name=attributesConfigForm layout=column> <md-input-container class=md-block> <label translate>attribute.attributes-scope</label> <md-select ng-model=configuration.scope ng-disabled=$root.loading> <md-option ng-repeat="scope in types.attributesScope" ng-value=scope.value> {{scope.name | translate}} </md-option> </md-select> </md-input-container> </section> '},function(e,t){e.exports=" <section class=tb-alarm-config ng-form name=alarmConfigForm layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-details-builder</label> <tb-js-func ng-model=configuration.alarmDetailsBuildJs function-name=Details function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testDetailsBuildJs($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-details-function' | translate }} </md-button> </div> <md-input-container class=md-block> <label translate>tb.rulenode.alarm-type</label> <input ng-required=true name=alarmType ng-model=configuration.alarmType> <div ng-messages=alarmConfigForm.alarmType.$error> <div ng-message=required translate>tb.rulenode.alarm-type-required</div> </div> </md-input-container> </section> "},function(e,t){e.exports=" <section class=tb-alarm-config ng-form name=alarmConfigForm layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-details-builder</label> <tb-js-func ng-model=configuration.alarmDetailsBuildJs function-name=Details function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testDetailsBuildJs($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-details-function' | translate }} </md-button> </div> <section layout=column layout-gt-sm=row> <md-input-container flex class=md-block> <label translate>tb.rulenode.alarm-type</label> <input ng-required=true name=alarmType ng-model=configuration.alarmType> <div ng-messages=alarmConfigForm.alarmType.$error> <div ng-message=required translate>tb.rulenode.alarm-type-required</div> </div> </md-input-container> <md-input-container flex class=md-block> <label translate>tb.rulenode.alarm-severity</label> <md-select required name=severity ng-model=configuration.severity> <md-option ng-repeat=\"(severityKey, severity) in types.alarmSeverity\" ng-value=severityKey> {{ severity.name | translate}} </md-option> </md-select> <div ng-messages=alarmConfigForm.severity.$error> <div ng-message=required translate>tb.rulenode.alarm-severity-required</div> </div> </md-input-container> </section> <md-checkbox aria-label=\"{{ 'tb.rulenode.propagate' | translate }}\" ng-model=configuration.propagate>{{ 'tb.rulenode.propagate' | translate }} </md-checkbox> </section> "},function(e,t){e.exports=" <section class=tb-generator-config ng-form name=generatorConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.message-count</label> <input ng-required=true type=number step=1 name=messageCount ng-model=configuration.msgCount min=0> <div ng-messages=generatorConfigForm.messageCount.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.message-count-required</div> <div ng-message=min translate>tb.rulenode.min-message-count-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.period-seconds</label> <input ng-required=true type=number step=1 name=periodInSeconds ng-model=configuration.periodInSeconds min=1> <div ng-messages=generatorConfigForm.periodInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.period-seconds-required</div> <div ng-message=min translate>tb.rulenode.min-period-seconds-message</div> </div> </md-input-container> <div layout=column> <label class=tb-small>{{ 'tb.rulenode.originator' | translate }}</label> <tb-entity-select the-form=generatorConfigForm tb-required=false ng-model=originator> </tb-entity-select> </div> <label translate class=\"tb-title no-padding\">tb.rulenode.generate</label> <tb-js-func ng-model=configuration.jsScript function-name=Generate function-args=\"{{ ['prevMsg', 'prevMetadata', 'prevMsgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-generator-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section ng-form name=kafkaConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-pattern</label> <input ng-required=true name=topicPattern ng-model=configuration.topicPattern> <div ng-messages=kafkaConfigForm.topicPattern.$error> <div ng-message=required translate>tb.rulenode.topic-pattern-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.bootstrap-servers</label> <input ng-required=true name=bootstrapServers ng-model=configuration.bootstrapServers> <div ng-messages=kafkaConfigForm.bootstrapServers.$error> <div ng-message=required translate>tb.rulenode.bootstrap-servers-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.retries</label> <input type=number step=1 name=retries ng-model=configuration.retries min=0> <div ng-messages=kafkaConfigForm.retries.$error> <div ng-message=min translate>tb.rulenode.min-retries-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.batch-size-bytes</label> <input type=number step=1 name=batchSize ng-model=configuration.batchSize min=0> <div ng-messages=kafkaConfigForm.batchSize.$error> <div ng-message=min translate>tb.rulenode.min-batch-size-bytes-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.linger-ms</label> <input type=number step=1 name=linger ng-model=configuration.linger min=0> <div ng-messages=kafkaConfigForm.linger.$error> <div ng-message=min translate>tb.rulenode.min-linger-ms-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.buffer-memory-bytes</label> <input type=number step=1 name=bufferMemory ng-model=configuration.bufferMemory min=0> <div ng-messages=kafkaConfigForm.bufferMemory.$error> <div ng-message=min translate>tb.rulenode.min-buffer-memory-bytes-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.acks</label> <md-select ng-model=configuration.acks ng-disabled=$root.loading> <md-option ng-repeat="ackValue in ackValues" ng-value=ackValue> {{ ackValue }} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.key-serializer</label> <input ng-required=true name=keySerializer ng-model=configuration.keySerializer> <div ng-messages=kafkaConfigForm.keySerializer.$error> <div ng-message=required translate>tb.rulenode.key-serializer-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.value-serializer</label> <input ng-required=true name=valueSerializer ng-model=configuration.valueSerializer> <div ng-messages=kafkaConfigForm.valueSerializer.$error> <div ng-message=required translate>tb.rulenode.value-serializer-required</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.other-properties</label> <tb-kv-map-config ng-model=configuration.otherProperties ng-required=false key-text="\'tb.rulenode.key\'" key-required-text="\'tb.rulenode.key-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.to-string</label> <tb-js-func ng-model=configuration.jsScript function-name=ToString function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-to-string-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section class=tb-mqtt-config ng-form name=mqttConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-pattern</label> <input ng-required=true name=topicPattern ng-model=configuration.topicPattern> <div ng-messages=mqttConfigForm.topicPattern.$error> <div translate ng-message=required>tb.rulenode.topic-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.mqtt-topic-pattern-hint</div> </md-input-container> <div flex layout=column layout-gt-sm=row> <md-input-container flex=60 class=md-block> <label translate>tb.rulenode.host</label> <input ng-required=true name=host ng-model=configuration.host> <div ng-messages=mqttConfigForm.host.$error> <div translate ng-message=required>tb.rulenode.host-required</div> </div> </md-input-container> <md-input-container flex=40 class=md-block> <label translate>tb.rulenode.port</label> <input type=number step=1 min=1 max=65535 ng-required=true name=port ng-model=configuration.port> <div ng-messages=mqttConfigForm.port.$error> <div translate ng-message=required>tb.rulenode.port-required</div> <div translate ng-message=min>tb.rulenode.port-range</div> <div translate ng-message=max>tb.rulenode.port-range</div> </div> </md-input-container> <md-input-container flex=40 class=md-block> <label translate>tb.rulenode.connect-timeout</label> <input type=number step=1 min=1 max=200 ng-required=true name=connectTimeoutSec ng-model=configuration.connectTimeoutSec> <div ng-messages=mqttConfigForm.connectTimeoutSec.$error> <div translate ng-message=required>tb.rulenode.connect-timeout-required</div> <div translate ng-message=min>tb.rulenode.connect-timeout-range</div> <div translate ng-message=max>tb.rulenode.connect-timeout-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.client-id</label> <input name=clientId ng-model=configuration.clientId> </md-input-container> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.enable-ssl\' | translate }}" ng-model=configuration.ssl> {{ \'tb.rulenode.enable-ssl\' | translate }} </md-checkbox> <md-expansion-panel-group class=tb-credentials-panel-group ng-class="{\'disabled\': $root.loading || readonly}" md-component-id=credentialsPanelGroup> <md-expansion-panel md-component-id=credentialsPanel> <md-expansion-panel-collapsed> <div class=tb-panel-title>{{ \'tb.rulenode.credentials\' | translate }}</div> <div class=tb-panel-prompt>{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}</div> <span flex></span> <md-expansion-panel-icon></md-expansion-panel-icon> </md-expansion-panel-collapsed> <md-expansion-panel-expanded> <md-expansion-panel-header ng-click="$mdExpansionPanel(\'credentialsPanel\').collapse()"> <div class=tb-panel-title>{{ \'tb.rulenode.credentials\' | translate }}</div> <div class=tb-panel-prompt>{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}</div> <span flex></span> <md-expansion-panel-icon></md-expansion-panel-icon> </md-expansion-panel-header> <md-expansion-panel-content> <div layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.credentials-type</label> <md-select ng-required=true name=credentialsType ng-model=configuration.credentials.type ng-disabled="$root.loading || readonly" ng-change=credentialsTypeChanged()> <md-option ng-repeat="(credentialsType, credentialsValue) in ruleNodeTypes.mqttCredentialTypes" ng-value=credentialsValue.value> {{credentialsValue.name | translate}} </md-option> </md-select> <div ng-messages=mqttConfigForm.credentialsType.$error> <div translate ng-message=required>tb.rulenode.credentials-type-required</div> </div> </md-input-container> <section flex layout=column ng-if="configuration.credentials.type == ruleNodeTypes.mqttCredentialTypes.basic.value"> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input ng-required=true name=mqttUsername ng-model=configuration.credentials.username> <div ng-messages=mqttConfigForm.mqttUsername.$error> <div translate ng-message=required>tb.rulenode.username-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input type=password ng-required=true name=mqttPassword ng-model=configuration.credentials.password> <div ng-messages=mqttConfigForm.mqttPassword.$error> <div translate ng-message=required>tb.rulenode.password-required</div> </div> </md-input-container> </section> <section flex layout=column ng-if="configuration.credentials.type == ruleNodeTypes.mqttCredentialTypes[\'cert.PEM\'].value" class=dropdown-section> <div class=tb-container ng-class="configuration.credentials.caCertFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.ca-cert</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'caCert\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'caCert\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=caCertSelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=caCertSelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.caCertFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.caCertFileName>{{configuration.credentials.caCertFileName}}</div> </div> <div class=tb-container ng-class="configuration.credentials.certFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.cert</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'Cert\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'Cert\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=CertSelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=CertSelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.certFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.certFileName>{{configuration.credentials.certFileName}}</div> </div> <div class=tb-container ng-class="configuration.credentials.privateKeyFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.private-key</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'privateKey\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'privateKey\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=privateKeySelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=privateKeySelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.privateKeyFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.privateKeyFileName>{{configuration.credentials.privateKeyFileName}}</div> </div> <md-input-container class=md-block> <label translate>tb.rulenode.private-key-password</label> <input type=password name=privateKeyPassword ng-model=configuration.credentials.password> </md-input-container> </section> </div> </md-expansion-panel-content> </md-expansion-panel-expanded> </md-expansion-panel> </md-expansion-panel-group> </section>'},function(e,t){e.exports=' <section ng-form name=rabbitMqConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.exchange-name-pattern</label> <input name=exchangeNamePattern ng-model=configuration.exchangeNamePattern> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.routing-key-pattern</label> <input name=routingKeyPattern ng-model=configuration.routingKeyPattern> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.message-properties</label> <md-select ng-model=configuration.messageProperties ng-disabled="$root.loading || readonly"> <md-option ng-repeat="property in messageProperties" ng-value=property> {{ property }} </md-option> </md-select> </md-input-container> <div layout-gt-sm=row> <md-input-container class=md-block flex=100 flex-gt-sm=60> <label translate>tb.rulenode.host</label> <input ng-required=true name=host ng-model=configuration.host> <div ng-messages=rabbitMqConfigForm.host.$error> <div ng-message=required translate>tb.rulenode.host-required</div> </div> </md-input-container> <md-input-container class=md-block flex=100 flex-gt-sm=40> <label translate>tb.rulenode.port</label> <input ng-required=true type=number step=1 name=port ng-model=configuration.port min=0 max=65535> <div ng-messages=rabbitMqConfigForm.port.$error> <div ng-message=required translate>tb.rulenode.port-required</div> <div ng-message=min translate>tb.rulenode.port-range</div> <div ng-message=max translate>tb.rulenode.port-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.virtual-host</label> <input name=virtualHost ng-model=configuration.virtualHost> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input name=virtualHost ng-model=configuration.username> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input name=virtualHost type=password ng-model=configuration.password> </md-input-container> <md-input-container class=md-block> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.automatic-recovery\' | translate }}" ng-model=ruleNode.automaticRecoveryEnabled>{{ \'tb.rulenode.automatic-recovery\' | translate }} </md-checkbox> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.connection-timeout-ms</label> <input type=number step=1 name=connectionTimeout ng-model=configuration.connectionTimeout min=0> <div ng-messages=rabbitMqConfigForm.connectionTimeout.$error> <div ng-message=min translate>tb.rulenode.min-connection-timeout-ms-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.handshake-timeout-ms</label> <input type=number step=1 name=handshakeTimeout ng-model=configuration.handshakeTimeout min=0> <div ng-messages=rabbitMqConfigForm.handshakeTimeout.$error> <div ng-message=min translate>tb.rulenode.min-handshake-timeout-ms-message</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.client-properties</label> <tb-kv-map-config ng-model=configuration.clientProperties ng-required=false key-text="\'tb.rulenode.key\'" key-required-text="\'tb.rulenode.key-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=' <section ng-form name=restApiCallConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.endpoint-url-pattern</label> <input ng-required=true name=endpointUrlPattern ng-model=configuration.restEndpointUrlPattern> <div ng-messages=restApiCallConfigForm.endpointUrlPattern.$error> <div ng-message=required translate>tb.rulenode.endpoint-url-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.endpoint-url-pattern-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.request-method</label> <md-select ng-model=configuration.requestMethod ng-disabled=$root.loading> <md-option ng-repeat="type in ruleNodeTypes.httpRequestType" ng-value=type> {{ type }} </md-option> </md-select> </md-input-container> <label translate class=tb-title>tb.rulenode.headers</label> <div class=tb-hint translate>tb.rulenode.headers-hint</div> <tb-kv-map-config ng-model=configuration.headers ng-required=false key-text="\'tb.rulenode.header\'" key-required-text="\'tb.rulenode.header-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=" <section ng-form name=rpcReplyConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.request-id-metadata-attribute</label> <input name=requestIdMetaDataAttribute ng-model=configuration.requestIdMetaDataAttribute> </md-input-container> </section> "},function(e,t){e.exports=" <section ng-form name=rpcRequestConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.timeout-sec</label> <input ng-required=true type=number step=1 name=timeoutInSeconds ng-model=configuration.timeoutInSeconds min=0> <div ng-messages=rpcRequestConfigForm.timeoutInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.timeout-required</div> <div ng-message=min translate>tb.rulenode.min-timeout-message</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=sendEmailConfigForm layout=column> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.use-system-smtp-settings\' | translate }}" ng-model=configuration.useSystemSmtpSettings> {{ \'tb.rulenode.use-system-smtp-settings\' | translate }} </md-checkbox> <section layout=column ng-if=!configuration.useSystemSmtpSettings> <md-input-container class=md-block> <label translate>tb.rulenode.smtp-protocol</label> <md-select ng-disabled="$root.loading || readonly" ng-model=configuration.smtpProtocol> <md-option ng-repeat="smtpProtocol in smtpProtocols" value={{smtpProtocol}}> {{smtpProtocol.toUpperCase()}} </md-option> </md-select> </md-input-container> <div layout-gt-sm=row> <md-input-container class=md-block flex=100 flex-gt-sm=60> <label translate>tb.rulenode.smtp-host</label> <input ng-required=true name=smtpHost ng-model=configuration.smtpHost> <div ng-messages=sendEmailConfigForm.smtpHost.$error> <div translate ng-message=required>tb.rulenode.smtp-host-required</div> </div> </md-input-container> <md-input-container class=md-block flex=100 flex-gt-sm=40> <label translate>tb.rulenode.smtp-port</label> <input type=number step=1 min=1 max=65535 ng-required=true name=port ng-model=configuration.smtpPort> <div ng-messages=sendEmailConfigForm.port.$error> <div translate ng-message=required>tb.rulenode.smtp-port-required</div> <div translate ng-message=min>tb.rulenode.smtp-port-range</div> <div translate ng-message=max>tb.rulenode.smtp-port-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.timeout-msec</label> <input type=number step=1 min=0 ng-required=true name=timeout ng-model=configuration.timeout> <div ng-messages=sendEmailConfigForm.timeout.$error> <div translate ng-message=required>tb.rulenode.timeout-required</div> <div translate ng-message=min>tb.rulenode.min-timeout-msec-message</div> </div> </md-input-container> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.enable-tls\' | translate }}" ng-model=configuration.enableTls>{{ \'tb.rulenode.enable-tls\' | translate }}</md-checkbox> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input name=username placeholder="{{ \'tb.rulenode.enter-username\' | translate }}" ng-model=configuration.username> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input name=password placeholder="{{ \'tb.rulenode.enter-password\' | translate }}" type=password ng-model=configuration.password> </md-input-container> </section> </section> '},function(e,t){e.exports=" <section ng-form name=snsConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-arn-pattern</label> <input ng-required=true name=topicArnPattern ng-model=configuration.topicArnPattern> <div ng-messages=snsConfigForm.topicArnPattern.$error> <div ng-message=required translate>tb.rulenode.topic-arn-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.topic-arn-pattern-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-access-key-id</label> <input ng-required=true name=accessKeyId ng-model=configuration.accessKeyId> <div ng-messages=snsConfigForm.accessKeyId.$error> <div ng-message=required translate>tb.rulenode.aws-access-key-id-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-secret-access-key</label> <input ng-required=true name=secretAccessKey ng-model=configuration.secretAccessKey> <div ng-messages=snsConfigForm.secretAccessKey.$error> <div ng-message=required translate>tb.rulenode.aws-secret-access-key-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-region</label> <input ng-required=true name=region ng-model=configuration.region> <div ng-messages=snsConfigForm.region.$error> <div ng-message=required translate>tb.rulenode.aws-region-required</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=sqsConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.queue-type</label> <md-select ng-model=configuration.queueType ng-disabled="$root.loading || readonly"> <md-option ng-repeat="type in ruleNodeTypes.sqsQueueType" ng-value=type.value> {{ type.name | translate }} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.queue-url-pattern</label> <input ng-required=true name=queueUrlPattern ng-model=configuration.queueUrlPattern> <div ng-messages=sqsConfigForm.queueUrlPattern.$error> <div ng-message=required translate>tb.rulenode.queue-url-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.queue-url-pattern-hint</div> </md-input-container> <md-input-container class=md-block ng-if="configuration.queueType == ruleNodeTypes.sqsQueueType.STANDARD.value"> <label translate>tb.rulenode.delay-seconds</label> <input type=number step=1 name=delaySeconds ng-model=configuration.delaySeconds min=0 max=900> <div ng-messages=sqsConfigForm.delaySeconds.$error> <div ng-message=min translate>tb.rulenode.min-delay-seconds-message</div> <div ng-message=max translate>tb.rulenode.max-delay-seconds-message</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.message-attributes</label> <div class=tb-hint translate>tb.rulenode.message-attributes-hint</div> <tb-kv-map-config ng-model=configuration.messageAttributes ng-required=false key-text="\'tb.rulenode.name\'" key-required-text="\'tb.rulenode.name-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> <md-input-container class=md-block> <label translate>tb.rulenode.aws-access-key-id</label> <input ng-required=true name=accessKeyId ng-model=configuration.accessKeyId> <div ng-messages=snsConfigForm.accessKeyId.$error> <div ng-message=required translate>tb.rulenode.aws-access-key-id-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-secret-access-key</label> <input ng-required=true name=secretAccessKey ng-model=configuration.secretAccessKey> <div ng-messages=snsConfigForm.secretAccessKey.$error> <div ng-message=required translate>tb.rulenode.aws-secret-access-key-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-region</label> <input ng-required=true name=region ng-model=configuration.region> <div ng-messages=snsConfigForm.region.$error> <div ng-message=required translate>tb.rulenode.aws-region-required</div> </div> </md-input-container> </section> '},function(e,t){e.exports=" <section ng-form name=timeseriesConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.default-ttl</label> <input ng-required=true type=number step=1 name=defaultTTL ng-model=configuration.defaultTTL min=0> <div ng-messages=timeseriesConfigForm.defaultTTL.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.default-ttl-required</div> <div ng-message=min translate>tb.rulenode.min-default-ttl-message</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section layout=column> <div layout=row> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=query.direction> <md-option ng-repeat="direction in types.entitySearchDirection" ng-value=direction> {{ (\'relation.search-direction.\' + direction) | translate}} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-relation-level</label> <input name=maxRelationLevel type=number min=1 step=1 placeholder="{{ \'tb.rulenode.unlimited-level\' | translate }}" ng-model=query.maxLevel aria-label="{{ \'tb.rulenode.max-relation-level\' | translate }}"> </md-input-container> </div> <div class=md-caption style=color:rgba(0,0,0,.57) translate>relation.relation-type</div> <tb-relation-type-autocomplete flex hide-label ng-model=query.relationType tb-required=false> </tb-relation-type-autocomplete> <div class="md-caption tb-required" style=color:rgba(0,0,0,.57) translate>device.device-types</div> <tb-entity-subtype-list tb-required=true entity-type=types.entityType.device ng-model=query.deviceTypes> </tb-entity-subtype-list> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> ";
+},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title tb-required">tb.rulenode.device-relations-query</label> <tb-device-relations-query-config style=padding-bottom:15px ng-model=configuration.deviceRelationsQuery> </tb-device-relations-query-config> <label translate class="tb-title no-padding">tb.rulenode.client-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.clientAttributeNames placeholder="{{\'tb.rulenode.client-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.shared-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.sharedAttributeNames placeholder="{{\'tb.rulenode.shared-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.server-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.serverAttributeNames placeholder="{{\'tb.rulenode.server-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> </section> '},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding">tb.rulenode.client-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.clientAttributeNames placeholder="{{\'tb.rulenode.client-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.shared-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.sharedAttributeNames placeholder="{{\'tb.rulenode.shared-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.server-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.serverAttributeNames placeholder="{{\'tb.rulenode.server-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> "},21,function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding" ng-class="{\'tb-required\': required}">tb.rulenode.message-types-filter</label> <md-chips id=message_type_chips ng-required=required readonly=readonly ng-model=messageTypes md-autocomplete-snap md-transform-chip=transformMessageTypeChip($chip) md-require-match=false> <md-autocomplete id=message_type md-no-cache=true md-selected-item=selectedMessageType md-search-text=messageTypeSearchText md-items="item in messageTypesSearch(messageTypeSearchText)" md-item-text=item.name md-min-length=0 placeholder="{{\'tb.rulenode.message-type\' | translate }}" md-menu-class=tb-message-type-autocomplete> <span md-highlight-text=messageTypeSearchText md-highlight-flags=^i>{{item}}</span> <md-not-found> <div class=tb-not-found> <div class=tb-no-entries ng-if="!messageTypeSearchText || !messageTypeSearchText.length"> <span translate>tb.rulenode.no-message-types-found</span> </div> <div ng-if="messageTypeSearchText && messageTypeSearchText.length"> <span translate translate-values=\'{ messageType: "{{messageTypeSearchText | truncate:true:6:'...'}}" }\'>tb.rulenode.no-message-type-matching</span> <span> <a translate ng-click="createMessageType($event, \'#message_type_chips\')">tb.rulenode.create-new-message-type</a> </span> </div> </div> </md-not-found> </md-autocomplete> <md-chip-template> <span>{{$chip.name}}</span> </md-chip-template> </md-chips> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=messageTypes class=tb-error-message>tb.rulenode.message-types-required</div> </div> </section>'},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.filter</label> <tb-js-func ng-model=configuration.jsScript function-name=Filter function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-filter-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.switch</label> <tb-js-func ng-model=configuration.jsScript function-name=Switch function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-switch-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section class=tb-kv-map-config layout=column> <div class=header flex layout=row> <span class=cell flex translate>{{ keyText }}</span> <span class=cell flex translate>{{ valText }}</span> <span ng-show=!disabled style=width:52px> </span> </div> <div class=body> <div class=row ng-form name=kvForm flex layout=row layout-align="start center" ng-repeat="keyVal in kvList track by $index"> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ keyText | translate }}" ng-required=true name=key ng-model=keyVal.key> <div ng-messages=kvForm.key.$error> <div translate ng-message=required>{{keyRequiredText}}</div> </div> </md-input-container> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ valText | translate }}" ng-required=true name=value ng-model=keyVal.value> <div ng-messages=kvForm.value.$error> <div translate ng-message=required>{{valRequiredText}}</div> </div> </md-input-container> <md-button ng-show=!disabled ng-disabled=loading class="md-icon-button md-primary" ng-click=removeKeyVal($index) aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.remove-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.delete\' | translate }}" class=material-icons> close </md-icon> </md-button> </div> </div> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=kvMap class=tb-error-message>{{requiredText}}</div> </div> <div> <md-button ng-show=!disabled ng-disabled=loading class="md-primary md-raised" ng-click=addKeyVal() aria-label="{{ \'action.add\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.add-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.add\' | translate }}" class=material-icons> add </md-icon> {{ \'action.add\' | translate }} </md-button> </div> </section> '},function(e,t){e.exports=" <section layout=column> <div layout=row> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=query.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-relation-level</label> <input name=maxRelationLevel type=number min=1 step=1 placeholder=\"{{ 'tb.rulenode.unlimited-level' | translate }}\" ng-model=query.maxLevel aria-label=\"{{ 'tb.rulenode.max-relation-level' | translate }}\"> </md-input-container> </div> <div class=md-caption style=padding-bottom:10px;color:rgba(0,0,0,.57) translate>relation.relation-filters</div> <tb-relation-filters ng-model=query.filters> </tb-relation-filters> </section> "},function(e,t){e.exports=' <section layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.originator-source</label> <md-select required ng-model=configuration.originatorSource> <md-option ng-repeat="source in ruleNodeTypes.originatorSource" ng-value=source.value> {{ source.name | translate}} </md-option> </md-select> </md-input-container> <section layout=column ng-if="configuration.originatorSource == ruleNodeTypes.originatorSource.RELATED.value"> <label translate class="tb-title tb-required">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> </section> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.transform</label> <tb-js-func ng-model=configuration.jsScript function-name=Transform function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-transformer-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section ng-form name=toEmailConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.from-template</label> <textarea ng-required=true name=fromTemplate ng-model=configuration.fromTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.fromTemplate.$error> <div ng-message=required translate>tb.rulenode.from-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.from-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.to-template</label> <textarea ng-required=true name=toTemplate ng-model=configuration.toTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.toTemplate.$error> <div ng-message=required translate>tb.rulenode.to-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.cc-template</label> <textarea name=ccTemplate ng-model=configuration.ccTemplate rows=2></textarea> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.bcc-template</label> <textarea name=ccTemplate ng-model=configuration.bccTemplate rows=2></textarea> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.subject-template</label> <textarea ng-required=true name=subjectTemplate ng-model=configuration.subjectTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.subjectTemplate.$error> <div ng-message=required translate>tb.rulenode.subject-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.subject-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.body-template</label> <textarea ng-required=true name=bodyTemplate ng-model=configuration.bodyTemplate rows=6></textarea> <div ng-messages=toEmailConfigForm.bodyTemplate.$error> <div ng-message=required translate>tb.rulenode.body-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.body-template-hint</div> </md-input-container> </section> "},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(5),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,a){var r=function(r,i,l,s){var u=o.default;i.html(u),r.types=n,r.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(r.configuration)}),s.$render=function(){r.configuration=s.$viewValue},r.testDetailsBuildJs=function(e){var n=angular.copy(r.configuration.alarmDetailsBuildJs);a.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(i.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}r.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(6),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,a){var r=function(r,i,l,s){var u=o.default;i.html(u),r.types=n,r.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(r.configuration)}),s.$render=function(){r.configuration=s.$viewValue},r.testDetailsBuildJs=function(e){var n=angular.copy(r.configuration.alarmDetailsBuildJs);a.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(i.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}r.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(7),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,a){var r=function(r,i,l,s){var u=o.default;i.html(u),r.types=n,r.originator=null,r.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(r.configuration)}),s.$render=function(){r.configuration=s.$viewValue,r.configuration.originatorId&&r.configuration.originatorType?r.originator={id:r.configuration.originatorId,entityType:r.configuration.originatorType}:r.originator=null,r.$watch("originator",function(e,t){angular.equals(e,t)||(r.originator?(s.$viewValue.originatorId=r.originator.id,s.$viewValue.originatorType=r.originator.entityType):(s.$viewValue.originatorId=null,s.$viewValue.originatorType=null))},!0)},r.testScript=function(e){var n=angular.copy(r.configuration.jsScript);a.testNodeScript(e,n,"generate",t.instant("tb.rulenode.generator")+"","Generate",["prevMsg","prevMetadata","prevMsgType"],r.ruleNodeId).then(function(e){r.configuration.jsScript=e,s.$setDirty()})},e(i.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}r.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(1);var i=n(8),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(49),i=a(r),o=n(34),l=a(o),s=n(37),u=a(s),d=n(36),c=a(d),m=n(35),g=a(m),p=n(40),f=a(p),b=n(44),v=a(b),y=n(45),q=a(y),T=n(43),h=a(T),$=n(39),k=a($),w=n(47),C=a(w),x=n(48),_=a(x),E=n(42),S=a(E),M=n(41),N=a(M),V=n(46),P=a(V);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTimeseriesConfig",i.default).directive("tbActionNodeAttributesConfig",l.default).directive("tbActionNodeGeneratorConfig",u.default).directive("tbActionNodeCreateAlarmConfig",c.default).directive("tbActionNodeClearAlarmConfig",g.default).directive("tbActionNodeLogConfig",f.default).directive("tbActionNodeRpcReplyConfig",v.default).directive("tbActionNodeRpcRequestConfig",q.default).directive("tbActionNodeRestApiCallConfig",h.default).directive("tbActionNodeKafkaConfig",k.default).directive("tbActionNodeSnsConfig",C.default).directive("tbActionNodeSqsConfig",_.default).directive("tbActionNodeRabbitMqConfig",S.default).directive("tbActionNodeMqttConfig",N.default).directive("tbActionNodeSendEmailConfig",P.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.ackValues=["all","-1","0","1"],t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(9),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(10),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$mdExpansionPanel=t,a.ruleNodeTypes=n,a.credentialsTypeChanged=function(){var e=a.configuration.credentials.type;a.configuration.credentials={},a.configuration.credentials.type=e,a.updateValidity()},a.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){a.$apply(function(){if(n.target.result){l.$setDirty();var r=n.target.result;r&&r.length>0&&("caCert"==t&&(a.configuration.credentials.caCertFileName=e.name,a.configuration.credentials.caCert=r),"privateKey"==t&&(a.configuration.credentials.privateKeyFileName=e.name,a.configuration.credentials.privateKey=r),"Cert"==t&&(a.configuration.credentials.certFileName=e.name,a.configuration.credentials.cert=r)),a.updateValidity()}})},n.readAsText(e.file)},a.clearCertFile=function(e){l.$setDirty(),"caCert"==e&&(a.configuration.credentials.caCertFileName=null,a.configuration.credentials.caCert=null),"privateKey"==e&&(a.configuration.credentials.privateKeyFileName=null,a.configuration.credentials.privateKey=null),"Cert"==e&&(a.configuration.credentials.certFileName=null,a.configuration.credentials.cert=null),a.updateValidity()},a.updateValidity=function(){var e=!0,t=a.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.caCert&&t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:a}}r.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(2);var i=n(11),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(12),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(13),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(14),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(15),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.smtpProtocols=["smtp","smtps"],t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(16),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(17),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(18),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(19),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(20),o=a(i)},function(e,t){"use strict";function n(e){var t=function(t,n,a,r){n.html("<div></div>"),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(21),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(22),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(55),i=a(r),o=n(53),l=a(o),s=n(56),u=a(s),d=n(52),c=a(d),m=n(57),g=a(m);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",i.default).directive("tbEnrichmentNodeDeviceAttributesConfig",l.default).directive("tbEnrichmentNodeRelatedAttributesConfig",u.default).directive("tbEnrichmentNodeCustomerAttributesConfig",c.default).directive("tbEnrichmentNodeTenantAttributesConfig",g.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(23),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(24),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(25),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(60),i=a(r),o=n(59),l=a(o),s=n(61),u=a(s);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",i.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",u.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){function s(){if(l.$viewValue){for(var e=[],t=0;t<a.messageTypes.length;t++)e.push(a.messageTypes[t].value);l.$viewValue.messageTypes=e,u()}}function u(){if(a.required){var e=!(!l.$viewValue.messageTypes||!l.$viewValue.messageTypes.length);l.$setValidity("messageTypes",e)}else l.$setValidity("messageTypes",!0)}var d=o.default;r.html(d),a.selectedMessageType=null,a.messageTypeSearchText=null,a.ngModelCtrl=l;var c=[];for(var m in n.messageType){var g={name:n.messageType[m].name,value:n.messageType[m].value};c.push(g)}a.transformMessageTypeChip=function(e){var n,a=t("filter")(c,{name:e},!0);return n=a&&a.length?angular.copy(a[0]):{name:e,value:e}},a.messageTypesSearch=function(e){var n=e?t("filter")(c,{name:e}):c;return n.map(function(e){return e.name})},a.createMessageType=function(e,t){var n=angular.element(t,r)[0].firstElementChild,a=angular.element(n),i=a.scope().$mdChipsCtrl.getChipBuffer();e.preventDefault(),e.stopPropagation(),a.scope().$mdChipsCtrl.appendChip(i.trim()),a.scope().$mdChipsCtrl.resetChipBuffer()},l.$render=function(){var e=l.$viewValue,t=[];if(e&&e.messageTypes)for(var r=0;r<e.messageTypes.length;r++){var i=e.messageTypes[r];n.messageType[i]?t.push(angular.copy(n.messageType[i])):t.push({name:i,value:i})}a.messageTypes=t,a.$watch("messageTypes",function(e,t){angular.equals(e,t)||s()},!0)},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",readonly:"=ngReadonly"},link:a}}r.$inject=["$compile","$filter","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(3);var i=n(26),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"filter",t.instant("tb.rulenode.filter")+"","Filter",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(27),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"switch",t.instant("tb.rulenode.switch")+"","Switch",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(28),o=a(i)},function(e,t,n){
+"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){function i(e){e>-1&&t.kvList.splice(e,1)}function l(){t.kvList||(t.kvList=[]),t.kvList.push({key:"",value:""})}function s(){var e={};t.kvList.forEach(function(t){t.key&&(e[t.key]=t.value)}),r.$setViewValue(e),u()}function u(){var e=!0;t.required&&!t.kvList.length&&(e=!1),r.$setValidity("kvMap",e)}var d=o.default;n.html(d),t.ngModelCtrl=r,t.removeKeyVal=i,t.addKeyVal=l,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||r.$setViewValue(t.query)}),r.$render=function(){if(r.$viewValue){var e=r.$viewValue;t.kvList.length=0;for(var n in e)t.kvList.push({key:n,value:e[n]})}t.$watch("kvList",function(e,t){angular.equals(e,t)||s()},!0),u()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(29),o=a(i);n(4)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(30),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(31),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(64),i=a(r),o=n(66),l=a(o),s=n(67),u=a(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",i.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",u.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(32),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(33),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(71),i=a(r),o=n(58),l=a(o),s=n(54),u=a(s),d=n(65),c=a(d),m=n(38),g=a(m),p=n(51),f=a(p),b=n(63),v=a(b),y=n(50),q=a(y),T=n(62),h=a(T),$=n(70),k=a($);t.default=angular.module("thingsboard.ruleChain.config",[i.default,l.default,u.default,c.default,g.default]).directive("tbNodeEmptyConfig",f.default).directive("tbRelationsQueryConfig",v.default).directive("tbDeviceRelationsQueryConfig",q.default).directive("tbKvMapConfig",h.default).config(k.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","latest-timeseries":"Latest timeseries","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","from-template-hint":"From address template, use <code>${metaKeyName}</code> to substitute variables from metadata","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":"Comma separated address list, use <code>${metaKeyName}</code> to substitute variables from metadata","cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","subject-template-hint":"Mail subject template, use <code>${metaKeyName}</code> to substitute variables from metadata","body-template":"Body Template","body-template-required":"Body Template is required","body-template-hint":"Mail body template, use <code>${metaKeyName}</code> to substitute variables from metadata","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","endpoint-url-pattern-hint":"HTTP URL address pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","request-method":"Request method",headers:"Headers","headers-hint":"Use <code>${metaKeyName}</code> in header/value fields to substitute variables from metadata",header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required","mqtt-topic-pattern-hint":"MQTT topic pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","topic-arn-pattern-hint":"Topic ARN pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","queue-url-pattern-hint":"Queue URL pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","message-attributes":"Message attributes","message-attributes-hint":"Use <code>${metaKeyName}</code> in name/value fields to substitute variables from metadata","connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"CA certificate file *","private-key":"Private key file *",cert:"Certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","smtp-protocol":"Protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS"},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};angular.merge(e.en_US,t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){(0,o.default)(t);for(var n in t){var a=t[n];e.translations(n,a)}}r.$inject=["$translateProvider","locales"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(69),o=a(i)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{messageType:{POST_ATTRIBUTES_REQUEST:{name:"Post attributes",value:"POST_ATTRIBUTES_REQUEST"},POST_TELEMETRY_REQUEST:{name:"Post telemetry",value:"POST_TELEMETRY_REQUEST"},TO_SERVER_RPC_REQUEST:{name:"RPC Request",value:"TO_SERVER_RPC_REQUEST"},ACTIVITY_EVENT:{name:"Activity Event",value:"ACTIVITY_EVENT"},INACTIVITY_EVENT:{name:"Inactivity Event",value:"INACTIVITY_EVENT"},CONNECT_EVENT:{name:"Connect Event",value:"CONNECT_EVENT"},DISCONNECT_EVENT:{name:"Disconnect Event",value:"DISCONNECT_EVENT"},ENTITY_CREATED:{name:"Entity Created",value:"ENTITY_CREATED"},ENTITY_UPDATED:{name:"Entity Updated",value:"ENTITY_UPDATED"},ENTITY_DELETED:{name:"Entity Deleted",value:"ENTITY_DELETED"},ENTITY_ASSIGNED:{name:"Entity Assigned",value:"ENTITY_ASSIGNED"},ENTITY_UNASSIGNED:{name:"Entity Unassigned",value:"ENTITY_UNASSIGNED"},ATTRIBUTES_UPDATED:{name:"Attributes Updated",value:"ATTRIBUTES_UPDATED"},ATTRIBUTES_DELETED:{name:"Attributes Deleted",value:"ATTRIBUTES_DELETED"}},originatorSource:{CUSTOMER:{name:"tb.rulenode.originator-customer",value:"CUSTOMER"},TENANT:{name:"tb.rulenode.originator-tenant",value:"TENANT"},RELATED:{name:"tb.rulenode.originator-related",value:"RELATED"}},httpRequestType:["GET","POST","PUT","DELETE"],sqsQueueType:{STANDARD:{name:"tb.rulenode.sqs-queue-standard",value:"STANDARD"},FIFO:{name:"tb.rulenode.sqs-queue-fifo",value:"FIFO"}},mqttCredentialTypes:{anonymous:{value:"anonymous",name:"tb.rulenode.credentials-anonymous"},basic:{value:"basic",name:"tb.rulenode.credentials-basic"},"cert.PEM":{value:"cert.PEM",name:"tb.rulenode.credentials-pem"}}}).name}]));
+//# sourceMappingURL=rulenode-core-config.js.map
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java
new file mode 100644
index 0000000..aeda2de
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java
@@ -0,0 +1,378 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.action;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.apache.commons.lang3.NotImplementedException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.alarm.AlarmService;
+
+import javax.script.ScriptException;
+import java.io.IOException;
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.*;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.*;
+import static org.thingsboard.rule.engine.action.TbAbstractAlarmNode.*;
+import static org.thingsboard.server.common.data.alarm.AlarmSeverity.CRITICAL;
+import static org.thingsboard.server.common.data.alarm.AlarmSeverity.WARNING;
+import static org.thingsboard.server.common.data.alarm.AlarmStatus.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbAlarmNodeTest {
+
+ private TbAbstractAlarmNode node;
+
+ @Mock
+ private TbContext ctx;
+ @Mock
+ private ListeningExecutor executor;
+ @Mock
+ private AlarmService alarmService;
+
+ @Mock
+ private ScriptEngine detailsJs;
+
+ private RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+ private RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+
+ private ListeningExecutor dbExecutor;
+
+ private EntityId originator = new DeviceId(UUIDs.timeBased());
+ private TenantId tenantId = new TenantId(UUIDs.timeBased());
+ private TbMsgMetaData metaData = new TbMsgMetaData();
+ private String rawJson = "{\"name\": \"Vit\", \"passed\": 5}";
+
+ @Before
+ public void before() {
+ dbExecutor = new ListeningExecutor() {
+ @Override
+ public <T> ListenableFuture<T> executeAsync(Callable<T> task) {
+ try {
+ return Futures.immediateFuture(task.call());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+ };
+ }
+
+ @Test
+ public void newAlarmCanBeCreated() throws ScriptException, IOException {
+ initWithCreateAlarmScript();
+ metaData.putValue("key", "value");
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+
+ when(detailsJs.executeJson(msg)).thenReturn(null);
+ when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null));
+
+ doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class));
+
+ node.onMsg(ctx, msg);
+
+ verify(ctx).tellNext(any(), eq("Created"));
+
+ ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
+ ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
+ ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
+ ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
+ verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
+
+ assertEquals("ALARM", typeCaptor.getValue());
+ assertEquals(originator, originatorCaptor.getValue());
+ assertEquals("value", metadataCaptor.getValue().getValue("key"));
+ assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_NEW_ALARM));
+ assertNotSame(metaData, metadataCaptor.getValue());
+
+ Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class);
+ Alarm expectedAlarm = Alarm.builder()
+ .tenantId(tenantId)
+ .originator(originator)
+ .status(ACTIVE_UNACK)
+ .severity(CRITICAL)
+ .propagate(true)
+ .type("SomeType")
+ .details(null)
+ .build();
+
+ assertEquals(expectedAlarm, actualAlarm);
+
+ verify(executor, times(1)).executeAsync(any(Callable.class));
+ }
+
+ @Test
+ public void buildDetailsThrowsException() throws ScriptException, IOException {
+ initWithCreateAlarmScript();
+ metaData.putValue("key", "value");
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+
+ when(detailsJs.executeJson(msg)).thenThrow(new NotImplementedException("message"));
+ when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null));
+
+ node.onMsg(ctx, msg);
+
+ verifyError(msg, "message", NotImplementedException.class);
+
+ verify(ctx).createJsScriptEngine("DETAILS");
+ verify(ctx, times(1)).getJsExecutor();
+ verify(ctx).getAlarmService();
+ verify(ctx, times(3)).getDbCallbackExecutor();
+ verify(ctx).getTenantId();
+ verify(alarmService).findLatestByOriginatorAndType(tenantId, originator, "SomeType");
+
+ verifyNoMoreInteractions(ctx, alarmService);
+ }
+
+ @Test
+ public void ifAlarmClearedCreateNew() throws ScriptException, IOException {
+ initWithCreateAlarmScript();
+ metaData.putValue("key", "value");
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+
+ Alarm clearedAlarm = Alarm.builder().status(CLEARED_ACK).build();
+
+ when(detailsJs.executeJson(msg)).thenReturn(null);
+ when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(clearedAlarm));
+
+ doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class));
+
+ node.onMsg(ctx, msg);
+
+ verify(ctx).tellNext(any(), eq("Created"));
+
+ ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
+ ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
+ ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
+ ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
+ verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
+
+ assertEquals("ALARM", typeCaptor.getValue());
+ assertEquals(originator, originatorCaptor.getValue());
+ assertEquals("value", metadataCaptor.getValue().getValue("key"));
+ assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_NEW_ALARM));
+ assertNotSame(metaData, metadataCaptor.getValue());
+
+
+ Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class);
+ Alarm expectedAlarm = Alarm.builder()
+ .tenantId(tenantId)
+ .originator(originator)
+ .status(ACTIVE_UNACK)
+ .severity(CRITICAL)
+ .propagate(true)
+ .type("SomeType")
+ .details(null)
+ .build();
+
+ assertEquals(expectedAlarm, actualAlarm);
+
+ verify(executor, times(1)).executeAsync(any(Callable.class));
+ }
+
+ @Test
+ public void alarmCanBeUpdated() throws ScriptException, IOException {
+ initWithCreateAlarmScript();
+ metaData.putValue("key", "value");
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+
+ long oldEndDate = System.currentTimeMillis();
+ Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build();
+
+ when(detailsJs.executeJson(msg)).thenReturn(null);
+ when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm));
+
+ doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm);
+
+ node.onMsg(ctx, msg);
+
+ verify(ctx).tellNext(any(), eq("Updated"));
+
+ ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
+ ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
+ ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
+ ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
+ verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
+
+ assertEquals("ALARM", typeCaptor.getValue());
+ assertEquals(originator, originatorCaptor.getValue());
+ assertEquals("value", metadataCaptor.getValue().getValue("key"));
+ assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_EXISTING_ALARM));
+ assertNotSame(metaData, metadataCaptor.getValue());
+
+ Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class);
+ assertTrue(activeAlarm.getEndTs() > oldEndDate);
+ Alarm expectedAlarm = Alarm.builder()
+ .tenantId(tenantId)
+ .originator(originator)
+ .status(ACTIVE_UNACK)
+ .severity(CRITICAL)
+ .propagate(true)
+ .type("SomeType")
+ .details(null)
+ .endTs(activeAlarm.getEndTs())
+ .build();
+
+ assertEquals(expectedAlarm, actualAlarm);
+
+ verify(executor, times(1)).executeAsync(any(Callable.class));
+ }
+
+ @Test
+ public void alarmCanBeCleared() throws ScriptException, IOException {
+ initWithClearAlarmScript();
+ metaData.putValue("key", "value");
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+
+ long oldEndDate = System.currentTimeMillis();
+ Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build();
+
+// when(detailsJs.executeJson(msg)).thenReturn(null);
+ when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm));
+ when(alarmService.clearAlarm(eq(activeAlarm.getId()), org.mockito.Mockito.any(JsonNode.class), anyLong())).thenReturn(Futures.immediateFuture(true));
+// doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm);
+
+ node.onMsg(ctx, msg);
+
+ verify(ctx).tellNext(any(), eq("Cleared"));
+
+ ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
+ ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
+ ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
+ ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
+ verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
+
+ assertEquals("ALARM", typeCaptor.getValue());
+ assertEquals(originator, originatorCaptor.getValue());
+ assertEquals("value", metadataCaptor.getValue().getValue("key"));
+ assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_CLEARED_ALARM));
+ assertNotSame(metaData, metadataCaptor.getValue());
+
+ Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class);
+ Alarm expectedAlarm = Alarm.builder()
+ .tenantId(tenantId)
+ .originator(originator)
+ .status(CLEARED_UNACK)
+ .severity(WARNING)
+ .propagate(false)
+ .type("SomeType")
+ .details(null)
+ .endTs(oldEndDate)
+ .build();
+
+ assertEquals(expectedAlarm, actualAlarm);
+ }
+
+ private void initWithCreateAlarmScript() {
+ try {
+ TbCreateAlarmNodeConfiguration config = new TbCreateAlarmNodeConfiguration();
+ config.setPropagate(true);
+ config.setSeverity(CRITICAL);
+ config.setAlarmType("SomeType");
+ config.setAlarmDetailsBuildJs("DETAILS");
+ ObjectMapper mapper = new ObjectMapper();
+ TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+ when(ctx.createJsScriptEngine("DETAILS")).thenReturn(detailsJs);
+
+ when(ctx.getTenantId()).thenReturn(tenantId);
+ when(ctx.getJsExecutor()).thenReturn(executor);
+ when(ctx.getAlarmService()).thenReturn(alarmService);
+ when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor);
+
+ mockJsExecutor();
+
+ node = new TbCreateAlarmNode();
+ node.init(ctx, nodeConfiguration);
+ } catch (TbNodeException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ private void initWithClearAlarmScript() {
+ try {
+ TbClearAlarmNodeConfiguration config = new TbClearAlarmNodeConfiguration();
+ config.setAlarmType("SomeType");
+ config.setAlarmDetailsBuildJs("DETAILS");
+ ObjectMapper mapper = new ObjectMapper();
+ TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+ when(ctx.createJsScriptEngine("DETAILS")).thenReturn(detailsJs);
+
+ when(ctx.getTenantId()).thenReturn(tenantId);
+ when(ctx.getJsExecutor()).thenReturn(executor);
+ when(ctx.getAlarmService()).thenReturn(alarmService);
+ when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor);
+
+ mockJsExecutor();
+
+ node = new TbClearAlarmNode();
+ node.init(ctx, nodeConfiguration);
+ } catch (TbNodeException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ private void mockJsExecutor() {
+ when(ctx.getJsExecutor()).thenReturn(executor);
+ doAnswer((Answer<ListenableFuture<Boolean>>) invocationOnMock -> {
+ try {
+ Callable task = (Callable) (invocationOnMock.getArguments())[0];
+ return Futures.immediateFuture((Boolean) task.call());
+ } catch (Throwable th) {
+ return Futures.immediateFailedFuture(th);
+ }
+ }).when(executor).executeAsync(any(Callable.class));
+ }
+
+ private void verifyError(TbMsg msg, String message, Class expectedClass) {
+ ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+ verify(ctx).tellFailure(same(msg), captor.capture());
+
+ Throwable value = captor.getValue();
+ assertEquals(expectedClass, value.getClass());
+ assertEquals(message, value.getMessage());
+ }
+
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java
new file mode 100644
index 0000000..b3e097b
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java
@@ -0,0 +1,126 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.filter;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbJsFilterNodeTest {
+
+ private TbJsFilterNode node;
+
+ @Mock
+ private TbContext ctx;
+ @Mock
+ private ListeningExecutor executor;
+ @Mock
+ private ScriptEngine scriptEngine;
+
+ private RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+ private RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+
+ @Test
+ public void falseEvaluationDoNotSendMsg() throws TbNodeException, ScriptException {
+ initWithScript();
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+ mockJsExecutor();
+ when(scriptEngine.executeFilter(msg)).thenReturn(false);
+
+ node.onMsg(ctx, msg);
+ verify(ctx).getJsExecutor();
+ verify(ctx).tellNext(msg, "False");
+ }
+
+ @Test
+ public void exceptionInJsThrowsException() throws TbNodeException, ScriptException {
+ initWithScript();
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}", ruleChainId, ruleNodeId, 0L);
+ mockJsExecutor();
+ when(scriptEngine.executeFilter(msg)).thenThrow(new ScriptException("error"));
+
+
+ node.onMsg(ctx, msg);
+ verifyError(msg, "error", ScriptException.class);
+ }
+
+ @Test
+ public void metadataConditionCanBeTrue() throws TbNodeException, ScriptException {
+ initWithScript();
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}", ruleChainId, ruleNodeId, 0L);
+ mockJsExecutor();
+ when(scriptEngine.executeFilter(msg)).thenReturn(true);
+
+ node.onMsg(ctx, msg);
+ verify(ctx).getJsExecutor();
+ verify(ctx).tellNext(msg, "True");
+ }
+
+ private void initWithScript() throws TbNodeException {
+ TbJsFilterNodeConfiguration config = new TbJsFilterNodeConfiguration();
+ config.setJsScript("scr");
+ ObjectMapper mapper = new ObjectMapper();
+ TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+ when(ctx.createJsScriptEngine("scr")).thenReturn(scriptEngine);
+
+ node = new TbJsFilterNode();
+ node.init(ctx, nodeConfiguration);
+ }
+
+ private void mockJsExecutor() {
+ when(ctx.getJsExecutor()).thenReturn(executor);
+ doAnswer((Answer<ListenableFuture<Boolean>>) invocationOnMock -> {
+ try {
+ Callable task = (Callable) (invocationOnMock.getArguments())[0];
+ return Futures.immediateFuture((Boolean) task.call());
+ } catch (Throwable th) {
+ return Futures.immediateFailedFuture(th);
+ }
+ }).when(executor).executeAsync(Matchers.any(Callable.class));
+ }
+
+ private void verifyError(TbMsg msg, String message, Class expectedClass) {
+ ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+ verify(ctx).tellFailure(same(msg), captor.capture());
+
+ Throwable value = captor.getValue();
+ assertEquals(expectedClass, value.getClass());
+ assertEquals(message, value.getMessage());
+ }
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java
new file mode 100644
index 0000000..f547807
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java
@@ -0,0 +1,108 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.filter;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbJsSwitchNodeTest {
+
+ private TbJsSwitchNode node;
+
+ @Mock
+ private TbContext ctx;
+ @Mock
+ private ListeningExecutor executor;
+ @Mock
+ private ScriptEngine scriptEngine;
+
+ private RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+ private RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+
+ @Test
+ public void multipleRoutesAreAllowed() throws TbNodeException, ScriptException {
+ initWithScript();
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "10");
+ metaData.putValue("humidity", "99");
+ String rawJson = "{\"name\": \"Vit\", \"passed\": 5}";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+ mockJsExecutor();
+ when(scriptEngine.executeSwitch(msg)).thenReturn(Sets.newHashSet("one", "three"));
+
+ node.onMsg(ctx, msg);
+ verify(ctx).getJsExecutor();
+ verify(ctx).tellNext(msg, Sets.newHashSet("one", "three"));
+ }
+
+ private void initWithScript() throws TbNodeException {
+ TbJsSwitchNodeConfiguration config = new TbJsSwitchNodeConfiguration();
+ config.setJsScript("scr");
+ ObjectMapper mapper = new ObjectMapper();
+ TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+ when(ctx.createJsScriptEngine("scr")).thenReturn(scriptEngine);
+
+ node = new TbJsSwitchNode();
+ node.init(ctx, nodeConfiguration);
+ }
+
+ private void mockJsExecutor() {
+ when(ctx.getJsExecutor()).thenReturn(executor);
+ doAnswer((Answer<ListenableFuture<Set<String>>>) invocationOnMock -> {
+ try {
+ Callable task = (Callable) (invocationOnMock.getArguments())[0];
+ return Futures.immediateFuture((Set<String>) task.call());
+ } catch (Throwable th) {
+ return Futures.immediateFailedFuture(th);
+ }
+ }).when(executor).executeAsync(Matchers.any(Callable.class));
+ }
+
+ private void verifyError(TbMsg msg, String message, Class expectedClass) {
+ ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+ verify(ctx).tellFailure(same(msg), captor.capture());
+
+ Throwable value = captor.getValue();
+ assertEquals(expectedClass, value.getClass());
+ assertEquals(message, value.getMessage());
+ }
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java
new file mode 100644
index 0000000..19d4a49
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java
@@ -0,0 +1,110 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.mail;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.verify;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbMsgToEmailNodeTest {
+
+ private TbMsgToEmailNode emailNode;
+
+ @Mock
+ private TbContext ctx;
+
+ private EntityId originator = new DeviceId(UUIDs.timeBased());
+ private TbMsgMetaData metaData = new TbMsgMetaData();
+ private String rawJson = "{\"name\": \"temp\", \"passed\": 5 , \"complex\": {\"val\":12, \"count\":100}}";
+
+ private RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+ private RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+
+ @Test
+ public void msgCanBeConverted() throws IOException {
+ initWithScript();
+ metaData.putValue("username", "oreo");
+ metaData.putValue("userEmail", "user@email.io");
+ metaData.putValue("name", "temp");
+ metaData.putValue("passed", "5");
+ metaData.putValue("count", "100");
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+
+ emailNode.onMsg(ctx, msg);
+
+ ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
+ ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
+ ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
+ ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
+ verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
+
+
+ assertEquals("SEND_EMAIL", typeCaptor.getValue());
+ assertEquals(originator, originatorCaptor.getValue());
+ assertEquals("oreo", metadataCaptor.getValue().getValue("username"));
+ assertNotSame(metaData, metadataCaptor.getValue());
+
+ EmailPojo actual = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), EmailPojo.class);
+
+ EmailPojo expected = new EmailPojo.EmailPojoBuilder()
+ .from("test@mail.org")
+ .to("user@email.io")
+ .subject("Hi oreo there")
+ .body("temp is to high. Current 5 and 100")
+ .build();
+ assertEquals(expected, actual);
+ }
+
+ private void initWithScript() {
+ try {
+ TbMsgToEmailNodeConfiguration config = new TbMsgToEmailNodeConfiguration();
+ config.setFromTemplate("test@mail.org");
+ config.setToTemplate("${userEmail}");
+ config.setSubjectTemplate("Hi ${username} there");
+ config.setBodyTemplate("${name} is to high. Current ${passed} and ${count}");
+ ObjectMapper mapper = new ObjectMapper();
+ TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+ emailNode = new TbMsgToEmailNode();
+ emailNode.init(ctx, nodeConfiguration);
+ } catch (TbNodeException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java
new file mode 100644
index 0000000..4a855ae
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java
@@ -0,0 +1,262 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Futures;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.dao.user.UserService;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbGetCustomerAttributeNodeTest {
+
+ private TbGetCustomerAttributeNode node;
+
+ @Mock
+ private TbContext ctx;
+
+ @Mock
+ private AttributesService attributesService;
+ @Mock
+ private TimeseriesService timeseriesService;
+ @Mock
+ private UserService userService;
+ @Mock
+ private AssetService assetService;
+ @Mock
+ private DeviceService deviceService;
+
+ private TbMsg msg;
+
+ private RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+ private RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+
+ @Before
+ public void init() throws TbNodeException {
+ TbGetEntityAttrNodeConfiguration config = new TbGetEntityAttrNodeConfiguration();
+ Map<String, String> attrMapping = new HashMap<>();
+ attrMapping.putIfAbsent("temperature", "tempo");
+ config.setAttrMapping(attrMapping);
+ config.setTelemetry(false);
+ ObjectMapper mapper = new ObjectMapper();
+ TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+ node = new TbGetCustomerAttributeNode();
+ node.init(null, nodeConfiguration);
+ }
+
+ @Test
+ public void errorThrownIfCannotLoadAttributes() {
+ UserId userId = new UserId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ User user = new User();
+ user.setCustomerId(customerId);
+
+ msg = new TbMsg(UUIDs.timeBased(), "USER", userId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+ when(ctx.getUserService()).thenReturn(userService);
+ when(userService.findUserByIdAsync(userId)).thenReturn(Futures.immediateFuture(user));
+
+ when(ctx.getAttributesService()).thenReturn(attributesService);
+ when(attributesService.find(customerId, SERVER_SCOPE, Collections.singleton("temperature")))
+ .thenThrow(new IllegalStateException("something wrong"));
+
+ node.onMsg(ctx, msg);
+ final ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+ verify(ctx).tellFailure(same(msg), captor.capture());
+
+ Throwable value = captor.getValue();
+ assertEquals("something wrong", value.getMessage());
+ assertTrue(msg.getMetaData().getData().isEmpty());
+ }
+
+ @Test
+ public void errorThrownIfCannotLoadAttributesAsync() {
+ UserId userId = new UserId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ User user = new User();
+ user.setCustomerId(customerId);
+
+ msg = new TbMsg(UUIDs.timeBased(), "USER", userId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+ when(ctx.getUserService()).thenReturn(userService);
+ when(userService.findUserByIdAsync(userId)).thenReturn(Futures.immediateFuture(user));
+
+ when(ctx.getAttributesService()).thenReturn(attributesService);
+ when(attributesService.find(customerId, SERVER_SCOPE, Collections.singleton("temperature")))
+ .thenReturn(Futures.immediateFailedFuture(new IllegalStateException("something wrong")));
+
+ node.onMsg(ctx, msg);
+ final ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+ verify(ctx).tellFailure(same(msg), captor.capture());
+
+ Throwable value = captor.getValue();
+ assertEquals("something wrong", value.getMessage());
+ assertTrue(msg.getMetaData().getData().isEmpty());
+ }
+
+ @Test
+ public void failedChainUsedIfCustomerCannotBeFound() {
+ UserId userId = new UserId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ User user = new User();
+ user.setCustomerId(customerId);
+
+ msg = new TbMsg(UUIDs.timeBased(), "USER", userId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+ when(ctx.getUserService()).thenReturn(userService);
+ when(userService.findUserByIdAsync(userId)).thenReturn(Futures.immediateFuture(null));
+
+
+ node.onMsg(ctx, msg);
+ verify(ctx).tellNext(msg, FAILURE);
+ assertTrue(msg.getMetaData().getData().isEmpty());
+ }
+
+ @Test
+ public void customerAttributeAddedInMetadata() {
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ msg = new TbMsg(UUIDs.timeBased(), "CUSTOMER", customerId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+ entityAttributeFetched(customerId);
+ }
+
+ @Test
+ public void usersCustomerAttributesFetched() {
+ UserId userId = new UserId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ User user = new User();
+ user.setCustomerId(customerId);
+
+ msg = new TbMsg(UUIDs.timeBased(), "USER", userId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+ when(ctx.getUserService()).thenReturn(userService);
+ when(userService.findUserByIdAsync(userId)).thenReturn(Futures.immediateFuture(user));
+
+ entityAttributeFetched(customerId);
+ }
+
+ @Test
+ public void assetsCustomerAttributesFetched() {
+ AssetId assetId = new AssetId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ Asset asset = new Asset();
+ asset.setCustomerId(customerId);
+
+ msg = new TbMsg(UUIDs.timeBased(), "USER", assetId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+ when(ctx.getAssetService()).thenReturn(assetService);
+ when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFuture(asset));
+
+ entityAttributeFetched(customerId);
+ }
+
+ @Test
+ public void deviceCustomerAttributesFetched() {
+ DeviceId deviceId = new DeviceId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ Device device = new Device();
+ device.setCustomerId(customerId);
+
+ msg = new TbMsg(UUIDs.timeBased(), "USER", deviceId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+ when(ctx.getDeviceService()).thenReturn(deviceService);
+ when(deviceService.findDeviceByIdAsync(deviceId)).thenReturn(Futures.immediateFuture(device));
+
+ entityAttributeFetched(customerId);
+ }
+
+ @Test
+ public void deviceCustomerTelemetryFetched() throws TbNodeException {
+ TbGetEntityAttrNodeConfiguration config = new TbGetEntityAttrNodeConfiguration();
+ Map<String, String> attrMapping = new HashMap<>();
+ attrMapping.putIfAbsent("temperature", "tempo");
+ config.setAttrMapping(attrMapping);
+ config.setTelemetry(true);
+ ObjectMapper mapper = new ObjectMapper();
+ TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+ node = new TbGetCustomerAttributeNode();
+ node.init(null, nodeConfiguration);
+
+
+ DeviceId deviceId = new DeviceId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ Device device = new Device();
+ device.setCustomerId(customerId);
+
+ msg = new TbMsg(UUIDs.timeBased(), "USER", deviceId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+ when(ctx.getDeviceService()).thenReturn(deviceService);
+ when(deviceService.findDeviceByIdAsync(deviceId)).thenReturn(Futures.immediateFuture(device));
+
+ List<TsKvEntry> timeseries = Lists.newArrayList(new BasicTsKvEntry(1L, new StringDataEntry("temperature", "highest")));
+
+ when(ctx.getTimeseriesService()).thenReturn(timeseriesService);
+ when(timeseriesService.findLatest(customerId, Collections.singleton("temperature")))
+ .thenReturn(Futures.immediateFuture(timeseries));
+
+ node.onMsg(ctx, msg);
+ verify(ctx).tellNext(msg, SUCCESS);
+ assertEquals(msg.getMetaData().getValue("tempo"), "highest");
+ }
+
+ private void entityAttributeFetched(CustomerId customerId) {
+ List<AttributeKvEntry> attributes = Lists.newArrayList(new BaseAttributeKvEntry(new StringDataEntry("temperature", "high"), 1L));
+
+ when(ctx.getAttributesService()).thenReturn(attributesService);
+ when(attributesService.find(customerId, SERVER_SCOPE, Collections.singleton("temperature")))
+ .thenReturn(Futures.immediateFuture(attributes));
+
+ node.onMsg(ctx, msg);
+ verify(ctx).tellNext(msg, SUCCESS);
+ assertEquals(msg.getMetaData().getValue("tempo"), "high");
+ }
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java
new file mode 100644
index 0000000..ce0afc9
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java
@@ -0,0 +1,167 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.transform;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.thingsboard.rule.engine.api.ListeningExecutor;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.asset.AssetService;
+
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbChangeOriginatorNodeTest {
+
+ private TbChangeOriginatorNode node;
+
+ @Mock
+ private TbContext ctx;
+ @Mock
+ private AssetService assetService;
+
+ private ListeningExecutor dbExecutor;
+
+ @Before
+ public void before() {
+ dbExecutor = new ListeningExecutor() {
+ @Override
+ public <T> ListenableFuture<T> executeAsync(Callable<T> task) {
+ try {
+ return Futures.immediateFuture(task.call());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+ };
+ }
+
+ @Test
+ public void originatorCanBeChangedToCustomerId() throws TbNodeException {
+ init();
+ AssetId assetId = new AssetId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ Asset asset = new Asset();
+ asset.setCustomerId(customerId);
+
+ RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+ RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "ASSET", assetId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+ when(ctx.getAssetService()).thenReturn(assetService);
+ when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFuture(asset));
+
+ node.onMsg(ctx, msg);
+
+ ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
+ ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
+ ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
+ ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
+ verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
+
+ assertEquals(customerId, originatorCaptor.getValue());
+ }
+
+ @Test
+ public void newChainCanBeStarted() throws TbNodeException {
+ init();
+ AssetId assetId = new AssetId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ Asset asset = new Asset();
+ asset.setCustomerId(customerId);
+
+ RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+ RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "ASSET", assetId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+ when(ctx.getAssetService()).thenReturn(assetService);
+ when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFuture(asset));
+
+ node.onMsg(ctx, msg);
+ ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
+ ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
+ ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
+ ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
+ verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
+
+ assertEquals(customerId, originatorCaptor.getValue());
+ }
+
+ @Test
+ public void exceptionThrownIfCannotFindNewOriginator() throws TbNodeException {
+ init();
+ AssetId assetId = new AssetId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ Asset asset = new Asset();
+ asset.setCustomerId(customerId);
+
+ RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+ RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "ASSET", assetId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+ when(ctx.getAssetService()).thenReturn(assetService);
+ when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFuture(null));
+
+ node.onMsg(ctx, msg);
+ verify(ctx).tellNext(same(msg), same(FAILURE));
+ }
+
+ public void init() throws TbNodeException {
+ TbChangeOriginatorNodeConfiguration config = new TbChangeOriginatorNodeConfiguration();
+ config.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE);
+ ObjectMapper mapper = new ObjectMapper();
+ TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+ when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor);
+
+ node = new TbChangeOriginatorNode();
+ node.init(null, nodeConfiguration);
+ }
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java
new file mode 100644
index 0000000..1daeb19
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java
@@ -0,0 +1,126 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.transform;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.*;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbTransformMsgNodeTest {
+
+ private TbTransformMsgNode node;
+
+ @Mock
+ private TbContext ctx;
+ @Mock
+ private ListeningExecutor executor;
+ @Mock
+ private ScriptEngine scriptEngine;
+
+ @Test
+ public void metadataCanBeUpdated() throws TbNodeException, ScriptException {
+ initWithScript();
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "7");
+ String rawJson = "{\"passed\": 5}";
+
+ RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+ RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+ TbMsg transformedMsg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{new}", ruleChainId, ruleNodeId, 0L);
+ mockJsExecutor();
+ when(scriptEngine.executeUpdate(msg)).thenReturn(transformedMsg);
+
+ node.onMsg(ctx, msg);
+ verify(ctx).getJsExecutor();
+ ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+ verify(ctx).tellNext(captor.capture(), eq(SUCCESS));
+ TbMsg actualMsg = captor.getValue();
+ assertEquals(transformedMsg, actualMsg);
+ }
+
+ @Test
+ public void exceptionHandledCorrectly() throws TbNodeException, ScriptException {
+ initWithScript();
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "7");
+ String rawJson = "{\"passed\": 5";
+
+ RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+ RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+ mockJsExecutor();
+ when(scriptEngine.executeUpdate(msg)).thenThrow(new IllegalStateException("error"));
+
+ node.onMsg(ctx, msg);
+ verifyError(msg, "error", IllegalStateException.class);
+ }
+
+ private void initWithScript() throws TbNodeException {
+ TbTransformMsgNodeConfiguration config = new TbTransformMsgNodeConfiguration();
+ config.setJsScript("scr");
+ ObjectMapper mapper = new ObjectMapper();
+ TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+ when(ctx.createJsScriptEngine("scr")).thenReturn(scriptEngine);
+
+ node = new TbTransformMsgNode();
+ node.init(ctx, nodeConfiguration);
+ }
+
+ private void mockJsExecutor() {
+ when(ctx.getJsExecutor()).thenReturn(executor);
+ doAnswer((Answer<ListenableFuture<TbMsg>>) invocationOnMock -> {
+ try {
+ Callable task = (Callable) (invocationOnMock.getArguments())[0];
+ return Futures.immediateFuture((TbMsg) task.call());
+ } catch (Throwable th) {
+ return Futures.immediateFailedFuture(th);
+ }
+ }).when(executor).executeAsync(Matchers.any(Callable.class));
+ }
+
+ private void verifyError(TbMsg msg, String message, Class expectedClass) {
+ ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+ verify(ctx).tellFailure(same(msg), captor.capture());
+
+ Throwable value = captor.getValue();
+ assertEquals(expectedClass, value.getClass());
+ assertEquals(message, value.getMessage());
+ }
+}
\ No newline at end of file
tools/pom.xml 2(+1 -1)
diff --git a/tools/pom.xml b/tools/pom.xml
index 112d8f1..c936287 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.4.1-SNAPSHOT</version>
+ <version>2.0.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
transport/coap/pom.xml 2(+1 -1)
diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml
index 52f0357..18e8377 100644
--- a/transport/coap/pom.xml
+++ b/transport/coap/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.4.1-SNAPSHOT</version>
+ <version>2.0.0-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
index e3ef2cc..1c96311 100644
--- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
@@ -28,7 +28,7 @@ import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
import org.thingsboard.server.common.msg.session.BasicAdaptorToSessionActorMsg;
import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
import org.thingsboard.server.common.msg.session.SessionActorToAdaptorMsg;
import org.thingsboard.server.common.msg.session.SessionContext;
import org.thingsboard.server.common.msg.session.ToDeviceMsg;
@@ -48,7 +48,7 @@ import org.thingsboard.server.transport.coap.session.CoapSessionCtx;
public class JsonCoapAdaptor implements CoapTransportAdaptor {
@Override
- public AdaptorToSessionActorMsg convertToActorMsg(CoapSessionCtx ctx, MsgType type, Request inbound) throws AdaptorException {
+ public AdaptorToSessionActorMsg convertToActorMsg(CoapSessionCtx ctx, SessionMsgType type, Request inbound) throws AdaptorException {
FromDeviceMsg msg = null;
switch (type) {
case POST_TELEMETRY_REQUEST:
@@ -104,7 +104,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
@Override
public Optional<Response> convertToAdaptorMsg(CoapSessionCtx ctx, SessionActorToAdaptorMsg source) throws AdaptorException {
ToDeviceMsg msg = source.getMsg();
- switch (msg.getMsgType()) {
+ switch (msg.getSessionMsgType()) {
case STATUS_CODE_RESPONSE:
case TO_DEVICE_RPC_RESPONSE_ACK:
return Optional.of(convertStatusCodeResponse((StatusCodeResponse) msg));
@@ -119,19 +119,19 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
case RULE_ENGINE_ERROR:
return Optional.of(convertToRuleEngineErrorResponse(ctx, (RuleEngineErrorMsg) msg));
default:
- log.warn("[{}] Unsupported msg type: {}!", source.getSessionId(), msg.getMsgType());
- throw new AdaptorException(new IllegalArgumentException("Unsupported msg type: " + msg.getMsgType() + "!"));
+ log.warn("[{}] Unsupported msg type: {}!", source.getSessionId(), msg.getSessionMsgType());
+ throw new AdaptorException(new IllegalArgumentException("Unsupported msg type: " + msg.getSessionMsgType() + "!"));
}
}
private Response convertToRuleEngineErrorResponse(CoapSessionCtx ctx, RuleEngineErrorMsg msg) {
ResponseCode status = ResponseCode.INTERNAL_SERVER_ERROR;
switch (msg.getError()) {
- case PLUGIN_TIMEOUT:
+ case QUEUE_PUT_TIMEOUT:
status = ResponseCode.GATEWAY_TIMEOUT;
break;
default:
- if (msg.getInMsgType() == MsgType.TO_SERVER_RPC_REQUEST) {
+ if (msg.getInSessionMsgType() == SessionMsgType.TO_SERVER_RPC_REQUEST) {
status = ResponseCode.BAD_REQUEST;
}
break;
@@ -156,7 +156,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
return response;
}
- private UpdateAttributesRequest convertToUpdateAttributesRequest(SessionContext ctx, Request inbound) throws AdaptorException {
+ private AttributesUpdateRequest convertToUpdateAttributesRequest(SessionContext ctx, Request inbound) throws AdaptorException {
String payload = validatePayload(ctx, inbound);
try {
return JsonConverter.convertToAttributes(new JsonParser().parse(payload));
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
index 7ba5e36..bac68ff 100644
--- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
@@ -38,8 +38,6 @@ import org.thingsboard.server.common.transport.quota.QuotaService;
import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
import org.thingsboard.server.transport.coap.session.CoapExchangeObserverProxy;
import org.thingsboard.server.transport.coap.session.CoapSessionCtx;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.util.ReflectionUtils;
@Slf4j
@@ -90,7 +88,7 @@ public class CoapTransportResource extends CoapResource {
} else if (exchange.getRequestOptions().hasObserve()) {
processExchangeGetRequest(exchange, featureType.get());
} else if (featureType.get() == FeatureType.ATTRIBUTES) {
- processRequest(exchange, MsgType.GET_ATTRIBUTES_REQUEST);
+ processRequest(exchange, SessionMsgType.GET_ATTRIBUTES_REQUEST);
} else {
log.trace("Invalid feature type parameter");
exchange.respond(ResponseCode.BAD_REQUEST);
@@ -99,13 +97,13 @@ public class CoapTransportResource extends CoapResource {
private void processExchangeGetRequest(CoapExchange exchange, FeatureType featureType) {
boolean unsubscribe = exchange.getRequestOptions().getObserve() == 1;
- MsgType msgType;
+ SessionMsgType sessionMsgType;
if (featureType == FeatureType.RPC) {
- msgType = unsubscribe ? MsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST : MsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST;
+ sessionMsgType = unsubscribe ? SessionMsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST : SessionMsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST;
} else {
- msgType = unsubscribe ? MsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST : MsgType.SUBSCRIBE_ATTRIBUTES_REQUEST;
+ sessionMsgType = unsubscribe ? SessionMsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST : SessionMsgType.SUBSCRIBE_ATTRIBUTES_REQUEST;
}
- Optional<SessionId> sessionId = processRequest(exchange, msgType);
+ Optional<SessionId> sessionId = processRequest(exchange, sessionMsgType);
if (sessionId.isPresent()) {
if (exchange.getRequestOptions().getObserve() == 1) {
exchange.respond(ResponseCode.VALID);
@@ -122,24 +120,24 @@ public class CoapTransportResource extends CoapResource {
} else {
switch (featureType.get()) {
case ATTRIBUTES:
- processRequest(exchange, MsgType.POST_ATTRIBUTES_REQUEST);
+ processRequest(exchange, SessionMsgType.POST_ATTRIBUTES_REQUEST);
break;
case TELEMETRY:
- processRequest(exchange, MsgType.POST_TELEMETRY_REQUEST);
+ processRequest(exchange, SessionMsgType.POST_TELEMETRY_REQUEST);
break;
case RPC:
Optional<Integer> requestId = getRequestId(exchange.advanced().getRequest());
if (requestId.isPresent()) {
- processRequest(exchange, MsgType.TO_DEVICE_RPC_RESPONSE);
+ processRequest(exchange, SessionMsgType.TO_DEVICE_RPC_RESPONSE);
} else {
- processRequest(exchange, MsgType.TO_SERVER_RPC_REQUEST);
+ processRequest(exchange, SessionMsgType.TO_SERVER_RPC_REQUEST);
}
break;
}
}
}
- private Optional<SessionId> processRequest(CoapExchange exchange, MsgType type) {
+ private Optional<SessionId> processRequest(CoapExchange exchange, SessionMsgType type) {
log.trace("Processing {}", exchange.advanced().getRequest());
exchange.accept();
Exchange advanced = exchange.advanced();
@@ -186,7 +184,7 @@ public class CoapTransportResource extends CoapResource {
throw new IllegalArgumentException("Unsupported msg type: " + type);
}
log.trace("Processing msg: {}", msg);
- processor.process(new BasicToDeviceActorSessionMsg(ctx.getDevice(), msg));
+ processor.process(new BasicTransportToDeviceSessionActorMsg(ctx.getDevice(), msg));
} catch (AdaptorException e) {
log.debug("Failed to decode payload {}", e);
exchange.respond(ResponseCode.BAD_REQUEST, e.getMessage());
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
index 15706d4..6c8437c 100644
--- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
@@ -27,6 +27,7 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.common.transport.quota.host.HostRequestsQuotaService;
import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
import javax.annotation.PostConstruct;
@@ -55,7 +56,7 @@ public class CoapTransportService {
private DeviceAuthService authService;
@Autowired(required = false)
- private QuotaService quotaService;
+ private HostRequestsQuotaService quotaService;
@Value("${coap.bind_address}")
diff --git a/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
index 072c735..4815056 100644
--- a/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
+++ b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
@@ -50,7 +50,7 @@ import org.thingsboard.server.common.msg.session.*;
import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.auth.DeviceAuthResult;
import org.thingsboard.server.common.transport.auth.DeviceAuthService;
-import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.common.transport.quota.host.HostRequestsQuotaService;
import java.util.ArrayList;
import java.util.List;
@@ -108,14 +108,14 @@ public class CoapServerTest {
@Override
public void process(SessionAwareMsg toActorMsg) {
- if (toActorMsg instanceof ToDeviceActorSessionMsg) {
- AdaptorToSessionActorMsg sessionMsg = ((ToDeviceActorSessionMsg) toActorMsg).getSessionMsg();
+ if (toActorMsg instanceof TransportToDeviceSessionActorMsg) {
+ AdaptorToSessionActorMsg sessionMsg = ((TransportToDeviceSessionActorMsg) toActorMsg).getSessionMsg();
try {
FromDeviceMsg deviceMsg = sessionMsg.getMsg();
ToDeviceMsg toDeviceMsg = null;
- if (deviceMsg.getMsgType() == MsgType.POST_TELEMETRY_REQUEST) {
+ if (deviceMsg.getMsgType() == SessionMsgType.POST_TELEMETRY_REQUEST) {
toDeviceMsg = BasicStatusCodeResponse.onSuccess(deviceMsg.getMsgType(), BasicRequest.DEFAULT_REQUEST_ID);
- } else if (deviceMsg.getMsgType() == MsgType.GET_ATTRIBUTES_REQUEST) {
+ } else if (deviceMsg.getMsgType() == SessionMsgType.GET_ATTRIBUTES_REQUEST) {
List<AttributeKvEntry> data = new ArrayList<>();
data.add(new BaseAttributeKvEntry(new StringDataEntry("key1", "value1"), System.currentTimeMillis()));
data.add(new BaseAttributeKvEntry(new LongDataEntry("key2", 42L), System.currentTimeMillis()));
@@ -134,8 +134,8 @@ public class CoapServerTest {
}
@Bean
- public static QuotaService quotaService() {
- return key -> false;
+ public static HostRequestsQuotaService quotaService() {
+ return new HostRequestsQuotaService(null, null, null, null, false);
}
}
transport/http/pom.xml 2(+1 -1)
diff --git a/transport/http/pom.xml b/transport/http/pom.xml
index 63bbbe0..0d36ba5 100644
--- a/transport/http/pom.xml
+++ b/transport/http/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.4.1-SNAPSHOT</version>
+ <version>2.0.0-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>
diff --git a/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
index 4d90b5f..d26d076 100644
--- a/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
+++ b/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
@@ -30,12 +30,13 @@ import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
import org.thingsboard.server.common.msg.core.*;
import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
import org.thingsboard.server.common.msg.session.BasicAdaptorToSessionActorMsg;
-import org.thingsboard.server.common.msg.session.BasicToDeviceActorSessionMsg;
+import org.thingsboard.server.common.msg.session.BasicTransportToDeviceSessionActorMsg;
import org.thingsboard.server.common.msg.session.FromDeviceMsg;
import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.common.transport.quota.host.HostRequestsQuotaService;
import org.thingsboard.server.transport.http.session.HttpSessionCtx;
import javax.servlet.http.HttpServletRequest;
@@ -61,7 +62,7 @@ public class DeviceApiController {
private DeviceAuthService authService;
@Autowired(required = false)
- private QuotaService quotaService;
+ private HostRequestsQuotaService quotaService;
@RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.GET, produces = "application/json")
public DeferredResult<ResponseEntity> getDeviceAttributes(@PathVariable("deviceToken") String deviceToken,
@@ -219,7 +220,7 @@ public class DeviceApiController {
private void process(HttpSessionCtx ctx, FromDeviceMsg request) {
AdaptorToSessionActorMsg msg = new BasicAdaptorToSessionActorMsg(ctx, request);
- processor.process(new BasicToDeviceActorSessionMsg(ctx.getDevice(), msg));
+ processor.process(new BasicTransportToDeviceSessionActorMsg(ctx.getDevice(), msg));
}
private boolean quotaExceeded(HttpServletRequest request, DeferredResult<ResponseEntity> responseWriter) {
diff --git a/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionCtx.java b/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionCtx.java
index 743b3e7..4732785 100644
--- a/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionCtx.java
+++ b/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionCtx.java
@@ -57,7 +57,7 @@ public class HttpSessionCtx extends DeviceAwareSessionContext {
@Override
public void onMsg(SessionActorToAdaptorMsg source) throws SessionException {
ToDeviceMsg msg = source.getMsg();
- switch (msg.getMsgType()) {
+ switch (msg.getSessionMsgType()) {
case GET_ATTRIBUTES_RESPONSE:
reply((GetAttributesResponse) msg);
return;
@@ -84,11 +84,11 @@ public class HttpSessionCtx extends DeviceAwareSessionContext {
private void reply(RuleEngineErrorMsg msg) {
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
switch (msg.getError()) {
- case PLUGIN_TIMEOUT:
+ case QUEUE_PUT_TIMEOUT:
status = HttpStatus.REQUEST_TIMEOUT;
break;
default:
- if (msg.getInMsgType() == MsgType.TO_SERVER_RPC_REQUEST) {
+ if (msg.getInSessionMsgType() == SessionMsgType.TO_SERVER_RPC_REQUEST) {
status = HttpStatus.BAD_REQUEST;
}
break;
transport/mqtt/pom.xml 2(+1 -1)
diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml
index a048218..239721d 100644
--- a/transport/mqtt/pom.xml
+++ b/transport/mqtt/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.4.1-SNAPSHOT</version>
+ <version>2.0.0-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
index 64df6bc..f0b29cb 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
@@ -53,7 +53,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
private static final ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false);
@Override
- public AdaptorToSessionActorMsg convertToActorMsg(DeviceSessionCtx ctx, MsgType type, MqttMessage inbound) throws AdaptorException {
+ public AdaptorToSessionActorMsg convertToActorMsg(DeviceSessionCtx ctx, SessionMsgType type, MqttMessage inbound) throws AdaptorException {
FromDeviceMsg msg;
switch (type) {
case POST_TELEMETRY_REQUEST:
@@ -94,7 +94,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
public Optional<MqttMessage> convertToAdaptorMsg(DeviceSessionCtx ctx, SessionActorToAdaptorMsg sessionMsg) throws AdaptorException {
MqttMessage result = null;
ToDeviceMsg msg = sessionMsg.getMsg();
- switch (msg.getMsgType()) {
+ switch (msg.getSessionMsgType()) {
case STATUS_CODE_RESPONSE:
case GET_ATTRIBUTES_RESPONSE:
ResponseMsg<?> responseMsg = (ResponseMsg) msg;
@@ -134,12 +134,12 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
private MqttMessage convertResponseMsg(DeviceSessionCtx ctx, ToDeviceMsg msg,
ResponseMsg<?> responseMsg, Optional<Exception> responseError) throws AdaptorException {
MqttMessage result = null;
- MsgType requestMsgType = responseMsg.getRequestMsgType();
+ SessionMsgType requestMsgType = responseMsg.getRequestMsgType();
Integer requestId = responseMsg.getRequestId();
if (requestId >= 0) {
- if (requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) {
+ if (requestMsgType == SessionMsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == SessionMsgType.POST_TELEMETRY_REQUEST) {
result = MqttTransportHandler.createMqttPubAckMsg(requestId);
- } else if (requestMsgType == MsgType.GET_ATTRIBUTES_REQUEST) {
+ } else if (requestMsgType == SessionMsgType.GET_ATTRIBUTES_REQUEST) {
GetAttributesResponse response = (GetAttributesResponse) msg;
Optional<AttributesKVMsg> responseData = response.getData();
if (response.isSuccess() && responseData.isPresent()) {
@@ -219,7 +219,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
}
}
- private UpdateAttributesRequest convertToUpdateAttributesRequest(SessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
+ private AttributesUpdateRequest convertToUpdateAttributesRequest(SessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
String payload = validatePayload(ctx.getSessionId(), inbound.payload());
try {
return JsonConverter.convertToAttributes(new JsonParser().parse(payload), inbound.variableHeader().messageId());
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
index 8766599..185b7a8 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
@@ -29,8 +29,10 @@ import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
import org.thingsboard.server.common.data.security.DeviceX509Credentials;
+import org.thingsboard.server.common.msg.core.SessionOpenMsg;
import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
-import org.thingsboard.server.common.msg.session.BasicToDeviceActorSessionMsg;
+import org.thingsboard.server.common.msg.session.BasicAdaptorToSessionActorMsg;
+import org.thingsboard.server.common.msg.session.BasicTransportToDeviceSessionActorMsg;
import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
@@ -53,7 +55,7 @@ import java.util.List;
import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.*;
import static io.netty.handler.codec.mqtt.MqttMessageType.*;
import static io.netty.handler.codec.mqtt.MqttQoS.*;
-import static org.thingsboard.server.common.msg.session.MsgType.*;
+import static org.thingsboard.server.common.msg.session.SessionMsgType.*;
import static org.thingsboard.server.transport.mqtt.MqttTopics.*;
/**
@@ -95,6 +97,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
log.trace("[{}] Processing msg: {}", sessionId, msg);
if (msg instanceof MqttMessage) {
processMqttMsg(ctx, (MqttMessage) msg);
+ } else {
+ ctx.close();
}
}
@@ -207,7 +211,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e);
}
if (msg != null) {
- processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), msg));
+ processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(), msg));
} else {
log.info("[{}] Closing current session due to invalid publish msg [{}][{}]", sessionId, topicName, msgId);
ctx.close();
@@ -227,11 +231,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
try {
if (topicName.equals(DEVICE_ATTRIBUTES_TOPIC)) {
AdaptorToSessionActorMsg msg = adaptor.convertToActorMsg(deviceSessionCtx, SUBSCRIBE_ATTRIBUTES_REQUEST, mqttMsg);
- processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), msg));
+ processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(), msg));
grantedQoSList.add(getMinSupportedQos(reqQoS));
} else if (topicName.equals(DEVICE_RPC_REQUESTS_SUB_TOPIC)) {
AdaptorToSessionActorMsg msg = adaptor.convertToActorMsg(deviceSessionCtx, SUBSCRIBE_RPC_COMMANDS_REQUEST, mqttMsg);
- processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), msg));
+ processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(), msg));
grantedQoSList.add(getMinSupportedQos(reqQoS));
} else if (topicName.equals(DEVICE_RPC_RESPONSE_SUB_TOPIC)) {
grantedQoSList.add(getMinSupportedQos(reqQoS));
@@ -261,10 +265,10 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
try {
if (topicName.equals(DEVICE_ATTRIBUTES_TOPIC)) {
AdaptorToSessionActorMsg msg = adaptor.convertToActorMsg(deviceSessionCtx, UNSUBSCRIBE_ATTRIBUTES_REQUEST, mqttMsg);
- processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), msg));
+ processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(), msg));
} else if (topicName.equals(DEVICE_RPC_REQUESTS_SUB_TOPIC)) {
AdaptorToSessionActorMsg msg = adaptor.convertToActorMsg(deviceSessionCtx, UNSUBSCRIBE_RPC_COMMANDS_REQUEST, mqttMsg);
- processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), msg));
+ processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(), msg));
} else if (topicName.equals(DEVICE_ATTRIBUTES_RESPONSES_TOPIC)) {
deviceSessionCtx.setDisallowAttributeResponses();
}
@@ -303,6 +307,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
} else {
ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED));
connected = true;
+ processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(),
+ new BasicAdaptorToSessionActorMsg(deviceSessionCtx, new SessionOpenMsg())));
checkGatewaySession();
}
}
@@ -314,6 +320,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
if (deviceSessionCtx.login(new DeviceX509Credentials(sha3Hash))) {
ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED));
connected = true;
+ processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(),
+ new BasicAdaptorToSessionActorMsg(deviceSessionCtx, new SessionOpenMsg())));
checkGatewaySession();
} else {
ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_NOT_AUTHORIZED));
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
index cbe3ba6..bb8d4ad 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
@@ -29,7 +29,7 @@ import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.auth.DeviceAuthService;
-import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.common.transport.quota.host.HostRequestsQuotaService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
@@ -67,7 +67,7 @@ public class MqttTransportService {
private MqttSslHandlerProvider sslHandlerProvider;
@Autowired(required = false)
- private QuotaService quotaService;
+ private HostRequestsQuotaService quotaService;
@Value("${mqtt.bind_address}")
private String host;
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
index 632ab28..6377fad 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
@@ -80,13 +80,13 @@ public class GatewayDeviceSessionCtx extends DeviceAwareSessionContext {
private Optional<MqttMessage> getToDeviceMsg(SessionActorToAdaptorMsg sessionMsg) {
ToDeviceMsg msg = sessionMsg.getMsg();
- switch (msg.getMsgType()) {
+ switch (msg.getSessionMsgType()) {
case STATUS_CODE_RESPONSE:
ResponseMsg<?> responseMsg = (ResponseMsg) msg;
if (responseMsg.isSuccess()) {
- MsgType requestMsgType = responseMsg.getRequestMsgType();
+ SessionMsgType requestMsgType = responseMsg.getRequestMsgType();
Integer requestId = responseMsg.getRequestId();
- if (requestId >= 0 && requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) {
+ if (requestId >= 0 && requestMsgType == SessionMsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == SessionMsgType.POST_TELEMETRY_REQUEST) {
return Optional.of(MqttTransportHandler.createMqttPubAckMsg(requestId));
}
}
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
index 7eda5bd..f666bb8 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
@@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.msg.core.*;
import org.thingsboard.server.common.msg.session.BasicAdaptorToSessionActorMsg;
-import org.thingsboard.server.common.msg.session.BasicToDeviceActorSessionMsg;
+import org.thingsboard.server.common.msg.session.BasicTransportToDeviceSessionActorMsg;
import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
@@ -96,8 +96,8 @@ public class GatewaySessionCtx {
GatewayDeviceSessionCtx ctx = new GatewayDeviceSessionCtx(this, device);
devices.put(deviceName, ctx);
log.debug("[{}] Added device [{}] to the gateway session", gatewaySessionId, deviceName);
- processor.process(new BasicToDeviceActorSessionMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new AttributesSubscribeMsg())));
- processor.process(new BasicToDeviceActorSessionMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new RpcSubscribeMsg())));
+ processor.process(new BasicTransportToDeviceSessionActorMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new AttributesSubscribeMsg())));
+ processor.process(new BasicTransportToDeviceSessionActorMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new RpcSubscribeMsg())));
}
}
@@ -136,7 +136,7 @@ public class GatewaySessionCtx {
JsonConverter.parseWithTs(request, element.getAsJsonObject());
}
GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
- processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
+ processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(),
new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
}
} else {
@@ -152,7 +152,7 @@ public class GatewaySessionCtx {
Integer requestId = jsonObj.get("id").getAsInt();
String data = jsonObj.get("data").toString();
GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
- processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
+ processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(),
new BasicAdaptorToSessionActorMsg(deviceSessionCtx, new ToDeviceRpcResponseMsg(requestId, data))));
} else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
@@ -170,11 +170,11 @@ public class GatewaySessionCtx {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
}
long ts = System.currentTimeMillis();
- BasicUpdateAttributesRequest request = new BasicUpdateAttributesRequest(requestId);
+ BasicAttributesUpdateRequest request = new BasicAttributesUpdateRequest(requestId);
JsonObject deviceData = deviceEntry.getValue().getAsJsonObject();
request.add(JsonConverter.parseValues(deviceData).stream().map(kv -> new BaseAttributeKvEntry(kv, ts)).collect(Collectors.toList()));
GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
- processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
+ processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(),
new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
}
} else {
@@ -207,7 +207,7 @@ public class GatewaySessionCtx {
request = new BasicGetAttributesRequest(requestId, null, keys);
}
GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
- processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
+ processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(),
new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
ack(msg);
} else {
transport/pom.xml 2(+1 -1)
diff --git a/transport/pom.xml b/transport/pom.xml
index e3e6d66..b88598f 100644
--- a/transport/pom.xml
+++ b/transport/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.4.1-SNAPSHOT</version>
+ <version>2.0.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
ui/package.json 3(+2 -1)
diff --git a/ui/package.json b/ui/package.json
index ad9a7a6..bbf4bc3 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -1,7 +1,7 @@
{
"name": "thingsboard",
"private": true,
- "version": "1.4.1",
+ "version": "2.0.0",
"description": "Thingsboard UI",
"licenses": [
{
@@ -69,6 +69,7 @@
"moment": "^2.15.0",
"ngclipboard": "^1.1.1",
"ngreact": "^0.3.0",
+ "ngFlowchart": "git://github.com/thingsboard/ngFlowchart.git#master",
"objectpath": "^1.2.1",
"oclazyload": "^1.0.9",
"raphael": "^2.2.7",
ui/pom.xml 2(+1 -1)
diff --git a/ui/pom.xml b/ui/pom.xml
index 12e5979..5e89278 100644
--- a/ui/pom.xml
+++ b/ui/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.4.1-SNAPSHOT</version>
+ <version>2.0.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
ui/server.js 20(+20 -0)
diff --git a/ui/server.js b/ui/server.js
index fae132f..65a2bc7 100644
--- a/ui/server.js
+++ b/ui/server.js
@@ -30,6 +30,9 @@ const httpProxy = require('http-proxy');
const forwardHost = 'localhost';
const forwardPort = 8080;
+const ruleNodeUiforwardHost = 'localhost';
+const ruleNodeUiforwardPort = 8080;
+
const app = express();
const server = http.createServer(app);
@@ -52,17 +55,34 @@ const apiProxy = httpProxy.createProxyServer({
}
});
+const ruleNodeUiApiProxy = httpProxy.createProxyServer({
+ target: {
+ host: ruleNodeUiforwardHost,
+ port: ruleNodeUiforwardPort
+ }
+});
+
apiProxy.on('error', function (err, req, res) {
console.warn('API proxy error: ' + err);
res.end('Error.');
});
+ruleNodeUiApiProxy.on('error', function (err, req, res) {
+ console.warn('RuleNode UI API proxy error: ' + err);
+ res.end('Error.');
+});
+
console.info(`Forwarding API requests to http://${forwardHost}:${forwardPort}`);
+console.info(`Forwarding Rule Node UI requests to http://${ruleNodeUiforwardHost}:${ruleNodeUiforwardPort}`);
app.all('/api/*', (req, res) => {
apiProxy.web(req, res);
});
+app.all('/static/rulenode/*', (req, res) => {
+ ruleNodeUiApiProxy.web(req, res);
+});
+
app.get('*', function(req, res) {
res.sendFile(path.join(__dirname, 'src/index.html'));
});
diff --git a/ui/src/app/alarm/alarm-table.directive.js b/ui/src/app/alarm/alarm-table.directive.js
index 03470c6..775088c 100644
--- a/ui/src/app/alarm/alarm-table.directive.js
+++ b/ui/src/app/alarm/alarm-table.directive.js
@@ -42,7 +42,7 @@ export default function AlarmTableDirective($compile, $templateCache, $rootScope
history: {
timewindowMs: 24 * 60 * 60 * 1000 // 1 day
}
- }
+ };
scope.topIndex = 0;
@@ -98,6 +98,8 @@ export default function AlarmTableDirective($compile, $templateCache, $rootScope
}
};
+ scope.reload = reload;
+
scope.$watch("entityId", function(newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) {
resetFilter();
diff --git a/ui/src/app/alarm/alarm-table.tpl.html b/ui/src/app/alarm/alarm-table.tpl.html
index b9e466f..658362e 100644
--- a/ui/src/app/alarm/alarm-table.tpl.html
+++ b/ui/src/app/alarm/alarm-table.tpl.html
@@ -26,6 +26,13 @@
</md-select>
</md-input-container>
<tb-timewindow flex ng-model="timewindow" history-only as-button="true"></tb-timewindow>
+ <md-button ng-disabled="$root.loading"
+ class="md-icon-button" ng-click="reload()">
+ <md-icon>refresh</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.refresh' | translate }}
+ </md-tooltip>
+ </md-button>
</section>
<div flex layout="column" class="tb-alarm-container md-whiteframe-z1">
<md-list flex layout="column" class="tb-alarm-table">
diff --git a/ui/src/app/api/component-descriptor.service.js b/ui/src/app/api/component-descriptor.service.js
index 4478d71..cc3f710 100644
--- a/ui/src/app/api/component-descriptor.service.js
+++ b/ui/src/app/api/component-descriptor.service.js
@@ -26,7 +26,8 @@ function ComponentDescriptorService($http, $q) {
var service = {
getComponentDescriptorsByType: getComponentDescriptorsByType,
getComponentDescriptorByClazz: getComponentDescriptorByClazz,
- getPluginActionsByPluginClazz: getPluginActionsByPluginClazz
+ getPluginActionsByPluginClazz: getPluginActionsByPluginClazz,
+ getComponentDescriptorsByTypes: getComponentDescriptorsByTypes
}
return service;
@@ -52,6 +53,41 @@ function ComponentDescriptorService($http, $q) {
return deferred.promise;
}
+ function getComponentDescriptorsByTypes(componentTypes) {
+ var deferred = $q.defer();
+ var result = [];
+ for (var i=componentTypes.length-1;i>=0;i--) {
+ var componentType = componentTypes[i];
+ if (componentsByType[componentType]) {
+ result = result.concat(componentsByType[componentType]);
+ componentTypes.splice(i, 1);
+ }
+ }
+ if (!componentTypes.length) {
+ deferred.resolve(result);
+ } else {
+ var url = '/api/components?componentTypes=' + componentTypes.join(',');
+ $http.get(url, null).then(function success(response) {
+ var components = response.data;
+ for (var i = 0; i < components.length; i++) {
+ var component = components[i];
+ var componentsList = componentsByType[component.type];
+ if (!componentsList) {
+ componentsList = [];
+ componentsByType[component.type] = componentsList;
+ }
+ componentsList.push(component);
+ componentsByClazz[component.clazz] = component;
+ }
+ result = result.concat(components);
+ deferred.resolve(components);
+ }, function fail() {
+ deferred.reject();
+ });
+ }
+ return deferred.promise;
+ }
+
function getComponentDescriptorByClazz(componentDescriptorClazz) {
var deferred = $q.defer();
if (componentsByClazz[componentDescriptorClazz]) {
ui/src/app/api/entity.service.js 31(+6 -25)
diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js
index e4c51a2..762bf1a 100644
--- a/ui/src/app/api/entity.service.js
+++ b/ui/src/app/api/entity.service.js
@@ -21,8 +21,7 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes])
/*@ngInject*/
function EntityService($http, $q, $filter, $translate, $log, userService, deviceService,
- assetService, tenantService, customerService,
- ruleService, pluginService, dashboardService, entityRelationService, attributeService, types, utils) {
+ assetService, tenantService, customerService, ruleChainService, dashboardService, entityRelationService, attributeService, types, utils) {
var service = {
getEntity: getEntity,
getEntities: getEntities,
@@ -61,18 +60,15 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
case types.entityType.customer:
promise = customerService.getCustomer(entityId, config);
break;
- case types.entityType.rule:
- promise = ruleService.getRule(entityId, config);
- break;
- case types.entityType.plugin:
- promise = pluginService.getPlugin(entityId, config);
- break;
case types.entityType.dashboard:
promise = dashboardService.getDashboardInfo(entityId, config);
break;
case types.entityType.user:
promise = userService.getUser(entityId, true, config);
break;
+ case types.entityType.rulechain:
+ promise = ruleChainService.getRuleChain(entityId, config);
+ break;
case types.entityType.alarm:
$log.error('Get Alarm Entity is not implemented!');
break;
@@ -143,14 +139,6 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
promise = getEntitiesByIdsPromise(
(id) => customerService.getCustomer(id, config), entityIds);
break;
- case types.entityType.rule:
- promise = getEntitiesByIdsPromise(
- (id) => ruleService.getRule(id, config), entityIds);
- break;
- case types.entityType.plugin:
- promise = getEntitiesByIdsPromise(
- (id) => pluginService.getPlugin(id, config), entityIds);
- break;
case types.entityType.dashboard:
promise = getEntitiesByIdsPromise(
(id) => dashboardService.getDashboardInfo(id, config), entityIds);
@@ -265,11 +253,8 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
promise = customerService.getCustomers(pageLink, config);
}
break;
- case types.entityType.rule:
- promise = ruleService.getAllRules(pageLink, config);
- break;
- case types.entityType.plugin:
- promise = pluginService.getAllPlugins(pageLink, config);
+ case types.entityType.rulechain:
+ promise = ruleChainService.getRuleChains(pageLink, config);
break;
case types.entityType.dashboard:
if (user.authority === 'CUSTOMER_USER') {
@@ -736,16 +721,12 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
switch(authority) {
case 'SYS_ADMIN':
entityTypes.tenant = types.entityType.tenant;
- entityTypes.rule = types.entityType.rule;
- entityTypes.plugin = types.entityType.plugin;
break;
case 'TENANT_ADMIN':
entityTypes.device = types.entityType.device;
entityTypes.asset = types.entityType.asset;
entityTypes.tenant = types.entityType.tenant;
entityTypes.customer = types.entityType.customer;
- entityTypes.rule = types.entityType.rule;
- entityTypes.plugin = types.entityType.plugin;
entityTypes.dashboard = types.entityType.dashboard;
if (useAliasEntityTypes) {
entityTypes.current_customer = types.aliasEntityType.current_customer;
ui/src/app/api/rule-chain.service.js 298(+298 -0)
diff --git a/ui/src/app/api/rule-chain.service.js b/ui/src/app/api/rule-chain.service.js
new file mode 100644
index 0000000..e7436de
--- /dev/null
+++ b/ui/src/app/api/rule-chain.service.js
@@ -0,0 +1,298 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.api.ruleChain', [])
+ .factory('ruleChainService', RuleChainService).name;
+
+/*@ngInject*/
+function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, componentDescriptorService) {
+
+ var ruleNodeComponents = null;
+
+ var service = {
+ getRuleChains: getRuleChains,
+ getRuleChain: getRuleChain,
+ saveRuleChain: saveRuleChain,
+ setRootRuleChain: setRootRuleChain,
+ deleteRuleChain: deleteRuleChain,
+ getRuleChainMetaData: getRuleChainMetaData,
+ saveRuleChainMetaData: saveRuleChainMetaData,
+ getRuleNodeComponents: getRuleNodeComponents,
+ getRuleNodeComponentByClazz: getRuleNodeComponentByClazz,
+ getRuleNodeSupportedLinks: getRuleNodeSupportedLinks,
+ resolveTargetRuleChains: resolveTargetRuleChains,
+ testScript: testScript,
+ getLatestRuleNodeDebugInput: getLatestRuleNodeDebugInput
+ };
+
+ return service;
+
+ function getRuleChains (pageLink, config) {
+ var deferred = $q.defer();
+ var url = '/api/ruleChains?limit=' + pageLink.limit;
+ if (angular.isDefined(pageLink.textSearch)) {
+ url += '&textSearch=' + pageLink.textSearch;
+ }
+ if (angular.isDefined(pageLink.idOffset)) {
+ url += '&idOffset=' + pageLink.idOffset;
+ }
+ if (angular.isDefined(pageLink.textOffset)) {
+ url += '&textOffset=' + pageLink.textOffset;
+ }
+ $http.get(url, config).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getRuleChain(ruleChainId, config) {
+ var deferred = $q.defer();
+ var url = '/api/ruleChain/' + ruleChainId;
+ $http.get(url, config).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function saveRuleChain(ruleChain) {
+ var deferred = $q.defer();
+ var url = '/api/ruleChain';
+ $http.post(url, ruleChain).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function setRootRuleChain(ruleChainId) {
+ var deferred = $q.defer();
+ var url = '/api/ruleChain/' + ruleChainId + '/root';
+ $http.post(url).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function deleteRuleChain(ruleChainId) {
+ var deferred = $q.defer();
+ var url = '/api/ruleChain/' + ruleChainId;
+ $http.delete(url).then(function success() {
+ deferred.resolve();
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getRuleChainMetaData(ruleChainId, config) {
+ var deferred = $q.defer();
+ var url = '/api/ruleChain/' + ruleChainId + '/metadata';
+ $http.get(url, config).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function saveRuleChainMetaData(ruleChainMetaData) {
+ var deferred = $q.defer();
+ var url = '/api/ruleChain/metadata';
+ $http.post(url, ruleChainMetaData).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getRuleNodeSupportedLinks(component) {
+ var relationTypes = component.configurationDescriptor.nodeDefinition.relationTypes;
+ var customRelations = component.configurationDescriptor.nodeDefinition.customRelations;
+ var linkLabels = [];
+ for (var i=0;i<relationTypes.length;i++) {
+ linkLabels.push({
+ name: relationTypes[i], custom: false
+ });
+ }
+ if (customRelations) {
+ linkLabels.push(
+ { name: 'Custom', custom: true }
+ );
+ }
+ return linkLabels;
+ }
+
+ function getRuleNodeComponents() {
+ var deferred = $q.defer();
+ if (ruleNodeComponents) {
+ deferred.resolve(ruleNodeComponents);
+ } else {
+ loadRuleNodeComponents().then(
+ (components) => {
+ resolveRuleNodeComponentsUiResources(components).then(
+ (components) => {
+ ruleNodeComponents = components;
+ ruleNodeComponents.push(
+ types.ruleChainNodeComponent
+ );
+ ruleNodeComponents.sort(
+ (comp1, comp2) => {
+ var result = comp1.type.localeCompare(comp2.type);
+ if (result == 0) {
+ result = comp1.name.localeCompare(comp2.name);
+ }
+ return result;
+ }
+ );
+ deferred.resolve(ruleNodeComponents);
+ },
+ () => {
+ deferred.reject();
+ }
+ );
+ },
+ () => {
+ deferred.reject();
+ }
+ );
+ }
+ return deferred.promise;
+ }
+
+ function resolveRuleNodeComponentsUiResources(components) {
+ var deferred = $q.defer();
+ var tasks = [];
+ for (var i=0;i<components.length;i++) {
+ var component = components[i];
+ tasks.push(resolveRuleNodeComponentUiResources(component));
+ }
+ $q.all(tasks).then(
+ (components) => {
+ deferred.resolve(components);
+ },
+ () => {
+ deferred.resolve(components);
+ }
+ );
+ return deferred.promise;
+ }
+
+ function resolveRuleNodeComponentUiResources(component) {
+ var deferred = $q.defer();
+ var uiResources = component.configurationDescriptor.nodeDefinition.uiResources;
+ if (uiResources && uiResources.length) {
+ var tasks = [];
+ for (var i=0;i<uiResources.length;i++) {
+ var uiResource = uiResources[i];
+ tasks.push($ocLazyLoad.load(uiResource));
+ }
+ $q.all(tasks).then(
+ () => {
+ deferred.resolve(component);
+ },
+ () => {
+ component.configurationDescriptor.nodeDefinition.uiResourceLoadError = $translate.instant('rulenode.ui-resources-load-error');
+ deferred.resolve(component);
+ }
+ )
+ } else {
+ deferred.resolve(component);
+ }
+ return deferred.promise;
+ }
+
+ function getRuleNodeComponentByClazz(clazz) {
+ var res = $filter('filter')(ruleNodeComponents, {clazz: clazz}, true);
+ if (res && res.length) {
+ return res[0];
+ }
+ return null;
+ }
+
+ function resolveTargetRuleChains(ruleChainConnections) {
+ var deferred = $q.defer();
+ if (ruleChainConnections && ruleChainConnections.length) {
+ var tasks = [];
+ for (var i = 0; i < ruleChainConnections.length; i++) {
+ tasks.push(resolveRuleChain(ruleChainConnections[i].targetRuleChainId.id));
+ }
+ $q.all(tasks).then(
+ (ruleChains) => {
+ var ruleChainsMap = {};
+ for (var i = 0; i < ruleChains.length; i++) {
+ ruleChainsMap[ruleChains[i].id.id] = ruleChains[i];
+ }
+ deferred.resolve(ruleChainsMap);
+ },
+ () => {
+ deferred.reject();
+ }
+ );
+ } else {
+ deferred.resolve({});
+ }
+ return deferred.promise;
+ }
+
+ function resolveRuleChain(ruleChainId) {
+ var deferred = $q.defer();
+ getRuleChain(ruleChainId, {ignoreErrors: true}).then(
+ (ruleChain) => {
+ deferred.resolve(ruleChain);
+ },
+ () => {
+ deferred.resolve({
+ id: {id: ruleChainId, entityType: types.entityType.rulechain}
+ });
+ }
+ );
+ return deferred.promise;
+ }
+
+ function loadRuleNodeComponents() {
+ return componentDescriptorService.getComponentDescriptorsByTypes(types.ruleNodeTypeComponentTypes);
+ }
+
+ function testScript(inputParams) {
+ var deferred = $q.defer();
+ var url = '/api/ruleChain/testScript';
+ $http.post(url, inputParams).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getLatestRuleNodeDebugInput(ruleNodeId) {
+ var deferred = $q.defer();
+ var url = '/api/ruleNode/' + ruleNodeId + '/debugIn';
+ $http.get(url).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+}
ui/src/app/app.js 7(+7 -0)
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index c328a5a..f021efb 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -49,6 +49,7 @@ import 'material-ui';
import 'react-schema-form';
import react from 'ngreact';
import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
+import 'ngFlowchart/dist/ngFlowchart';
import thingsboardLocales from './locale/locale.constant';
import thingsboardLogin from './login';
@@ -73,6 +74,8 @@ import thingsboardApiAttribute from './api/attribute.service';
import thingsboardApiEntity from './api/entity.service';
import thingsboardApiAlarm from './api/alarm.service';
import thingsboardApiAuditLog from './api/audit-log.service';
+import thingsboardApiComponentDescriptor from './api/component-descriptor.service';
+import thingsboardApiRuleChain from './api/rule-chain.service';
import 'typeface-roboto';
import 'font-awesome/css/font-awesome.min.css';
@@ -85,6 +88,7 @@ import 'mdPickers/dist/mdPickers.min.css';
import 'angular-hotkeys/build/hotkeys.min.css';
import 'angular-carousel/dist/angular-carousel.min.css';
import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css';
+import 'ngFlowchart/dist/flowchart.css';
import '../scss/main.scss';
import AppConfig from './app.config';
@@ -112,6 +116,7 @@ angular.module('thingsboard', [
'ngclipboard',
react.name,
'flow',
+ 'flowchart',
thingsboardLocales,
thingsboardLogin,
thingsboardDialogs,
@@ -135,6 +140,8 @@ angular.module('thingsboard', [
thingsboardApiEntity,
thingsboardApiAlarm,
thingsboardApiAuditLog,
+ thingsboardApiComponentDescriptor,
+ thingsboardApiRuleChain,
uiRouter])
.config(AppConfig)
.factory('globalInterceptor', GlobalInterceptor)
ui/src/app/common/types.constant.js 130(+113 -17)
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index 66ed196..cf1023e 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -279,22 +279,40 @@ export default angular.module('thingsboard.types', [])
function: "function",
alarm: "alarm"
},
+ contentType: {
+ "JSON": {
+ value: "JSON",
+ name: "content-type.json",
+ code: "json"
+ },
+ "TEXT": {
+ value: "TEXT",
+ name: "content-type.text",
+ code: "text"
+ },
+ "BINARY": {
+ value: "BINARY",
+ name: "content-type.binary",
+ code: "text"
+ }
+ },
componentType: {
+ enrichment: "ENRICHMENT",
filter: "FILTER",
- processor: "PROCESSOR",
+ transformation: "TRANSFORMATION",
action: "ACTION",
- plugin: "PLUGIN"
+ external: "EXTERNAL"
},
entityType: {
device: "DEVICE",
asset: "ASSET",
- rule: "RULE",
- plugin: "PLUGIN",
tenant: "TENANT",
customer: "CUSTOMER",
user: "USER",
dashboard: "DASHBOARD",
- alarm: "ALARM"
+ alarm: "ALARM",
+ rulechain: "RULE_CHAIN",
+ rulenode: "RULE_NODE"
},
aliasEntityType: {
current_customer: "CURRENT_CUSTOMER"
@@ -312,18 +330,6 @@ export default angular.module('thingsboard.types', [])
list: 'entity.list-of-assets',
nameStartsWith: 'entity.asset-name-starts-with'
},
- "RULE": {
- type: 'entity.type-rule',
- typePlural: 'entity.type-rules',
- list: 'entity.list-of-rules',
- nameStartsWith: 'entity.rule-name-starts-with'
- },
- "PLUGIN": {
- type: 'entity.type-plugin',
- typePlural: 'entity.type-plugins',
- list: 'entity.list-of-plugins',
- nameStartsWith: 'entity.plugin-name-starts-with'
- },
"TENANT": {
type: 'entity.type-tenant',
typePlural: 'entity.type-tenants',
@@ -354,6 +360,12 @@ export default angular.module('thingsboard.types', [])
list: 'entity.list-of-alarms',
nameStartsWith: 'entity.alarm-name-starts-with'
},
+ "RULE_CHAIN": {
+ type: 'entity.type-rulechain',
+ typePlural: 'entity.type-rulechains',
+ list: 'entity.list-of-rulechains',
+ nameStartsWith: 'entity.rulechain-name-starts-with'
+ },
"CURRENT_CUSTOMER": {
type: 'entity.type-current-customer',
list: 'entity.type-current-customer'
@@ -381,6 +393,16 @@ export default angular.module('thingsboard.types', [])
name: "event.type-stats"
}
},
+ debugEventType: {
+ debugRuleNode: {
+ value: "DEBUG_RULE_NODE",
+ name: "event.type-debug-rule-node"
+ },
+ debugRuleChain: {
+ value: "DEBUG_RULE_CHAIN",
+ name: "event.type-debug-rule-chain"
+ }
+ },
extensionType: {
http: "HTTP",
mqtt: "MQTT",
@@ -471,6 +493,80 @@ export default angular.module('thingsboard.types', [])
clientSide: false
}
},
+ ruleNodeTypeComponentTypes: ["FILTER", "ENRICHMENT", "TRANSFORMATION", "ACTION", "EXTERNAL"],
+ ruleChainNodeComponent: {
+ type: 'RULE_CHAIN',
+ name: 'rule chain',
+ clazz: 'tb.internal.RuleChain',
+ configurationDescriptor: {
+ nodeDefinition: {
+ description: "",
+ details: "Forwards incoming messages to specified Rule Chain",
+ inEnabled: true,
+ outEnabled: false,
+ relationTypes: [],
+ customRelations: false,
+ defaultConfiguration: {}
+ }
+ }
+ },
+ inputNodeComponent: {
+ type: 'INPUT',
+ name: 'Input',
+ clazz: 'tb.internal.Input'
+ },
+ ruleNodeType: {
+ FILTER: {
+ value: "FILTER",
+ name: "rulenode.type-filter",
+ details: "rulenode.type-filter-details",
+ nodeClass: "tb-filter-type",
+ icon: "filter_list"
+ },
+ ENRICHMENT: {
+ value: "ENRICHMENT",
+ name: "rulenode.type-enrichment",
+ details: "rulenode.type-enrichment-details",
+ nodeClass: "tb-enrichment-type",
+ icon: "playlist_add"
+ },
+ TRANSFORMATION: {
+ value: "TRANSFORMATION",
+ name: "rulenode.type-transformation",
+ details: "rulenode.type-transformation-details",
+ nodeClass: "tb-transformation-type",
+ icon: "transform"
+ },
+ ACTION: {
+ value: "ACTION",
+ name: "rulenode.type-action",
+ details: "rulenode.type-action-details",
+ nodeClass: "tb-action-type",
+ icon: "flash_on"
+ },
+ EXTERNAL: {
+ value: "EXTERNAL",
+ name: "rulenode.type-external",
+ details: "rulenode.type-external-details",
+ nodeClass: "tb-external-type",
+ icon: "cloud_upload"
+ },
+ RULE_CHAIN: {
+ value: "RULE_CHAIN",
+ name: "rulenode.type-rule-chain",
+ details: "rulenode.type-rule-chain-details",
+ nodeClass: "tb-rule-chain-type",
+ icon: "settings_ethernet"
+ },
+ INPUT: {
+ value: "INPUT",
+ name: "rulenode.type-input",
+ details: "rulenode.type-input-details",
+ nodeClass: "tb-input-type",
+ icon: "input",
+ special: true
+ }
+ },
valueType: {
string: {
value: "string",
ui/src/app/components/ace-editor-fix.js 45(+45 -0)
diff --git a/ui/src/app/components/ace-editor-fix.js b/ui/src/app/components/ace-editor-fix.js
new file mode 100644
index 0000000..f68767e
--- /dev/null
+++ b/ui/src/app/components/ace-editor-fix.js
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default function fixAceEditor(aceEditor) {
+ aceEditor.$blockScrolling = Infinity;
+ aceEditor.on("showGutterTooltip", function (tooltip) {
+ if (!tooltip.isAttachedToBody) {
+ document.body.appendChild(tooltip.$element); //eslint-disable-line
+ tooltip.isAttachedToBody = true;
+ onElementRemoved(tooltip.$parentNode, () => {
+ if (tooltip.$element.parentNode != null) {
+ tooltip.$element.parentNode.removeChild(tooltip.$element);
+ }
+ });
+ }
+ });
+}
+
+function onElementRemoved(element, callback) {
+ if (!document.body.contains(element)) { //eslint-disable-line
+ callback();
+ } else {
+ var observer;
+ observer = new MutationObserver(function(mutations) { //eslint-disable-line
+ if (!document.body.contains(element)) { //eslint-disable-line
+ callback();
+ observer.disconnect();
+ }
+ });
+ observer.observe(document.body, {childList: true}); //eslint-disable-line
+ }
+}
diff --git a/ui/src/app/components/confirm-on-exit.directive.js b/ui/src/app/components/confirm-on-exit.directive.js
index fe9a9bd..e04e110 100644
--- a/ui/src/app/components/confirm-on-exit.directive.js
+++ b/ui/src/app/components/confirm-on-exit.directive.js
@@ -18,17 +18,17 @@ export default angular.module('thingsboard.directives.confirmOnExit', [])
.name;
/*@ngInject*/
-function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
+function ConfirmOnExit($state, $mdDialog, $window, $filter, $parse, userService) {
return {
- link: function ($scope) {
-
+ link: function ($scope, $element, $attributes) {
+ $scope.confirmForm = $scope.$eval($attributes.confirmForm);
$window.onbeforeunload = function () {
- if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
+ if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.$eval($attributes.isDirty))) {
return $filter('translate')('confirm-on-exit.message');
}
}
$scope.$on('$stateChangeStart', function (event, next, current, params) {
- if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
+ if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.$eval($attributes.isDirty))) {
event.preventDefault();
var confirm = $mdDialog.confirm()
.title($filter('translate')('confirm-on-exit.title'))
@@ -40,7 +40,9 @@ function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
if ($scope.confirmForm) {
$scope.confirmForm.$setPristine();
} else {
- $scope.isDirty = false;
+ var remoteSetter = $parse($attributes.isDirty).assign;
+ remoteSetter($scope, false);
+ //$scope.isDirty = false;
}
$state.go(next.name, params);
}, function () {
@@ -48,9 +50,6 @@ function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
}
});
},
- scope: {
- confirmForm: '=',
- isDirty: '='
- }
+ scope: false
};
}
\ No newline at end of file
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index 376f2d6..54a1a09 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -122,9 +122,9 @@
<md-menu-content id="menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
<md-menu-item ng-repeat ="item in vm.widgetContextMenuItems">
<md-button ng-disabled="!item.enabled" ng-click="item.action(vm.widgetContextMenuEvent, widget)">
+ <span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
<md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
<span translate>{{item.value}}</span>
- <span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
</md-button>
</md-menu-item>
</md-menu-content>
@@ -137,9 +137,9 @@
<md-menu-content id="menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
<md-menu-item ng-repeat ="item in vm.contextMenuItems">
<md-button ng-disabled="!item.enabled" ng-click="item.action(vm.contextMenuEvent)">
+ <span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
<md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
<span translate>{{item.value}}</span>
- <span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
</md-button>
</md-menu-item>
</md-menu-content>
diff --git a/ui/src/app/components/details-sidenav.directive.js b/ui/src/app/components/details-sidenav.directive.js
index e455a80..2516134 100644
--- a/ui/src/app/components/details-sidenav.directive.js
+++ b/ui/src/app/components/details-sidenav.directive.js
@@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.detailsSidenav', [])
.name;
/*@ngInject*/
-function DetailsSidenav($timeout) {
+function DetailsSidenav($timeout, $mdUtil, $q, $animate) {
var linker = function (scope, element, attrs) {
@@ -42,6 +42,63 @@ function DetailsSidenav($timeout) {
scope.isEdit = true;
}
+ var backdrop;
+ var previousContainerStyles;
+
+ if (attrs.hasOwnProperty('tbEnableBackdrop')) {
+ backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter");
+ element.on('$destroy', function() {
+ backdrop && backdrop.remove();
+ });
+ scope.$on('$destroy', function(){
+ backdrop && backdrop.remove();
+ });
+ scope.$watch('isOpen', updateIsOpen);
+ }
+
+ function updateIsOpen(isOpen) {
+ backdrop[isOpen ? 'on' : 'off']('click', (ev)=>{
+ ev.preventDefault();
+ scope.isOpen = false;
+ scope.$apply();
+ });
+ var parent = element.parent();
+ var restorePositioning = updateContainerPositions(parent, isOpen);
+
+ return $q.all([
+ isOpen && backdrop ? $animate.enter(backdrop, parent) : backdrop ?
+ $animate.leave(backdrop) : $q.when(true)
+ ]).then(function() {
+ restorePositioning && restorePositioning();
+ });
+ }
+
+ function updateContainerPositions(parent, willOpen) {
+ var drawerEl = element[0];
+ var scrollTop = parent[0].scrollTop;
+ if (willOpen && scrollTop) {
+ previousContainerStyles = {
+ top: drawerEl.style.top,
+ bottom: drawerEl.style.bottom,
+ height: drawerEl.style.height
+ };
+ var positionStyle = {
+ top: scrollTop + 'px',
+ bottom: 'auto',
+ height: parent[0].clientHeight + 'px'
+ };
+ backdrop.css(positionStyle);
+ }
+ if (!willOpen && previousContainerStyles) {
+ return function() {
+ backdrop[0].style.top = null;
+ backdrop[0].style.bottom = null;
+ backdrop[0].style.height = null;
+ previousContainerStyles = null;
+ };
+ }
+ }
+
scope.toggleDetailsEditMode = function () {
if (!scope.isAlwaysEdit) {
if (!scope.isEdit) {
ui/src/app/components/details-sidenav.scss 10(+10 -0)
diff --git a/ui/src/app/components/details-sidenav.scss b/ui/src/app/components/details-sidenav.scss
index c7e9919..360b133 100644
--- a/ui/src/app/components/details-sidenav.scss
+++ b/ui/src/app/components/details-sidenav.scss
@@ -59,4 +59,14 @@ md-sidenav.tb-sidenav-details {
background-color: $primary-hue-3;
}
}
+
+ md-tab-content.md-active > div {
+ height: 100%;
+ & > *:first-child {
+ height: 100%;
+ }
+ md-content {
+ height: 100%;
+ }
+ }
}
diff --git a/ui/src/app/components/details-sidenav.tpl.html b/ui/src/app/components/details-sidenav.tpl.html
index c504a24..763bc22 100644
--- a/ui/src/app/components/details-sidenav.tpl.html
+++ b/ui/src/app/components/details-sidenav.tpl.html
@@ -16,7 +16,7 @@
-->
<md-sidenav class="md-sidenav-right md-whiteframe-4dp tb-sidenav-details"
- md-disable-backdrop="true"
+ md-disable-backdrop
md-is-open="isOpen"
md-component-id="right"
layout="column">
ui/src/app/components/js-func.directive.js 48(+42 -6)
diff --git a/ui/src/app/components/js-func.directive.js b/ui/src/app/components/js-func.directive.js
index f95d003..f13f13c 100644
--- a/ui/src/app/components/js-func.directive.js
+++ b/ui/src/app/components/js-func.directive.js
@@ -22,12 +22,18 @@ import thingsboardToast from '../services/toast';
import thingsboardUtils from '../common/utils.service';
import thingsboardExpandFullscreen from './expand-fullscreen.directive';
+import fixAceEditor from './ace-editor-fix';
+
/* eslint-disable import/no-unresolved, import/default */
import jsFuncTemplate from './js-func.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
+import beautify from 'js-beautify';
+
+const js_beautify = beautify.js;
+
/* eslint-disable angular/angularelement */
export default angular.module('thingsboard.directives.jsFunc', [thingsboardToast, thingsboardUtils, thingsboardExpandFullscreen])
@@ -41,6 +47,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
var template = $templateCache.get(jsFuncTemplate);
element.html(template);
+ scope.functionName = attrs.functionName;
scope.functionArgs = scope.$eval(attrs.functionArgs);
scope.validationArgs = scope.$eval(attrs.validationArgs);
scope.resultType = attrs.resultType;
@@ -48,6 +55,8 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
scope.resultType = "nocheck";
}
+ scope.validationTriggerArg = attrs.validationTriggerArg;
+
scope.functionValid = true;
var Range = ace.acequire("ace/range").Range;
@@ -56,7 +65,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
scope.functionArgsString = '';
- for (var i in scope.functionArgs) {
+ for (var i = 0; i < scope.functionArgs.length; i++) {
if (scope.functionArgsString.length > 0) {
scope.functionArgsString += ', ';
}
@@ -64,11 +73,20 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
}
scope.onFullscreenChanged = function () {
+ updateEditorSize();
+ };
+
+ scope.beautifyJs = function () {
+ var res = js_beautify(scope.functionBody, {indent_size: 4, wrap_line_length: 60});
+ scope.functionBody = res;
+ };
+
+ function updateEditorSize() {
if (scope.js_editor) {
scope.js_editor.resize();
scope.js_editor.renderer.updateFull();
}
- };
+ }
scope.jsEditorOptions = {
useWrapMode: true,
@@ -83,6 +101,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
scope.js_editor.session.on("change", function () {
scope.cleanupJsErrors();
});
+ fixAceEditor(_ace);
}
};
@@ -128,6 +147,9 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
scope.validate = function () {
try {
var toValidate = new Function(scope.functionArgsString, scope.functionBody);
+ if (scope.noValidate) {
+ return true;
+ }
var res;
var validationError;
for (var i=0;i<scope.validationArgs.length;i++) {
@@ -197,9 +219,19 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
}
};
- scope.$on('form-submit', function () {
- scope.functionValid = scope.validate();
- scope.updateValidity();
+ scope.$on('form-submit', function (event, args) {
+ if (!args || scope.validationTriggerArg && scope.validationTriggerArg == args) {
+ scope.validationArgs = scope.$eval(attrs.validationArgs);
+ scope.cleanupJsErrors();
+ scope.functionValid = true;
+ scope.updateValidity();
+ scope.functionValid = scope.validate();
+ scope.updateValidity();
+ }
+ });
+
+ scope.$on('update-ace-editor-size', function () {
+ updateEditorSize();
});
$compile(element.contents())(scope);
@@ -208,7 +240,11 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
return {
restrict: "E",
require: "^ngModel",
- scope: {},
+ scope: {
+ disabled:'=ngDisabled',
+ noValidate: '=?',
+ fillHeight:'=?'
+ },
link: linker
};
}
ui/src/app/components/js-func.scss 25(+23 -2)
diff --git a/ui/src/app/components/js-func.scss b/ui/src/app/components/js-func.scss
index 2bd5df1..ade2830 100644
--- a/ui/src/app/components/js-func.scss
+++ b/ui/src/app/components/js-func.scss
@@ -15,16 +15,37 @@
*/
tb-js-func {
position: relative;
+ .tb-disabled {
+ color: rgba(0,0,0,0.38);
+ }
+ .fill-height {
+ height: 100%;
+ }
+}
+
+.tb-js-func-toolbar {
+ .md-button.tidy {
+ color: #7B7B7B;
+ min-width: 32px;
+ min-height: 15px;
+ line-height: 15px;
+ font-size: 0.800rem;
+ margin: 0 5px 0 0;
+ padding: 4px;
+ background: rgba(220, 220, 220, 0.35);
+ }
}
.tb-js-func-panel {
margin-left: 15px;
border: 1px solid #C0C0C0;
- height: 100%;
+ height: calc(100% - 80px);
#tb-javascript-input {
min-width: 200px;
- min-height: 200px;
width: 100%;
height: 100%;
+ &:not(.fill-height) {
+ min-height: 200px;
+ }
}
}
ui/src/app/components/js-func.tpl.html 22(+13 -9)
diff --git a/ui/src/app/components/js-func.tpl.html b/ui/src/app/components/js-func.tpl.html
index 806de4a..58675cb 100644
--- a/ui/src/app/components/js-func.tpl.html
+++ b/ui/src/app/components/js-func.tpl.html
@@ -15,19 +15,23 @@
limitations under the License.
-->
-<div style="background: #fff;" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
- <div layout="row" layout-align="start center" style="height: 40px;">
- <span style="font-style: italic;">function({{ functionArgsString }}) {</span>
+<div style="background: #fff;" ng-class="{'tb-disabled': disabled, 'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()">
+ <div layout="row" layout-align="start center" style="height: 40px;" class="tb-js-func-toolbar">
+ <label class="tb-title no-padding">function {{ functionName }}({{ functionArgsString }}) {</label>
<span flex></span>
+ <md-button ng-if="!disabled" class="tidy" aria-label="{{ 'js-func.tidy' | translate }}" ng-click="beautifyJs()">{{
+ 'js-func.tidy' | translate }}
+ </md-button>
<div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div>
</div>
- <div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column">
- <div flex id="tb-javascript-input"
- ui-ace="jsEditorOptions"
+ <div id="tb-javascript-panel" class="tb-js-func-panel">
+ <div id="tb-javascript-input" ng-class="{'fill-height': fillHeight}"
+ ui-ace="jsEditorOptions"
+ ng-readonly="disabled"
ng-model="functionBody">
</div>
</div>
<div layout="row" layout-align="start center" style="height: 40px;">
- <span style="font-style: italic;">}</span>
- </div>
-</div>
\ No newline at end of file
+ <label class="tb-title no-padding">}</label>
+ </div>
+</div>
ui/src/app/components/json-content.directive.js 184(+184 -0)
diff --git a/ui/src/app/components/json-content.directive.js b/ui/src/app/components/json-content.directive.js
new file mode 100644
index 0000000..e945079
--- /dev/null
+++ b/ui/src/app/components/json-content.directive.js
@@ -0,0 +1,184 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './json-content.scss';
+
+import 'brace/ext/language_tools';
+import 'brace/mode/json';
+import 'brace/mode/text';
+import 'ace-builds/src-min-noconflict/snippets/json';
+import 'ace-builds/src-min-noconflict/snippets/text';
+
+import fixAceEditor from './ace-editor-fix';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import jsonContentTemplate from './json-content.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import beautify from 'js-beautify';
+
+const js_beautify = beautify.js;
+
+export default angular.module('thingsboard.directives.jsonContent', [])
+ .directive('tbJsonContent', JsonContent)
+ .name;
+
+/*@ngInject*/
+function JsonContent($compile, $templateCache, toast, types, utils) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+ var template = $templateCache.get(jsonContentTemplate);
+ element.html(template);
+
+ scope.label = attrs.label;
+
+ scope.validationTriggerArg = attrs.validationTriggerArg;
+
+ scope.contentValid = true;
+
+ scope.json_editor;
+
+ scope.onFullscreenChanged = function () {
+ updateEditorSize();
+ };
+
+ scope.beautifyJson = function () {
+ var res = js_beautify(scope.contentBody, {indent_size: 4, wrap_line_length: 60});
+ scope.contentBody = res;
+ };
+
+ function updateEditorSize() {
+ if (scope.json_editor) {
+ scope.json_editor.resize();
+ scope.json_editor.renderer.updateFull();
+ }
+ }
+
+ var mode;
+ if (scope.contentType) {
+ mode = types.contentType[scope.contentType].code;
+ } else {
+ mode = 'text';
+ }
+
+ scope.jsonEditorOptions = {
+ useWrapMode: true,
+ mode: mode,
+ advanced: {
+ enableSnippets: true,
+ enableBasicAutocompletion: true,
+ enableLiveAutocompletion: true
+ },
+ onLoad: function (_ace) {
+ scope.json_editor = _ace;
+ scope.json_editor.session.on("change", function () {
+ scope.cleanupJsonErrors();
+ });
+ fixAceEditor(_ace);
+ }
+ };
+
+ scope.$watch('contentType', () => {
+ var mode;
+ if (scope.contentType) {
+ mode = types.contentType[scope.contentType].code;
+ } else {
+ mode = 'text';
+ }
+ if (scope.json_editor) {
+ scope.json_editor.session.setMode('ace/mode/' + mode);
+ }
+ });
+
+ scope.cleanupJsonErrors = function () {
+ toast.hide();
+ };
+
+ scope.updateValidity = function () {
+ ngModelCtrl.$setValidity('contentBody', scope.contentValid);
+ };
+
+ scope.$watch('contentBody', function (newContent, oldContent) {
+ ngModelCtrl.$setViewValue(scope.contentBody);
+ if (!angular.equals(newContent, oldContent)) {
+ scope.contentValid = true;
+ }
+ scope.updateValidity();
+ });
+
+ ngModelCtrl.$render = function () {
+ scope.contentBody = ngModelCtrl.$viewValue;
+ };
+
+ scope.showError = function (error) {
+ var toastParent = angular.element('#tb-json-panel', element);
+ toast.showError(error, toastParent, 'bottom left');
+ };
+
+ scope.validate = function () {
+ try {
+ if (scope.validateContent) {
+ if (scope.contentType == types.contentType.JSON.value) {
+ angular.fromJson(scope.contentBody);
+ }
+ }
+ return true;
+ } catch (e) {
+ var details = utils.parseException(e);
+ var errorInfo = 'Error:';
+ if (details.name) {
+ errorInfo += ' ' + details.name + ':';
+ }
+ if (details.message) {
+ errorInfo += ' ' + details.message;
+ }
+ scope.showError(errorInfo);
+ return false;
+ }
+ };
+
+ scope.$on('form-submit', function (event, args) {
+ if (!scope.readonly) {
+ if (!args || scope.validationTriggerArg && scope.validationTriggerArg == args) {
+ scope.cleanupJsonErrors();
+ scope.contentValid = true;
+ scope.updateValidity();
+ scope.contentValid = scope.validate();
+ scope.updateValidity();
+ }
+ }
+ });
+
+ scope.$on('update-ace-editor-size', function () {
+ updateEditorSize();
+ });
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {
+ contentType: '=',
+ validateContent: '=?',
+ readonly:'=ngReadonly',
+ fillHeight:'=?'
+ },
+ link: linker
+ };
+}
ui/src/app/components/json-content.scss 48(+48 -0)
diff --git a/ui/src/app/components/json-content.scss b/ui/src/app/components/json-content.scss
new file mode 100644
index 0000000..287c7e3
--- /dev/null
+++ b/ui/src/app/components/json-content.scss
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+tb-json-content {
+ position: relative;
+ .fill-height {
+ height: 100%;
+ }
+}
+
+.tb-json-content-toolbar {
+ .md-button.tidy {
+ color: #7B7B7B;
+ min-width: 32px;
+ min-height: 15px;
+ line-height: 15px;
+ font-size: 0.800rem;
+ margin: 0 5px 0 0;
+ padding: 4px;
+ background: rgba(220, 220, 220, 0.35);
+ }
+}
+
+.tb-json-content-panel {
+ margin-left: 15px;
+ border: 1px solid #C0C0C0;
+ height: 100%;
+ #tb-json-input {
+ min-width: 200px;
+ width: 100%;
+ height: 100%;
+ &:not(.fill-height) {
+ min-height: 200px;
+ }
+ }
+}
ui/src/app/components/json-content.tpl.html 34(+34 -0)
diff --git a/ui/src/app/components/json-content.tpl.html b/ui/src/app/components/json-content.tpl.html
new file mode 100644
index 0000000..a902b99
--- /dev/null
+++ b/ui/src/app/components/json-content.tpl.html
@@ -0,0 +1,34 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
+ <div layout="row" layout-align="start center" style="height: 40px;" class="tb-json-content-toolbar">
+ <label class="tb-title no-padding">{{ label }}</label>
+ <span flex></span>
+ <md-button ng-if="!readonly" class="tidy" aria-label="{{ 'js-func.tidy' | translate }}" ng-click="beautifyJson()">{{
+ 'js-func.tidy' | translate }}
+ </md-button>
+ <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
+ </div>
+ <div flex id="tb-json-panel" class="tb-json-content-panel" layout="column">
+ <div flex id="tb-json-input" ng-class="{'fill-height': fillHeight}"
+ ng-readonly="readonly"
+ ui-ace="jsonEditorOptions"
+ ng-model="contentBody">
+ </div>
+ </div>
+</div>
ui/src/app/components/json-object-edit.directive.js 183(+183 -0)
diff --git a/ui/src/app/components/json-object-edit.directive.js b/ui/src/app/components/json-object-edit.directive.js
new file mode 100644
index 0000000..215b7b9
--- /dev/null
+++ b/ui/src/app/components/json-object-edit.directive.js
@@ -0,0 +1,183 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './json-object-edit.scss';
+
+import 'brace/ext/language_tools';
+import 'brace/mode/json';
+import 'ace-builds/src-min-noconflict/snippets/json';
+
+import fixAceEditor from './ace-editor-fix';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import jsonObjectEditTemplate from './json-object-edit.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.jsonObjectEdit', [])
+ .directive('tbJsonObjectEdit', JsonObjectEdit)
+ .name;
+
+/*@ngInject*/
+function JsonObjectEdit($compile, $templateCache, $document, toast, utils) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+ var template = $templateCache.get(jsonObjectEditTemplate);
+ element.html(template);
+
+ scope.label = attrs.label;
+
+ scope.objectValid = true;
+ scope.validationError = '';
+
+ scope.json_editor;
+
+ scope.onFullscreenChanged = function () {
+ updateEditorSize();
+ };
+
+ function updateEditorSize() {
+ if (scope.json_editor) {
+ scope.json_editor.resize();
+ scope.json_editor.renderer.updateFull();
+ }
+ }
+
+ scope.jsonEditorOptions = {
+ useWrapMode: true,
+ mode: 'json',
+ advanced: {
+ enableSnippets: true,
+ enableBasicAutocompletion: true,
+ enableLiveAutocompletion: true
+ },
+ onLoad: function (_ace) {
+ scope.json_editor = _ace;
+ scope.json_editor.session.on("change", function () {
+ scope.cleanupJsonErrors();
+ });
+ fixAceEditor(_ace);
+ }
+ };
+
+ scope.cleanupJsonErrors = function () {
+ toast.hide();
+ };
+
+ scope.updateValidity = function () {
+ ngModelCtrl.$setValidity('objectValid', scope.objectValid);
+ };
+
+ scope.$watch('contentBody', function (newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal)) {
+ var object = scope.validate();
+ if (scope.objectValid) {
+ if (object == null) {
+ scope.object = null;
+ } else {
+ if (scope.object == null) {
+ scope.object = {};
+ }
+ Object.keys(scope.object).forEach(function (key) {
+ delete scope.object[key];
+ });
+ Object.keys(object).forEach(function (key) {
+ scope.object[key] = object[key];
+ });
+ }
+ ngModelCtrl.$setViewValue(scope.object);
+ }
+ scope.updateValidity();
+ }
+ });
+
+ ngModelCtrl.$render = function () {
+ scope.object = ngModelCtrl.$viewValue;
+ var content = '';
+ try {
+ if (scope.object) {
+ content = angular.toJson(scope.object, true);
+ }
+ } catch (e) {
+ //
+ }
+ scope.contentBody = content;
+ };
+
+ scope.showError = function (error) {
+ var toastParent = angular.element('#tb-json-panel', element);
+ toast.showError(error, toastParent, 'bottom left');
+ };
+
+ scope.validate = function () {
+ if (!scope.contentBody || !scope.contentBody.length) {
+ if (scope.required) {
+ scope.validationError = 'Json object is required.';
+ scope.objectValid = false;
+ } else {
+ scope.validationError = '';
+ scope.objectValid = true;
+ }
+ return null;
+ } else {
+ try {
+ var object = angular.fromJson(scope.contentBody);
+ scope.validationError = '';
+ scope.objectValid = true;
+ return object;
+ } catch (e) {
+ var details = utils.parseException(e);
+ var errorInfo = 'Error:';
+ if (details.name) {
+ errorInfo += ' ' + details.name + ':';
+ }
+ if (details.message) {
+ errorInfo += ' ' + details.message;
+ }
+ scope.validationError = errorInfo;
+ scope.objectValid = false;
+ return null;
+ }
+ }
+ };
+
+ scope.$on('form-submit', function () {
+ if (!scope.readonly) {
+ scope.cleanupJsonErrors();
+ if (!scope.objectValid) {
+ scope.showError(scope.validationError);
+ }
+ }
+ });
+
+ scope.$on('update-ace-editor-size', function () {
+ updateEditorSize();
+ });
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {
+ required:'=ngRequired',
+ readonly:'=ngReadonly',
+ fillHeight:'=?'
+ },
+ link: linker
+ };
+}
diff --git a/ui/src/app/components/json-object-edit.tpl.html b/ui/src/app/components/json-object-edit.tpl.html
new file mode 100644
index 0000000..ebab3c7
--- /dev/null
+++ b/ui/src/app/components/json-object-edit.tpl.html
@@ -0,0 +1,34 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
+ <div layout="row" layout-align="start center">
+ <label class="tb-title no-padding"
+ ng-class="{'tb-required': required,
+ 'tb-readonly': readonly,
+ 'tb-error': !objectValid}">{{ label }}</label>
+ <span flex></span>
+ <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
+ </div>
+ <div flex id="tb-json-panel" class="tb-json-object-panel" layout="column">
+ <div flex id="tb-json-input" ng-class="{'fill-height': fillHeight}"
+ ng-readonly="readonly"
+ ui-ace="jsonEditorOptions"
+ ng-model="contentBody">
+ </div>
+ </div>
+</div>
ui/src/app/components/kv-map.directive.js 119(+119 -0)
diff --git a/ui/src/app/components/kv-map.directive.js b/ui/src/app/components/kv-map.directive.js
new file mode 100644
index 0000000..bc8865f
--- /dev/null
+++ b/ui/src/app/components/kv-map.directive.js
@@ -0,0 +1,119 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './kv-map.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import kvMapTemplate from './kv-map.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.keyValMap', [])
+ .directive('tbKeyValMap', KeyValMap)
+ .name;
+
+/*@ngInject*/
+function KeyValMap() {
+ return {
+ restrict: "E",
+ scope: true,
+ bindToController: {
+ disabled:'=ngDisabled',
+ titleText: '@?',
+ keyPlaceholderText: '@?',
+ valuePlaceholderText: '@?',
+ noDataText: '@?',
+ keyValMap: '='
+ },
+ controller: KeyValMapController,
+ controllerAs: 'vm',
+ templateUrl: kvMapTemplate
+ };
+}
+
+/*@ngInject*/
+function KeyValMapController($scope, $mdUtil) {
+
+ let vm = this;
+
+ vm.kvList = [];
+
+ vm.removeKeyVal = removeKeyVal;
+ vm.addKeyVal = addKeyVal;
+
+ $scope.$watch('vm.keyValMap', () => {
+ stopWatchKvList();
+ vm.kvList.length = 0;
+ if (vm.keyValMap) {
+ for (var property in vm.keyValMap) {
+ if (vm.keyValMap.hasOwnProperty(property)) {
+ vm.kvList.push(
+ {
+ key: property + '',
+ value: vm.keyValMap[property]
+ }
+ );
+ }
+ }
+ }
+ $mdUtil.nextTick(() => {
+ watchKvList();
+ });
+ });
+
+ function watchKvList() {
+ $scope.kvListWatcher = $scope.$watch('vm.kvList', () => {
+ if (!vm.keyValMap) {
+ return;
+ }
+ for (var property in vm.keyValMap) {
+ if (vm.keyValMap.hasOwnProperty(property)) {
+ delete vm.keyValMap[property];
+ }
+ }
+ for (var i=0;i<vm.kvList.length;i++) {
+ var entry = vm.kvList[i];
+ vm.keyValMap[entry.key] = entry.value;
+ }
+ }, true);
+ }
+
+ function stopWatchKvList() {
+ if ($scope.kvListWatcher) {
+ $scope.kvListWatcher();
+ $scope.kvListWatcher = null;
+ }
+ }
+
+
+ function removeKeyVal(index) {
+ if (index > -1) {
+ vm.kvList.splice(index, 1);
+ }
+ }
+
+ function addKeyVal() {
+ if (!vm.kvList) {
+ vm.kvList = [];
+ }
+ vm.kvList.push(
+ {
+ key: '',
+ value: ''
+ }
+ );
+ }
+}
ui/src/app/components/kv-map.tpl.html 58(+58 -0)
diff --git a/ui/src/app/components/kv-map.tpl.html b/ui/src/app/components/kv-map.tpl.html
new file mode 100644
index 0000000..3e550f3
--- /dev/null
+++ b/ui/src/app/components/kv-map.tpl.html
@@ -0,0 +1,58 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<section layout="column" class="tb-kv-map">
+ <label translate class="tb-title no-padding">{{ vm.titleText }}</label>
+ <div layout="row"
+ ng-repeat="keyVal in vm.kvList track by $index"
+ style="max-height: 40px;" layout-align="start center">
+ <md-input-container flex md-no-float class="md-block"
+ style="margin: 10px 0px 0px 0px; max-height: 40px;">
+ <input placeholder="{{ (vm.keyPlaceholderText ? vm.keyPlaceholderText : 'key-val.key') | translate }}"
+ ng-disabled="vm.disabled" ng-required="true" name="key" ng-model="keyVal.key">
+ </md-input-container>
+ <md-input-container flex md-no-float class="md-block"
+ style="margin: 10px 0px 0px 0px; max-height: 40px;">
+ <input placeholder="{{ (vm.valuePlaceholderText ? vm.valuePlaceholderText : 'key-val.value') | translate }}"
+ ng-disabled="vm.disabled" ng-required="true" name="value" ng-model="keyVal.value">
+ </md-input-container>
+ <md-button ng-show="!vm.disabled" ng-disabled="$root.loading" class="md-icon-button md-primary"
+ ng-click="vm.removeKeyVal($index)"
+ aria-label="{{ 'action.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'key-val.remove-entry' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.delete' | translate }}"
+ class="material-icons">
+ close
+ </md-icon>
+ </md-button>
+ </div>
+ <span ng-show="!vm.kvList.length"
+ layout-align="center center" ng-class="{'disabled': vm.disabled}"
+ class="no-data-found" translate>{{vm.noDataText ? vm.noDataText : 'key-val.no-data'}}</span>
+ <div>
+ <md-button ng-show="!vm.disabled" ng-disabled="$root.loading" class="md-primary md-raised"
+ ng-click="vm.addKeyVal()"
+ aria-label="{{ 'action.add' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'key-val.add-entry' | translate }}
+ </md-tooltip>
+ <span translate>action.add</span>
+ </md-button>
+ </div>
+</section>
diff --git a/ui/src/app/components/mousepoint-menu.directive.js b/ui/src/app/components/mousepoint-menu.directive.js
index 2a0ab52..9015268 100644
--- a/ui/src/app/components/mousepoint-menu.directive.js
+++ b/ui/src/app/components/mousepoint-menu.directive.js
@@ -27,7 +27,12 @@ function MousepointMenu() {
var offset = $element.offset();
var x = $event.pageX - offset.left;
var y = $event.pageY - offset.top;
-
+ if ($attrs.tbOffsetX) {
+ x += Number($attrs.tbOffsetX);
+ }
+ if ($attrs.tbOffsetY) {
+ y += Number($attrs.tbOffsetY);
+ }
var offsets = {
left: x,
top: y
diff --git a/ui/src/app/components/react/json-form-ace-editor.jsx b/ui/src/app/components/react/json-form-ace-editor.jsx
index 1c4c02e..5afd3d1 100644
--- a/ui/src/app/components/react/json-form-ace-editor.jsx
+++ b/ui/src/app/components/react/json-form-ace-editor.jsx
@@ -23,6 +23,8 @@ import FlatButton from 'material-ui/FlatButton';
import 'brace/ext/language_tools';
import 'brace/theme/github';
+import fixAceEditor from './../ace-editor-fix';
+
class ThingsboardAceEditor extends React.Component {
constructor(props) {
@@ -31,6 +33,7 @@ class ThingsboardAceEditor extends React.Component {
this.onBlur = this.onBlur.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onTidy = this.onTidy.bind(this);
+ this.onLoad = this.onLoad.bind(this);
var value = props.value ? props.value + '' : '';
this.state = {
value: value,
@@ -72,6 +75,10 @@ class ThingsboardAceEditor extends React.Component {
}
}
+ onLoad(editor) {
+ fixAceEditor(editor);
+ }
+
render() {
const styles = reactCSS({
@@ -117,6 +124,7 @@ class ThingsboardAceEditor extends React.Component {
onChange={this.onValueChanged}
onFocus={this.onFocus}
onBlur={this.onBlur}
+ onLoad={this.onLoad}
name={this.props.form.title}
value={this.state.value}
readOnly={this.props.form.readonly}
diff --git a/ui/src/app/components/widget/widget-config.directive.js b/ui/src/app/components/widget/widget-config.directive.js
index d0ee6a3..4d4d958 100644
--- a/ui/src/app/components/widget/widget-config.directive.js
+++ b/ui/src/app/components/widget/widget-config.directive.js
@@ -23,6 +23,8 @@ import thingsboardJsonForm from '../json-form.directive';
import thingsboardManageWidgetActions from './action/manage-widget-actions.directive';
import 'angular-ui-ace';
+import fixAceEditor from './../ace-editor-fix';
+
import './widget-config.scss';
/* eslint-disable import/no-unresolved, import/default */
@@ -72,6 +74,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
+ },
+ onLoad: function (_ace) {
+ fixAceEditor(_ace);
}
};
diff --git a/ui/src/app/entity/entity-autocomplete.directive.js b/ui/src/app/entity/entity-autocomplete.directive.js
index c2053c0..e46c614 100644
--- a/ui/src/app/entity/entity-autocomplete.directive.js
+++ b/ui/src/app/entity/entity-autocomplete.directive.js
@@ -131,17 +131,11 @@ export default function EntityAutocomplete($compile, $templateCache, $q, $filter
scope.noEntitiesMatchingText = 'device.no-devices-matching';
scope.entityRequiredText = 'device.device-required';
break;
- case types.entityType.rule:
- scope.selectEntityText = 'rule.select-rule';
- scope.entityText = 'rule.rule';
- scope.noEntitiesMatchingText = 'rule.no-rules-matching';
- scope.entityRequiredText = 'rule.rule-required';
- break;
- case types.entityType.plugin:
- scope.selectEntityText = 'plugin.select-plugin';
- scope.entityText = 'plugin.plugin';
- scope.noEntitiesMatchingText = 'plugin.no-plugins-matching';
- scope.entityRequiredText = 'plugin.plugin-required';
+ case types.entityType.rulechain:
+ scope.selectEntityText = 'rulechain.select-rulechain';
+ scope.entityText = 'rulechain.rulechain';
+ scope.noEntitiesMatchingText = 'rulechain.no-rulechains-matching';
+ scope.entityRequiredText = 'rulechain.rulechain-required';
break;
case types.entityType.tenant:
scope.selectEntityText = 'tenant.select-tenant';
diff --git a/ui/src/app/entity/entity-type-list.tpl.html b/ui/src/app/entity/entity-type-list.tpl.html
index eb32124..d7d3918 100644
--- a/ui/src/app/entity/entity-type-list.tpl.html
+++ b/ui/src/app/entity/entity-type-list.tpl.html
@@ -16,8 +16,7 @@
-->
<section flex layout='column' class="tb-entity-type-list">
- <md-chips flex
- readonly="disabled"
+ <md-chips readonly="disabled"
id="entity_type_list_chips"
ng-required="tbRequired"
ng-model="entityTypeList"
diff --git a/ui/src/app/entity/relation/relation-filters.directive.js b/ui/src/app/entity/relation/relation-filters.directive.js
index 00d3b26..9ab66ca 100644
--- a/ui/src/app/entity/relation/relation-filters.directive.js
+++ b/ui/src/app/entity/relation/relation-filters.directive.js
@@ -46,6 +46,7 @@ export default function RelationFilters($compile, $templateCache) {
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
+ scope.relationFilters.length = 0;
value.forEach(function (filter) {
scope.relationFilters.push(filter);
});
diff --git a/ui/src/app/entity/relation/relation-filters.scss b/ui/src/app/entity/relation/relation-filters.scss
index 649879d..34254d0 100644
--- a/ui/src/app/entity/relation/relation-filters.scss
+++ b/ui/src/app/entity/relation/relation-filters.scss
@@ -51,9 +51,6 @@
md-chips-wrap {
padding: 0px;
margin: 0 0 24px;
- .md-chip-input-container {
- margin: 0;
- }
md-autocomplete {
height: 30px;
md-autocomplete-wrap {
ui/src/app/event/event.scss 25(+21 -4)
diff --git a/ui/src/app/event/event.scss b/ui/src/app/event/event.scss
index b3be35c..b0fc46f 100644
--- a/ui/src/app/event/event.scss
+++ b/ui/src/app/event/event.scss
@@ -24,6 +24,17 @@ md-list.tb-event-table {
height: 48px;
padding: 0px;
overflow: hidden;
+ .tb-cell {
+ text-overflow: ellipsis;
+ &.tb-scroll {
+ white-space: nowrap;
+ overflow-y: hidden;
+ overflow-x: auto;
+ }
+ &.tb-nowrap {
+ white-space: nowrap;
+ }
+ }
}
.tb-row:hover {
@@ -39,13 +50,19 @@ md-list.tb-event-table {
color: rgba(0,0,0,.54);
font-size: 12px;
font-weight: 700;
- white-space: nowrap;
background: none;
+ white-space: nowrap;
}
}
.tb-cell {
- padding: 0 24px;
+ &:first-child {
+ padding-left: 14px;
+ }
+ &:last-child {
+ padding-right: 14px;
+ }
+ padding: 0 6px;
margin: auto 0;
color: rgba(0,0,0,.87);
font-size: 13px;
@@ -53,8 +70,8 @@ md-list.tb-event-table {
text-align: left;
overflow: hidden;
.md-button {
- padding: 0;
- margin: 0;
+ padding: 0;
+ margin: 0;
}
}
diff --git a/ui/src/app/event/event-content-dialog.controller.js b/ui/src/app/event/event-content-dialog.controller.js
index 108f95e..b780b78 100644
--- a/ui/src/app/event/event-content-dialog.controller.js
+++ b/ui/src/app/event/event-content-dialog.controller.js
@@ -17,11 +17,14 @@ import $ from 'jquery';
import 'brace/ext/language_tools';
import 'brace/mode/java';
import 'brace/theme/github';
+import beautify from 'js-beautify';
/* eslint-disable angular/angularelement */
+const js_beautify = beautify.js;
+
/*@ngInject*/
-export default function EventContentDialogController($mdDialog, content, title, showingCallback) {
+export default function EventContentDialogController($mdDialog, types, content, contentType, title, showingCallback) {
var vm = this;
@@ -32,9 +35,19 @@ export default function EventContentDialogController($mdDialog, content, title,
vm.content = content;
vm.title = title;
+ var mode;
+ if (contentType) {
+ mode = types.contentType[contentType].code;
+ if (contentType == types.contentType.JSON.value && vm.content) {
+ vm.content = js_beautify(vm.content, {indent_size: 4});
+ }
+ } else {
+ mode = 'java';
+ }
+
vm.contentOptions = {
useWrapMode: false,
- mode: 'java',
+ mode: mode,
showGutter: false,
showPrintMargin: false,
theme: 'github',
@@ -55,7 +68,7 @@ export default function EventContentDialogController($mdDialog, content, title,
var lines = vm.content.split('\n');
newHeight = 16 * lines.length + 16;
var maxLineLength = 0;
- for (var i in lines) {
+ for (var i = 0; i < lines.length; i++) {
var line = lines[i].replace(/\t/g, ' ').replace(/\n/g, '');
var lineLength = line.length;
maxLineLength = Math.max(maxLineLength, lineLength);
diff --git a/ui/src/app/event/event-header.directive.js b/ui/src/app/event/event-header.directive.js
index afac804..bc4cdbe 100644
--- a/ui/src/app/event/event-header.directive.js
+++ b/ui/src/app/event/event-header.directive.js
@@ -18,6 +18,7 @@
import eventHeaderLcEventTemplate from './event-header-lc-event.tpl.html';
import eventHeaderStatsTemplate from './event-header-stats.tpl.html';
import eventHeaderErrorTemplate from './event-header-error.tpl.html';
+import eventHeaderDebugRuleNodeTemplate from './event-header-debug-rulenode.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
@@ -38,6 +39,12 @@ export default function EventHeaderDirective($compile, $templateCache, types) {
case types.eventType.error.value:
template = eventHeaderErrorTemplate;
break;
+ case types.debugEventType.debugRuleNode.value:
+ template = eventHeaderDebugRuleNodeTemplate;
+ break;
+ case types.debugEventType.debugRuleChain.value:
+ template = eventHeaderDebugRuleNodeTemplate;
+ break;
}
return $templateCache.get(template);
}
diff --git a/ui/src/app/event/event-header-debug-rulenode.tpl.html b/ui/src/app/event/event-header-debug-rulenode.tpl.html
new file mode 100644
index 0000000..bbf6ae0
--- /dev/null
+++ b/ui/src/app/event/event-header-debug-rulenode.tpl.html
@@ -0,0 +1,27 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div hide-xs hide-sm translate class="tb-cell" flex="25">event.event-time</div>
+<div translate class="tb-cell" flex="20">event.server</div>
+<div translate class="tb-cell" flex="10">event.type</div>
+<div translate class="tb-cell" flex="15">event.entity</div>
+<div translate class="tb-cell" flex="20">event.message-id</div>
+<div translate class="tb-cell" flex="20">event.message-type</div>
+<div translate class="tb-cell" flex="15">event.relation-type</div>
+<div translate class="tb-cell" flex="10">event.data</div>
+<div translate class="tb-cell" flex="10">event.metadata</div>
+<div translate class="tb-cell" flex="10">event.error</div>
ui/src/app/event/event-row.directive.js 24(+22 -2)
diff --git a/ui/src/app/event/event-row.directive.js b/ui/src/app/event/event-row.directive.js
index f005542..b808fb8 100644
--- a/ui/src/app/event/event-row.directive.js
+++ b/ui/src/app/event/event-row.directive.js
@@ -20,6 +20,7 @@ import eventErrorDialogTemplate from './event-content-dialog.tpl.html';
import eventRowLcEventTemplate from './event-row-lc-event.tpl.html';
import eventRowStatsTemplate from './event-row-stats.tpl.html';
import eventRowErrorTemplate from './event-row-error.tpl.html';
+import eventRowDebugRuleNodeTemplate from './event-row-debug-rulenode.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
@@ -40,6 +41,12 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
case types.eventType.error.value:
template = eventRowErrorTemplate;
break;
+ case types.debugEventType.debugRuleNode.value:
+ template = eventRowDebugRuleNodeTemplate;
+ break;
+ case types.debugEventType.debugRuleChain.value:
+ template = eventRowDebugRuleNodeTemplate;
+ break;
}
return $templateCache.get(template);
}
@@ -53,17 +60,22 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
scope.loadTemplate();
});
+ scope.types = types;
+
scope.event = attrs.event;
- scope.showContent = function($event, content, title) {
+ scope.showContent = function($event, content, title, contentType) {
var onShowingCallback = {
onShowing: function(){}
}
+ if (!contentType) {
+ contentType = null;
+ }
$mdDialog.show({
controller: 'EventContentDialogController',
controllerAs: 'vm',
templateUrl: eventErrorDialogTemplate,
- locals: {content: content, title: title, showingCallback: onShowingCallback},
+ locals: {content: content, title: title, contentType: contentType, showingCallback: onShowingCallback},
parent: angular.element($document[0].body),
fullscreen: true,
targetEvent: $event,
@@ -74,6 +86,14 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
});
}
+ scope.checkTooltip = function($event) {
+ var el = $event.target;
+ var $el = angular.element(el);
+ if(el.offsetWidth < el.scrollWidth && !$el.attr('title')){
+ $el.attr('title', $el.text());
+ }
+ }
+
$compile(element.contents())(scope);
}
diff --git a/ui/src/app/event/event-row-debug-rulenode.tpl.html b/ui/src/app/event/event-row-debug-rulenode.tpl.html
new file mode 100644
index 0000000..857cd8c
--- /dev/null
+++ b/ui/src/app/event/event-row-debug-rulenode.tpl.html
@@ -0,0 +1,63 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div hide-xs hide-sm class="tb-cell" flex="25">{{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}</div>
+<div class="tb-cell" flex="20">{{event.body.server}}</div>
+<div class="tb-cell" flex="10">{{event.body.type}}</div>
+<div class="tb-cell" flex="15">{{event.body.entityName}}</div>
+<div class="tb-cell tb-nowrap" flex="20" ng-mouseenter="checkTooltip($event)">{{event.body.msgId}}</div>
+<div class="tb-cell" flex="20" ng-mouseenter="checkTooltip($event)">{{event.body.msgType}}</div>
+<div class="tb-cell" flex="15">{{event.body.relationType}}</div>
+<div class="tb-cell" flex="10">
+ <md-button ng-if="event.body.data" class="md-icon-button md-primary"
+ ng-click="showContent($event, event.body.data, 'event.data', event.body.dataType)"
+ aria-label="{{ 'action.view' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.view' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.view' | translate }}"
+ class="material-icons">
+ more_horiz
+ </md-icon>
+ </md-button>
+</div>
+<div class="tb-cell" flex="10">
+ <md-button ng-if="event.body.metadata" class="md-icon-button md-primary"
+ ng-click="showContent($event, event.body.metadata, 'event.metadata', 'JSON')"
+ aria-label="{{ 'action.view' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.view' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.view' | translate }}"
+ class="material-icons">
+ more_horiz
+ </md-icon>
+ </md-button>
+</div>
+<div class="tb-cell" flex="10">
+ <md-button ng-if="event.body.error" class="md-icon-button md-primary"
+ ng-click="showContent($event, event.body.error, 'event.error')"
+ aria-label="{{ 'action.view' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.view' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.view' | translate }}"
+ class="material-icons">
+ more_horiz
+ </md-icon>
+ </md-button>
+</div>
ui/src/app/event/event-table.directive.js 18(+15 -3)
diff --git a/ui/src/app/event/event-table.directive.js b/ui/src/app/event/event-table.directive.js
index 4291014..c61078d 100644
--- a/ui/src/app/event/event-table.directive.js
+++ b/ui/src/app/event/event-table.directive.js
@@ -36,8 +36,8 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
for (var type in types.eventType) {
var eventType = types.eventType[type];
var enabled = true;
- for (var disabledType in disabledEventTypes) {
- if (eventType.value === disabledEventTypes[disabledType]) {
+ for (var i=0;i<disabledEventTypes.length;i++) {
+ if (eventType.value === disabledEventTypes[i]) {
enabled = false;
break;
}
@@ -47,7 +47,19 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
}
}
} else {
- scope.eventTypes = types.eventType;
+ scope.eventTypes = angular.copy(types.eventType);
+ }
+
+ if (attrs.debugEventTypes) {
+ var debugEventTypes = attrs.debugEventTypes.split(',');
+ for (i=0;i<debugEventTypes.length;i++) {
+ for (type in types.debugEventType) {
+ eventType = types.debugEventType[type];
+ if (eventType.value === debugEventTypes[i]) {
+ scope.eventTypes[type] = eventType;
+ }
+ }
+ }
}
scope.eventType = attrs.defaultEventType;
ui/src/app/help/help.directive.js 4(+4 -0)
diff --git a/ui/src/app/help/help.directive.js b/ui/src/app/help/help.directive.js
index bc7e84f..9227d44 100644
--- a/ui/src/app/help/help.directive.js
+++ b/ui/src/app/help/help.directive.js
@@ -35,6 +35,10 @@ function Help($compile, $window, helpLinks) {
$event.stopPropagation();
}
var helpUrl = helpLinks.linksMap[scope.helpLinkId];
+ if (!helpUrl && scope.helpLinkId &&
+ (scope.helpLinkId.startsWith('http://') || scope.helpLinkId.startsWith('https://'))) {
+ helpUrl = scope.helpLinkId;
+ }
if (helpUrl) {
$window.open(helpUrl, '_blank');
}
ui/src/app/help/help-links.constant.js 158(+72 -86)
diff --git a/ui/src/app/help/help-links.constant.js b/ui/src/app/help/help-links.constant.js
index 9b1f84f..37ed579 100644
--- a/ui/src/app/help/help-links.constant.js
+++ b/ui/src/app/help/help-links.constant.js
@@ -13,36 +13,37 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-var pluginClazzHelpLinkMap = {
- 'org.thingsboard.server.extensions.core.plugin.messaging.DeviceMessagingPlugin': 'pluginDeviceMessaging',
- 'org.thingsboard.server.extensions.core.plugin.telemetry.TelemetryStoragePlugin': 'pluginTelemetryStorage',
- 'org.thingsboard.server.extensions.core.plugin.rpc.RpcPlugin': 'pluginRpcPlugin',
- 'org.thingsboard.server.extensions.core.plugin.mail.MailPlugin': 'pluginMailPlugin',
- 'org.thingsboard.server.extensions.rest.plugin.RestApiCallPlugin': 'pluginRestApiCallPlugin',
- 'org.thingsboard.server.extensions.core.plugin.time.TimePlugin': 'pluginTimePlugin',
- 'org.thingsboard.server.extensions.kafka.plugin.KafkaPlugin': 'pluginKafkaPlugin',
- 'org.thingsboard.server.extensions.rabbitmq.plugin.RabbitMqPlugin': 'pluginRabbitMqPlugin'
-};
-
-var filterClazzHelpLinkMap = {
- 'org.thingsboard.server.extensions.core.filter.MsgTypeFilter': 'filterMsgType',
- 'org.thingsboard.server.extensions.core.filter.DeviceTelemetryFilter': 'filterDeviceTelemetry',
- 'org.thingsboard.server.extensions.core.filter.MethodNameFilter': 'filterMethodName',
- 'org.thingsboard.server.extensions.core.filter.DeviceAttributesFilter': 'filterDeviceAttributes'
-};
-
-var processorClazzHelpLinkMap = {
- 'org.thingsboard.server.extensions.core.processor.AlarmDeduplicationProcessor': 'processorAlarmDeduplication'
-};
-
-var pluginActionsClazzHelpLinkMap = {
- 'org.thingsboard.server.extensions.core.action.rpc.RpcPluginAction': 'pluginActionRpc',
- 'org.thingsboard.server.extensions.core.action.mail.SendMailAction': 'pluginActionSendMail',
- 'org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction': 'pluginActionTelemetry',
- 'org.thingsboard.server.extensions.kafka.action.KafkaPluginAction': 'pluginActionKafka',
- 'org.thingsboard.server.extensions.rabbitmq.action.RabbitMqPluginAction': 'pluginActionRabbitMq',
- 'org.thingsboard.server.extensions.rest.action.RestApiCallPluginAction': 'pluginActionRestApiCall'
+var ruleNodeClazzHelpLinkMap = {
+ 'org.thingsboard.rule.engine.filter.TbJsFilterNode': 'ruleNodeJsFilter',
+ 'org.thingsboard.rule.engine.filter.TbJsSwitchNode': 'ruleNodeJsSwitch',
+ 'org.thingsboard.rule.engine.filter.TbMsgTypeFilterNode': 'ruleNodeMessageTypeFilter',
+ 'org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode': 'ruleNodeMessageTypeSwitch',
+ 'org.thingsboard.rule.engine.filter.TbOriginatorTypeSwitchNode': 'ruleNodeOriginatorTypeSwitch',
+ 'org.thingsboard.rule.engine.metadata.TbGetAttributesNode': 'ruleNodeOriginatorAttributes',
+ 'org.thingsboard.rule.engine.metadata.TbGetCustomerAttributeNode': 'ruleNodeCustomerAttributes',
+ 'org.thingsboard.rule.engine.metadata.TbGetDeviceAttrNode': 'ruleNodeDeviceAttributes',
+ 'org.thingsboard.rule.engine.metadata.TbGetRelatedAttributeNode': 'ruleNodeRelatedAttributes',
+ 'org.thingsboard.rule.engine.metadata.TbGetTenantAttributeNode': 'ruleNodeTenantAttributes',
+ 'org.thingsboard.rule.engine.transform.TbChangeOriginatorNode': 'ruleNodeChangeOriginator',
+ 'org.thingsboard.rule.engine.transform.TbTransformMsgNode': 'ruleNodeTransformMsg',
+ 'org.thingsboard.rule.engine.mail.TbMsgToEmailNode': 'ruleNodeMsgToEmail',
+ 'org.thingsboard.rule.engine.action.TbClearAlarmNode': 'ruleNodeClearAlarm',
+ 'org.thingsboard.rule.engine.action.TbCreateAlarmNode': 'ruleNodeCrateAlarm',
+ 'org.thingsboard.rule.engine.debug.TbMsgGeneratorNode': 'ruleNodeMsgGenerator',
+ 'org.thingsboard.rule.engine.action.TbLogNode': 'ruleNodeLog',
+ 'org.thingsboard.rule.engine.rpc.TbSendRPCReplyNode': 'ruleNodeRpcCallReply',
+ 'org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode': 'ruleNodeRpcCallRequest',
+ 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode': 'ruleNodeSaveAttributes',
+ 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode': 'ruleNodeSaveTimeseries',
+ 'tb.internal.RuleChain': 'ruleNodeRuleChain',
+ 'org.thingsboard.rule.engine.aws.sns.TbSnsNode': 'ruleNodeAwsSns',
+ 'org.thingsboard.rule.engine.aws.sqs.TbSqsNode': 'ruleNodeAwsSqs',
+ 'org.thingsboard.rule.engine.kafka.TbKafkaNode': 'ruleNodeKafka',
+ 'org.thingsboard.rule.engine.mqtt.TbMqttNode': 'ruleNodeMqtt',
+ 'org.thingsboard.rule.engine.rabbitmq.TbRabbitMqNode': 'ruleNodeRabbitMq',
+ 'org.thingsboard.rule.engine.rest.TbRestApiCallNode': 'ruleNodeRestApiCall',
+ 'org.thingsboard.rule.engine.mail.TbSendEmailNode': 'ruleNodeSendEmail'
};
var helpBaseUrl = "https://thingsboard.io";
@@ -52,30 +53,37 @@ export default angular.module('thingsboard.help', [])
{
linksMap: {
outgoingMailSettings: helpBaseUrl + "/docs/user-guide/ui/mail-settings",
- plugins: helpBaseUrl + "/docs/user-guide/rule-engine/#plugins",
- pluginDeviceMessaging: helpBaseUrl + "/docs/reference/plugins/messaging/",
- pluginTelemetryStorage: helpBaseUrl + "/docs/reference/plugins/telemetry/",
- pluginRpcPlugin: helpBaseUrl + "/docs/reference/plugins/rpc/",
- pluginMailPlugin: helpBaseUrl + "/docs/reference/plugins/mail/",
- pluginRestApiCallPlugin: helpBaseUrl + "/docs/reference/plugins/rest/",
- pluginTimePlugin: helpBaseUrl + "/docs/reference/plugins/time/",
- pluginKafkaPlugin: helpBaseUrl + "/docs/reference/plugins/kafka/",
- pluginRabbitMqPlugin: helpBaseUrl + "/docs/reference/plugins/rabbitmq/",
- rules: helpBaseUrl + "/docs/user-guide/rule-engine/#rules",
- filters: helpBaseUrl + "/docs/user-guide/rule-engine/#filters",
- filterMsgType: helpBaseUrl + "/docs/reference/filters/message-type-filter",
- filterDeviceTelemetry: helpBaseUrl + "/docs/reference/filters/device-telemetry-filter",
- filterMethodName: helpBaseUrl + "/docs/reference/filters/method-name-filter/",
- filterDeviceAttributes: helpBaseUrl + "/docs/reference/filters/device-attributes-filter",
- processors: helpBaseUrl + "/docs/user-guide/rule-engine/#processors",
- processorAlarmDeduplication: "http://thingsboard.io/docs/#q=processorAlarmDeduplication",
- pluginActions: helpBaseUrl + "/docs/user-guide/rule-engine/#actions",
- pluginActionRpc: helpBaseUrl + "/docs/reference/actions/rpc-plugin-action",
- pluginActionSendMail: helpBaseUrl + "/docs/reference/actions/send-mail-action",
- pluginActionTelemetry: helpBaseUrl + "/docs/reference/actions/telemetry-plugin-action/",
- pluginActionKafka: helpBaseUrl + "/docs/reference/actions/kafka-plugin-action",
- pluginActionRabbitMq: helpBaseUrl + "/docs/reference/actions/rabbitmq-plugin-action",
- pluginActionRestApiCall: helpBaseUrl + "/docs/reference/actions/rest-api-call-plugin-action",
+ ruleEngine: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/overview/",
+ ruleNodeJsFilter: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#script-filter-node",
+ ruleNodeJsSwitch: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#switch-node",
+ ruleNodeMessageTypeFilter: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#message-type-filter-node",
+ ruleNodeMessageTypeSwitch: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#message-type-switch-node",
+ ruleNodeOriginatorTypeSwitch: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#originator-type-switch-node",
+ ruleNodeOriginatorAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#originator-attributes",
+ ruleNodeCustomerAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#customer-attributes",
+ ruleNodeDeviceAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#device-attributes",
+ ruleNodeRelatedAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#related-attributes",
+ ruleNodeTenantAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#tenant-attributes",
+ ruleNodeChangeOriginator: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/transformation-nodes/#change-originator",
+ ruleNodeTransformMsg: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/transformation-nodes/#script-transformation-node",
+ ruleNodeMsgToEmail: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/transformation-nodes/#to-email-node",
+ ruleNodeClearAlarm: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#clear-alarm-node",
+ ruleNodeCrateAlarm: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#create-alarm-node",
+ ruleNodeMsgGenerator: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#generator-node",
+ ruleNodeLog: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#log-node",
+ ruleNodeRpcCallReply: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#rpc-call-reply-node",
+ ruleNodeRpcCallRequest: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#rpc-call-request-node",
+ ruleNodeSaveAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#save-attributes-node",
+ ruleNodeSaveTimeseries: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#save-timeseries-node",
+ ruleNodeRuleChain: helpBaseUrl + "/docs/user-guide/ui/rule-chains/",
+ ruleNodeAwsSns: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#aws-sns-node",
+ ruleNodeAwsSqs: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#aws-sqs-node",
+ ruleNodeKafka: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#kafka-node",
+ ruleNodeMqtt: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#mqtt-node",
+ ruleNodeRabbitMq: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#rabbitmq-node",
+ ruleNodeRestApiCall: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#rest-api-call-node",
+ ruleNodeSendEmail: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#send-email-node",
+ rulechains: helpBaseUrl + "/docs/user-guide/ui/rule-chains/",
tenants: helpBaseUrl + "/docs/user-guide/ui/tenants",
customers: helpBaseUrl + "/docs/user-guide/ui/customers",
assets: helpBaseUrl + "/docs/user-guide/ui/assets",
@@ -90,41 +98,19 @@ export default angular.module('thingsboard.help', [])
widgetsConfigAlarm: helpBaseUrl + "/docs/user-guide/ui/dashboards#alarm",
widgetsConfigStatic: helpBaseUrl + "/docs/user-guide/ui/dashboards#static",
},
- getPluginLink: function(plugin) {
- var link = 'plugins';
- if (plugin && plugin.clazz) {
- if (pluginClazzHelpLinkMap[plugin.clazz]) {
- link = pluginClazzHelpLinkMap[plugin.clazz];
- }
- }
- return link;
- },
- getFilterLink: function(filter) {
- var link = 'filters';
- if (filter && filter.clazz) {
- if (filterClazzHelpLinkMap[filter.clazz]) {
- link = filterClazzHelpLinkMap[filter.clazz];
- }
- }
- return link;
- },
- getProcessorLink: function(processor) {
- var link = 'processors';
- if (processor && processor.clazz) {
- if (processorClazzHelpLinkMap[processor.clazz]) {
- link = processorClazzHelpLinkMap[processor.clazz];
- }
- }
- return link;
- },
- getPluginActionLink: function(pluginAction) {
- var link = 'pluginActions';
- if (pluginAction && pluginAction.clazz) {
- if (pluginActionsClazzHelpLinkMap[pluginAction.clazz]) {
- link = pluginActionsClazzHelpLinkMap[pluginAction.clazz];
+ getRuleNodeLink: function(ruleNode) {
+ if (ruleNode && ruleNode.component) {
+ if (ruleNode.component.configurationDescriptor &&
+ ruleNode.component.configurationDescriptor.nodeDefinition &&
+ ruleNode.component.configurationDescriptor.nodeDefinition.docUrl) {
+ return ruleNode.component.configurationDescriptor.nodeDefinition.docUrl;
+ } else if (ruleNode.component.clazz) {
+ if (ruleNodeClazzHelpLinkMap[ruleNode.component.clazz]) {
+ return ruleNodeClazzHelpLinkMap[ruleNode.component.clazz];
+ }
}
}
- return link;
+ return 'ruleEngine';
}
}
).name;
ui/src/app/import-export/import-export.service.js 148(+57 -91)
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
index 6071fd2..d64441f 100644
--- a/ui/src/app/import-export/import-export.service.js
+++ b/ui/src/app/import-export/import-export.service.js
@@ -25,8 +25,7 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
/*@ngInject*/
export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types,
- dashboardUtils, entityService, dashboardService, pluginService, ruleService,
- widgetService, toast, attributeService) {
+ dashboardUtils, entityService, dashboardService, ruleChainService, widgetService, toast, attributeService) {
var service = {
@@ -34,10 +33,8 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
importDashboard: importDashboard,
exportWidget: exportWidget,
importWidget: importWidget,
- exportPlugin: exportPlugin,
- importPlugin: importPlugin,
- exportRule: exportRule,
- importRule: importRule,
+ exportRuleChain: exportRuleChain,
+ importRuleChain: importRuleChain,
exportWidgetType: exportWidgetType,
importWidgetType: importWidgetType,
exportWidgetsBundle: exportWidgetsBundle,
@@ -219,101 +216,68 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
return true;
}
- // Rule functions
+ // Rule chain functions
- function exportRule(ruleId) {
- ruleService.getRule(ruleId).then(
- function success(rule) {
- var name = rule.name;
- name = name.toLowerCase().replace(/\W/g,"_");
- exportToPc(prepareExport(rule), name + '.json');
+ function exportRuleChain(ruleChainId) {
+ ruleChainService.getRuleChain(ruleChainId).then(
+ (ruleChain) => {
+ ruleChainService.getRuleChainMetaData(ruleChainId).then(
+ (ruleChainMetaData) => {
+ var ruleChainExport = {
+ ruleChain: prepareRuleChain(ruleChain),
+ metadata: prepareRuleChainMetaData(ruleChainMetaData)
+ };
+ var name = ruleChain.name;
+ name = name.toLowerCase().replace(/\W/g,"_");
+ exportToPc(ruleChainExport, name + '.json');
+ },
+ (rejection) => {
+ processExportRuleChainRejection(rejection);
+ }
+ );
},
- function fail(rejection) {
- var message = rejection;
- if (!message) {
- message = $translate.instant('error.unknown-error');
- }
- toast.showError($translate.instant('rule.export-failed-error', {error: message}));
+ (rejection) => {
+ processExportRuleChainRejection(rejection);
}
);
}
- function importRule($event) {
- var deferred = $q.defer();
- openImportDialog($event, 'rule.import', 'rule.rule-file').then(
- function success(rule) {
- if (!validateImportedRule(rule)) {
- toast.showError($translate.instant('rule.invalid-rule-file-error'));
- deferred.reject();
- } else {
- rule.state = 'SUSPENDED';
- ruleService.saveRule(rule).then(
- function success() {
- deferred.resolve();
- },
- function fail() {
- deferred.reject();
- }
- );
- }
- },
- function fail() {
- deferred.reject();
- }
- );
- return deferred.promise;
+ function prepareRuleChain(ruleChain) {
+ ruleChain = prepareExport(ruleChain);
+ if (ruleChain.firstRuleNodeId) {
+ ruleChain.firstRuleNodeId = null;
+ }
+ ruleChain.root = false;
+ return ruleChain;
}
- function validateImportedRule(rule) {
- if (angular.isUndefined(rule.name)
- || angular.isUndefined(rule.pluginToken)
- || angular.isUndefined(rule.filters)
- || angular.isUndefined(rule.action))
- {
- return false;
+ function prepareRuleChainMetaData(ruleChainMetaData) {
+ delete ruleChainMetaData.ruleChainId;
+ for (var i=0;i<ruleChainMetaData.nodes.length;i++) {
+ var node = ruleChainMetaData.nodes[i];
+ delete node.ruleChainId;
+ ruleChainMetaData.nodes[i] = prepareExport(node);
}
- return true;
+ return ruleChainMetaData;
}
- // Plugin functions
-
- function exportPlugin(pluginId) {
- pluginService.getPlugin(pluginId).then(
- function success(plugin) {
- if (!plugin.configuration || plugin.configuration === null) {
- plugin.configuration = {};
- }
- var name = plugin.name;
- name = name.toLowerCase().replace(/\W/g,"_");
- exportToPc(prepareExport(plugin), name + '.json');
- },
- function fail(rejection) {
- var message = rejection;
- if (!message) {
- message = $translate.instant('error.unknown-error');
- }
- toast.showError($translate.instant('plugin.export-failed-error', {error: message}));
- }
- );
+ function processExportRuleChainRejection(rejection) {
+ var message = rejection;
+ if (!message) {
+ message = $translate.instant('error.unknown-error');
+ }
+ toast.showError($translate.instant('rulechain.export-failed-error', {error: message}));
}
- function importPlugin($event) {
+ function importRuleChain($event) {
var deferred = $q.defer();
- openImportDialog($event, 'plugin.import', 'plugin.plugin-file').then(
- function success(plugin) {
- if (!validateImportedPlugin(plugin)) {
- toast.showError($translate.instant('plugin.invalid-plugin-file-error'));
+ openImportDialog($event, 'rulechain.import', 'rulechain.rulechain-file').then(
+ function success(ruleChainImport) {
+ if (!validateImportedRuleChain(ruleChainImport)) {
+ toast.showError($translate.instant('rulechain.invalid-rulechain-file-error'));
deferred.reject();
} else {
- plugin.state = 'SUSPENDED';
- pluginService.savePlugin(plugin).then(
- function success() {
- deferred.resolve();
- },
- function fail() {
- deferred.reject();
- }
- );
+ deferred.resolve(ruleChainImport);
}
},
function fail() {
@@ -323,12 +287,14 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
return deferred.promise;
}
- function validateImportedPlugin(plugin) {
- if (angular.isUndefined(plugin.name)
- || angular.isUndefined(plugin.clazz)
- || angular.isUndefined(plugin.apiToken)
- || angular.isUndefined(plugin.configuration))
- {
+ function validateImportedRuleChain(ruleChainImport) {
+ if (angular.isUndefined(ruleChainImport.ruleChain)) {
+ return false;
+ }
+ if (angular.isUndefined(ruleChainImport.metadata)) {
+ return false;
+ }
+ if (angular.isUndefined(ruleChainImport.ruleChain.name)) {
return false;
}
return true;
ui/src/app/layout/index.js 14(+9 -5)
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
index e5ca958..8f2958d 100644
--- a/ui/src/app/layout/index.js
+++ b/ui/src/app/layout/index.js
@@ -29,6 +29,9 @@ import thingsboardNoAnimate from '../components/no-animate.directive';
import thingsboardOnFinishRender from '../components/finish-render.directive';
import thingsboardSideMenu from '../components/side-menu.directive';
import thingsboardDashboardAutocomplete from '../components/dashboard-autocomplete.directive';
+import thingsboardKvMap from '../components/kv-map.directive';
+import thingsboardJsonObjectEdit from '../components/json-object-edit.directive';
+import thingsboardJsonContent from '../components/json-content.directive';
import thingsboardUserMenu from './user-menu.directive';
@@ -47,8 +50,7 @@ import thingsboardAsset from '../asset';
import thingsboardDevice from '../device';
import thingsboardWidgetLibrary from '../widget';
import thingsboardDashboard from '../dashboard';
-import thingsboardPlugin from '../plugin';
-import thingsboardRule from '../rule';
+import thingsboardRuleChain from '../rulechain';
import thingsboardJsonForm from '../jsonform';
@@ -79,8 +81,7 @@ export default angular.module('thingsboard.home', [
thingsboardDevice,
thingsboardWidgetLibrary,
thingsboardDashboard,
- thingsboardPlugin,
- thingsboardRule,
+ thingsboardRuleChain,
thingsboardJsonForm,
thingsboardApiDevice,
thingsboardApiLogin,
@@ -88,7 +89,10 @@ export default angular.module('thingsboard.home', [
thingsboardNoAnimate,
thingsboardOnFinishRender,
thingsboardSideMenu,
- thingsboardDashboardAutocomplete
+ thingsboardDashboardAutocomplete,
+ thingsboardKvMap,
+ thingsboardJsonObjectEdit,
+ thingsboardJsonContent
])
.config(HomeRoutes)
.controller('HomeController', HomeController)
ui/src/app/locale/locale.constant.js 208(+113 -95)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 0f0a1df..5bb599c 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -43,6 +43,7 @@ export default angular.module('thingsboard.locale', [])
"update": "Update",
"remove": "Remove",
"search": "Search",
+ "clear-search": "Clear search",
"assign": "Assign",
"unassign": "Unassign",
"share": "Share",
@@ -341,6 +342,11 @@ export default angular.module('thingsboard.locale', [])
"enter-password": "Enter password",
"enter-search": "Enter search"
},
+ "content-type": {
+ "json": "Json",
+ "text": "Text",
+ "binary": "Binary (Base64)"
+ },
"customer": {
"customer": "Customer",
"customers": "Customers",
@@ -745,6 +751,10 @@ export default angular.module('thingsboard.locale', [])
"type-alarms": "Alarms",
"list-of-alarms": "{ count, select, 1 {One alarms} other {List of # alarms} }",
"alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'",
+ "type-rulechain": "Rule chain",
+ "type-rulechains": "Rule chains",
+ "list-of-rulechains": "{ count, select, 1 {One rule chain} other {List of # rule chains} }",
+ "rulechain-name-starts-with": "Rule chains whose names start with '{{prefix}}'",
"type-current-customer": "Current Customer",
"search": "Search entities",
"selected-entities": "{ count, select, 1 {1 entity} other {# entities} } selected",
@@ -758,6 +768,8 @@ export default angular.module('thingsboard.locale', [])
"type-error": "Error",
"type-lc-event": "Lifecycle event",
"type-stats": "Statistics",
+ "type-debug-rule-node": "Debug",
+ "type-debug-rule-chain": "Debug",
"no-events-prompt": "No events found",
"error": "Error",
"alarm": "Alarm",
@@ -765,6 +777,14 @@ export default angular.module('thingsboard.locale', [])
"server": "Server",
"body": "Body",
"method": "Method",
+ "type": "Type",
+ "entity": "Entity",
+ "message-id": "Message Id",
+ "message-type": "Message Type",
+ "data-type": "Data Type",
+ "relation-type": "Relation Type",
+ "metadata": "Metadata",
+ "data": "Data",
"event": "Event",
"status": "Status",
"success": "Success",
@@ -971,7 +991,15 @@ export default angular.module('thingsboard.locale', [])
},
"js-func": {
"no-return-error": "Function must return value!",
- "return-type-mismatch": "Function must return value of '{{type}}' type!"
+ "return-type-mismatch": "Function must return value of '{{type}}' type!",
+ "tidy": "Tidy"
+ },
+ "key-val": {
+ "key": "Key",
+ "value": "Value",
+ "remove-entry": "Remove entry",
+ "add-entry": "Add entry",
+ "no-data": "No entries"
},
"layout": {
"layout": "Layout",
@@ -1011,47 +1039,6 @@ export default angular.module('thingsboard.locale', [])
"password-link-sent-message": "Password reset link was successfully sent!",
"email": "Email"
},
- "plugin": {
- "plugins": "Plugins",
- "delete": "Delete plugin",
- "activate": "Activate plugin",
- "suspend": "Suspend plugin",
- "active": "Active",
- "suspended": "Suspended",
- "name": "Name",
- "name-required": "Name is required.",
- "description": "Description",
- "add": "Add Plugin",
- "delete-plugin-title": "Are you sure you want to delete the plugin '{{pluginName}}'?",
- "delete-plugin-text": "Be careful, after the confirmation the plugin and all related data will become unrecoverable.",
- "delete-plugins-title": "Are you sure you want to delete { count, select, 1 {1 plugin} other {# plugins} }?",
- "delete-plugins-action-title": "Delete { count, select, 1 {1 plugin} other {# plugins} }",
- "delete-plugins-text": "Be careful, after the confirmation all selected plugins will be removed and all related data will become unrecoverable.",
- "add-plugin-text": "Add new plugin",
- "no-plugins-text": "No plugins found",
- "plugin-details": "Plugin details",
- "api-token": "API token",
- "api-token-required": "API token is required.",
- "type": "Plugin type",
- "type-required": "Plugin type is required.",
- "configuration": "Plugin configuration",
- "system": "System",
- "select-plugin": "Select plugin",
- "plugin": "Plugin",
- "no-plugins-matching": "No plugins matching '{{entity}}' were found.",
- "plugin-required": "Plugin is required.",
- "plugin-require-match": "Please select an existing plugin.",
- "events": "Events",
- "details": "Details",
- "import": "Import plugin",
- "export": "Export plugin",
- "export-failed-error": "Unable to export plugin: {{error}}",
- "create-new-plugin": "Create new plugin",
- "plugin-file": "Plugin file",
- "invalid-plugin-file-error": "Unable to import plugin: Invalid plugin data structure.",
- "copyId": "Copy plugin Id",
- "idCopiedMessage": "Plugin Id has been copied to clipboard"
- },
"position": {
"top": "Top",
"bottom": "Bottom",
@@ -1105,66 +1092,97 @@ export default angular.module('thingsboard.locale', [])
"additional-info": "Additional info (JSON)",
"invalid-additional-info": "Unable to parse additional info json."
},
- "rule": {
- "rule": "Rule",
- "rules": "Rules",
- "delete": "Delete rule",
- "activate": "Activate rule",
- "suspend": "Suspend rule",
- "active": "Active",
- "suspended": "Suspended",
+ "rulechain": {
+ "rulechain": "Rule chain",
+ "rulechains": "Rule chains",
+ "root": "Root",
+ "delete": "Delete rule chain",
"name": "Name",
"name-required": "Name is required.",
"description": "Description",
- "add": "Add Rule",
- "delete-rule-title": "Are you sure you want to delete the rule '{{ruleName}}'?",
- "delete-rule-text": "Be careful, after the confirmation the rule and all related data will become unrecoverable.",
- "delete-rules-title": "Are you sure you want to delete { count, select, 1 {1 rule} other {# rules} }?",
- "delete-rules-action-title": "Delete { count, select, 1 {1 rule} other {# rules} }",
- "delete-rules-text": "Be careful, after the confirmation all selected rules will be removed and all related data will become unrecoverable.",
- "add-rule-text": "Add new rule",
- "no-rules-text": "No rules found",
- "rule-details": "Rule details",
- "filters": "Filters",
- "filter": "Filter",
- "add-filter-prompt": "Please add filter",
- "remove-filter": "Remove filter",
- "add-filter": "Add filter",
- "filter-name": "Filter name",
- "filter-type": "Filter type",
- "edit-filter": "Edit filter",
- "view-filter": "View filter",
- "component-name": "Name",
- "component-name-required": "Name is required.",
- "component-type": "Type",
- "component-type-required": "Type is required.",
- "processor": "Processor",
- "no-processor-configured": "No processor configured",
- "create-processor": "Create processor",
- "processor-name": "Processor name",
- "processor-type": "Processor type",
- "plugin-action": "Plugin action",
- "action-name": "Action name",
- "action-type": "Action type",
- "create-action-prompt": "Please create action",
- "create-action": "Create action",
+ "add": "Add Rule Chain",
+ "set-root": "Make rule chain root",
+ "set-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' root?",
+ "set-root-rulechain-text": "After the confirmation the rule chain will become root and will handle all incoming transport messages.",
+ "delete-rulechain-title": "Are you sure you want to delete the rule chain '{{ruleChainName}}'?",
+ "delete-rulechain-text": "Be careful, after the confirmation the rule chain and all related data will become unrecoverable.",
+ "delete-rulechains-title": "Are you sure you want to delete { count, select, 1 {1 rule chain} other {# rule chains} }?",
+ "delete-rulechains-action-title": "Delete { count, select, 1 {1 rule chain} other {# rule chains} }",
+ "delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.",
+ "add-rulechain-text": "Add new rule chain",
+ "no-rulechains-text": "No rule chains found",
+ "rulechain-details": "Rule chain details",
"details": "Details",
"events": "Events",
"system": "System",
- "import": "Import rule",
- "export": "Export rule",
- "export-failed-error": "Unable to export rule: {{error}}",
- "create-new-rule": "Create new rule",
- "rule-file": "Rule file",
- "invalid-rule-file-error": "Unable to import rule: Invalid rule data structure.",
- "copyId": "Copy rule Id",
- "idCopiedMessage": "Rule Id has been copied to clipboard",
- "select-rule": "Select rule",
- "no-rules-matching": "No rules matching '{{entity}}' were found.",
- "rule-required": "Rule is required"
+ "import": "Import rule chain",
+ "export": "Export rule chain",
+ "export-failed-error": "Unable to export rule chain: {{error}}",
+ "create-new-rulechain": "Create new rule chain",
+ "rulechain-file": "Rule chain file",
+ "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.",
+ "copyId": "Copy rule chain Id",
+ "idCopiedMessage": "Rule chain Id has been copied to clipboard",
+ "select-rulechain": "Select rule chain",
+ "no-rulechains-matching": "No rule chains matching '{{entity}}' were found.",
+ "rulechain-required": "Rule chain is required",
+ "management": "Rules management",
+ "debug-mode": "Debug mode"
},
- "rule-plugin": {
- "management": "Rules and plugins management"
+ "rulenode": {
+ "details": "Details",
+ "events": "Events",
+ "search": "Search nodes",
+ "open-node-library": "Open node library",
+ "add": "Add rule node",
+ "name": "Name",
+ "name-required": "Name is required.",
+ "type": "Type",
+ "description": "Description",
+ "delete": "Delete rule node",
+ "select-all-objects": "Select all nodes and connections",
+ "deselect-all-objects": "Deselect all nodes and connections",
+ "delete-selected-objects": "Delete selected nodes and connections",
+ "delete-selected": "Delete selected",
+ "select-all": "Select all",
+ "copy-selected": "Copy selected",
+ "deselect-all": "Deselect all",
+ "rulenode-details": "Rule node details",
+ "debug-mode": "Debug mode",
+ "configuration": "Configuration",
+ "link": "Link",
+ "link-details": "Rule node link details",
+ "add-link": "Add link",
+ "link-label": "Link label",
+ "link-label-required": "Link label is required.",
+ "custom-link-label": "Custom link label",
+ "custom-link-label-required": "Custom link label is required.",
+ "type-filter": "Filter",
+ "type-filter-details": "Filter incoming messages with configured conditions",
+ "type-enrichment": "Enrichment",
+ "type-enrichment-details": "Add additional information into Message Metadata",
+ "type-transformation": "Transformation",
+ "type-transformation-details": "Change Message payload and Metadata",
+ "type-action": "Action",
+ "type-action-details": "Perform special action",
+ "type-external": "External",
+ "type-external-details": "Interacts with external system",
+ "type-rule-chain": "Rule Chain",
+ "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain",
+ "type-input": "Input",
+ "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node",
+ "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.",
+ "ui-resources-load-error": "Failed to load configuration ui resources.",
+ "invalid-target-rulechain": "Unable to resolve target rule chain!",
+ "test-script-function": "Test script function",
+ "message": "Message",
+ "message-type": "Message type",
+ "message-type-required": "Message type is required",
+ "metadata": "Metadata",
+ "metadata-required": "Metadata entries can't be empty.",
+ "output": "Output",
+ "test": "Test",
+ "help": "Help"
},
"tenant": {
"tenant": "Tenant",
ui/src/app/rulechain/add-link.tpl.html 48(+48 -0)
diff --git a/ui/src/app/rulechain/add-link.tpl.html b/ui/src/app/rulechain/add-link.tpl.html
new file mode 100644
index 0000000..0a21104
--- /dev/null
+++ b/ui/src/app/rulechain/add-link.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'rulenode.add-link' | translate }}" tb-help="'ruleEngine'" help-container-id="help-container">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>rulenode.add-link</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <tb-rule-node-link link="vm.link" labels="vm.labels" is-edit="true" the-form="theForm"></tb-rule-node-link>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/rulechain/add-rulechain.tpl.html 48(+48 -0)
diff --git a/ui/src/app/rulechain/add-rulechain.tpl.html b/ui/src/app/rulechain/add-rulechain.tpl.html
new file mode 100644
index 0000000..44d0ec3
--- /dev/null
+++ b/ui/src/app/rulechain/add-rulechain.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'rulechain.add' | translate }}" tb-help="'rulechains'" help-container-id="help-container">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>rulechain.add</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <tb-rule-chain rule-chain="vm.item" is-edit="true" the-form="theForm"></tb-rule-chain>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/rulechain/add-rulenode.tpl.html 48(+48 -0)
diff --git a/ui/src/app/rulechain/add-rulenode.tpl.html b/ui/src/app/rulechain/add-rulenode.tpl.html
new file mode 100644
index 0000000..c572915
--- /dev/null
+++ b/ui/src/app/rulechain/add-rulenode.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'rulenode.add' | translate }}" tb-help="vm.helpLinks.getRuleNodeLink(vm.ruleNode)" help-container-id="help-container" style="min-width: 650px;">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>rulenode.add</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <tb-rule-node rule-node="vm.ruleNode" rule-chain-id="vm.ruleChainId" is-edit="true" the-form="theForm"></tb-rule-node>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/rulechain/index.js 41(+41 -0)
diff --git a/ui/src/app/rulechain/index.js b/ui/src/app/rulechain/index.js
new file mode 100644
index 0000000..7740dd0
--- /dev/null
+++ b/ui/src/app/rulechain/index.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import RuleChainRoutes from './rulechain.routes';
+import RuleChainsController from './rulechains.controller';
+import {RuleChainController, AddRuleNodeController, AddRuleNodeLinkController} from './rulechain.controller';
+import NodeScriptTestController from './script/node-script-test.controller';
+import RuleChainDirective from './rulechain.directive';
+import RuleNodeDefinedConfigDirective from './rulenode-defined-config.directive';
+import RuleNodeConfigDirective from './rulenode-config.directive';
+import RuleNodeDirective from './rulenode.directive';
+import LinkDirective from './link.directive';
+import NodeScriptTest from './script/node-script-test.service';
+
+export default angular.module('thingsboard.ruleChain', [])
+ .config(RuleChainRoutes)
+ .controller('RuleChainsController', RuleChainsController)
+ .controller('RuleChainController', RuleChainController)
+ .controller('AddRuleNodeController', AddRuleNodeController)
+ .controller('AddRuleNodeLinkController', AddRuleNodeLinkController)
+ .controller('NodeScriptTestController', NodeScriptTestController)
+ .directive('tbRuleChain', RuleChainDirective)
+ .directive('tbRuleNodeDefinedConfig', RuleNodeDefinedConfigDirective)
+ .directive('tbRuleNodeConfig', RuleNodeConfigDirective)
+ .directive('tbRuleNode', RuleNodeDirective)
+ .directive('tbRuleNodeLink', LinkDirective)
+ .factory('ruleNodeScriptTest', NodeScriptTest)
+ .name;
ui/src/app/rulechain/link.directive.js 71(+71 -0)
diff --git a/ui/src/app/rulechain/link.directive.js b/ui/src/app/rulechain/link.directive.js
new file mode 100644
index 0000000..b3565a3
--- /dev/null
+++ b/ui/src/app/rulechain/link.directive.js
@@ -0,0 +1,71 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import linkFieldsetTemplate from './link-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function LinkDirective($compile, $templateCache, $filter) {
+ var linker = function (scope, element) {
+ var template = $templateCache.get(linkFieldsetTemplate);
+ element.html(template);
+
+ scope.selectedLabel = null;
+
+ scope.$watch('link', function() {
+ scope.selectedLabel = null;
+ if (scope.link && scope.labels) {
+ if (scope.link.label) {
+ var result = $filter('filter')(scope.labels, {name: scope.link.label});
+ if (result && result.length) {
+ scope.selectedLabel = result[0];
+ } else {
+ result = $filter('filter')(scope.labels, {custom: true});
+ if (result && result.length && result[0].custom) {
+ scope.selectedLabel = result[0];
+ }
+ }
+ }
+ }
+ });
+
+ scope.selectedLabelChanged = function() {
+ if (scope.link && scope.selectedLabel) {
+ if (!scope.selectedLabel.custom) {
+ scope.link.label = scope.selectedLabel.name;
+ } else {
+ scope.link.label = "";
+ }
+ }
+ };
+
+ $compile(element.contents())(scope);
+ }
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ link: '=',
+ labels: '=',
+ isEdit: '=',
+ isReadOnly: '=',
+ theForm: '='
+ }
+ };
+}
ui/src/app/rulechain/link-fieldset.tpl.html 39(+39 -0)
diff --git a/ui/src/app/rulechain/link-fieldset.tpl.html b/ui/src/app/rulechain/link-fieldset.tpl.html
new file mode 100644
index 0000000..13ec6c3
--- /dev/null
+++ b/ui/src/app/rulechain/link-fieldset.tpl.html
@@ -0,0 +1,39 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-content class="md-padding tb-link" layout="column">
+ <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
+ <md-input-container class="md-block">
+ <label translate>rulenode.link-label</label>
+ <md-select ng-model="selectedLabel" ng-change="selectedLabelChanged()">
+ <md-option ng-repeat="label in labels" ng-value="label">
+ {{label.name}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm.linkLabel.$error">
+ <div translate ng-message="required">rulenode.link-label-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="selectedLabel.custom" class="md-block">
+ <label translate>rulenode.link-label</label>
+ <input required name="customLinkLabel" ng-model="link.label">
+ <div ng-messages="theForm.customLinkLabel.$error">
+ <div translate ng-message="required">rulenode.custom-link-label-required</div>
+ </div>
+ </md-input-container>
+ </fieldset>
+</md-content>
ui/src/app/rulechain/rulechain.controller.js 1357(+1357 -0)
diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js
new file mode 100644
index 0000000..06df2cd
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.controller.js
@@ -0,0 +1,1357 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './rulechain.scss';
+
+import 'tooltipster/dist/css/tooltipster.bundle.min.css';
+import 'tooltipster/dist/js/tooltipster.bundle.min.js';
+import 'tooltipster/dist/css/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-shadow.min.css';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import addRuleNodeTemplate from './add-rulenode.tpl.html';
+import addRuleNodeLinkTemplate from './add-link.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog,
+ $filter, $translate, hotkeys, types, ruleChainService, itembuffer, Modelfactory, flowchartConstants,
+ ruleChain, ruleChainMetaData, ruleNodeComponents, helpLinks) {
+
+ var vm = this;
+
+ vm.$mdExpansionPanel = $mdExpansionPanel;
+ vm.types = types;
+
+ if ($state.current.data.import && !ruleChain) {
+ $state.go('home.ruleChains');
+ return;
+ }
+
+ vm.isImport = $state.current.data.import;
+ vm.isConfirmOnExit = false;
+
+ $scope.$watch(function() {
+ return vm.isDirty || vm.isImport;
+ }, (val) => {
+ vm.isConfirmOnExit = val;
+ });
+
+ vm.errorTooltips = {};
+
+ vm.isFullscreen = false;
+
+ vm.editingRuleNode = null;
+ vm.isEditingRuleNode = false;
+
+ vm.editingRuleNodeLink = null;
+ vm.isEditingRuleNodeLink = false;
+
+ vm.isLibraryOpen = true;
+ vm.enableHotKeys = true;
+
+ Object.defineProperty(vm, 'isLibraryOpenReadonly', {
+ get: function() { return vm.isLibraryOpen },
+ set: function() {}
+ });
+
+ vm.ruleNodeSearch = '';
+
+ vm.ruleChain = ruleChain;
+ vm.ruleChainMetaData = ruleChainMetaData;
+
+ vm.canvasControl = {};
+
+ vm.ruleChainModel = {
+ nodes: [],
+ edges: []
+ };
+
+ vm.ruleNodeTypesModel = {};
+ vm.ruleNodeTypesCanvasControl = {};
+ vm.ruleChainLibraryLoaded = false;
+ for (var type in types.ruleNodeType) {
+ if (!types.ruleNodeType[type].special) {
+ vm.ruleNodeTypesModel[type] = {
+ model: {
+ nodes: [],
+ edges: []
+ },
+ selectedObjects: []
+ };
+ vm.ruleNodeTypesCanvasControl[type] = {};
+ }
+ }
+
+
+
+ vm.selectedObjects = [];
+
+ vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects);
+
+ vm.saveRuleChain = saveRuleChain;
+ vm.revertRuleChain = revertRuleChain;
+
+ vm.objectsSelected = objectsSelected;
+ vm.deleteSelected = deleteSelected;
+
+ vm.triggerResize = triggerResize;
+
+ vm.openRuleChainContextMenu = openRuleChainContextMenu;
+
+ vm.helpLinkIdForRuleNodeType = helpLinkIdForRuleNodeType;
+
+ initHotKeys();
+
+ function openRuleChainContextMenu($event, $mdOpenMousepointMenu) {
+ if (vm.canvasControl.modelservice && !$event.ctrlKey && !$event.metaKey) {
+ var x = $event.clientX;
+ var y = $event.clientY;
+ var item = vm.canvasControl.modelservice.getItemInfoAtPoint(x, y);
+ vm.contextInfo = prepareContextMenu(item);
+ if (vm.contextInfo.items && vm.contextInfo.items.length > 0) {
+ vm.contextMenuEvent = $event;
+ $mdOpenMousepointMenu($event);
+ return false;
+ }
+ }
+ }
+
+ function prepareContextMenu(item) {
+ if (objectsSelected() || (!item.node && !item.edge)) {
+ return prepareRuleChainContextMenu();
+ } else if (item.node) {
+ return prepareRuleNodeContextMenu(item.node);
+ } else if (item.edge) {
+ return prepareEdgeContextMenu(item.edge);
+ }
+ }
+
+ function prepareRuleChainContextMenu() {
+ var contextInfo = {
+ headerClass: 'tb-rulechain',
+ icon: 'settings_ethernet',
+ title: vm.ruleChain.name,
+ subtitle: $translate.instant('rulechain.rulechain')
+ };
+ contextInfo.items = [];
+ if (vm.modelservice.nodes.getSelectedNodes().length) {
+ contextInfo.items.push(
+ {
+ action: function () {
+ copyRuleNodes();
+ },
+ enabled: true,
+ value: "rulenode.copy-selected",
+ icon: "content_copy",
+ shortcut: "M-C"
+ }
+ );
+ }
+ contextInfo.items.push(
+ {
+ action: function ($event) {
+ pasteRuleNodes($event);
+ },
+ enabled: itembuffer.hasRuleNodes(),
+ value: "action.paste",
+ icon: "content_paste",
+ shortcut: "M-V"
+ }
+ );
+ contextInfo.items.push(
+ {
+ divider: true
+ }
+ );
+ if (objectsSelected()) {
+ contextInfo.items.push(
+ {
+ action: function () {
+ vm.modelservice.deselectAll();
+ },
+ enabled: true,
+ value: "rulenode.deselect-all",
+ icon: "tab_unselected",
+ shortcut: "Esc"
+ }
+ );
+ contextInfo.items.push(
+ {
+ action: function () {
+ vm.modelservice.deleteSelected();
+ },
+ enabled: true,
+ value: "rulenode.delete-selected",
+ icon: "clear",
+ shortcut: "Del"
+ }
+ );
+ } else {
+ contextInfo.items.push(
+ {
+ action: function () {
+ vm.modelservice.selectAll();
+ },
+ enabled: true,
+ value: "rulenode.select-all",
+ icon: "select_all",
+ shortcut: "M-A"
+ }
+ );
+ }
+ contextInfo.items.push(
+ {
+ divider: true
+ }
+ );
+ contextInfo.items.push(
+ {
+ action: function () {
+ vm.saveRuleChain();
+ },
+ enabled: !(vm.isInvalid || (!vm.isDirty && !vm.isImport)),
+ value: "action.apply-changes",
+ icon: "done",
+ shortcut: "M-S"
+ }
+ );
+ contextInfo.items.push(
+ {
+ action: function () {
+ vm.revertRuleChain();
+ },
+ enabled: vm.isDirty,
+ value: "action.decline-changes",
+ icon: "close",
+ shortcut: "M-Z"
+ }
+ );
+ return contextInfo;
+ }
+
+ function prepareRuleNodeContextMenu(node) {
+ var contextInfo = {
+ headerClass: node.nodeClass,
+ icon: node.icon,
+ iconUrl: node.iconUrl,
+ title: node.name,
+ subtitle: node.component.name
+ };
+ contextInfo.items = [];
+ if (!node.readonly) {
+ contextInfo.items.push(
+ {
+ action: function () {
+ openNodeDetails(node);
+ },
+ enabled: true,
+ value: "rulenode.details",
+ icon: "menu"
+ }
+ );
+ contextInfo.items.push(
+ {
+ action: function () {
+ copyNode(node);
+ },
+ enabled: true,
+ value: "action.copy",
+ icon: "content_copy"
+ }
+ );
+ contextInfo.items.push(
+ {
+ action: function () {
+ vm.canvasControl.modelservice.nodes.delete(node);
+ },
+ enabled: true,
+ value: "action.delete",
+ icon: "clear",
+ shortcut: "M-X"
+ }
+ );
+ }
+ return contextInfo;
+ }
+
+ function prepareEdgeContextMenu(edge) {
+ var contextInfo = {
+ headerClass: 'tb-link',
+ icon: 'trending_flat',
+ title: edge.label,
+ subtitle: $translate.instant('rulenode.link')
+ };
+ contextInfo.items = [];
+ var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+ if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
+ contextInfo.items.push(
+ {
+ action: function () {
+ openLinkDetails(edge);
+ },
+ enabled: true,
+ value: "rulenode.details",
+ icon: "menu"
+ }
+ );
+ }
+ contextInfo.items.push(
+ {
+ action: function () {
+ vm.canvasControl.modelservice.edges.delete(edge);
+ },
+ enabled: true,
+ value: "action.delete",
+ icon: "clear",
+ shortcut: "M-X"
+ }
+ );
+ return contextInfo;
+ }
+
+ function initHotKeys() {
+ hotkeys.bindTo($scope)
+ .add({
+ combo: 'ctrl+a',
+ description: $translate.instant('rulenode.select-all-objects'),
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ if (vm.enableHotKeys) {
+ event.preventDefault();
+ vm.modelservice.selectAll();
+ }
+ }
+ })
+ .add({
+ combo: 'ctrl+c',
+ description: $translate.instant('rulenode.copy-selected'),
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ if (vm.enableHotKeys) {
+ event.preventDefault();
+ copyRuleNodes();
+ }
+ }
+ })
+ .add({
+ combo: 'ctrl+v',
+ description: $translate.instant('action.paste'),
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ if (vm.enableHotKeys) {
+ event.preventDefault();
+ if (itembuffer.hasRuleNodes()) {
+ pasteRuleNodes();
+ }
+ }
+ }
+ })
+ .add({
+ combo: 'esc',
+ description: $translate.instant('rulenode.deselect-all-objects'),
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ if (vm.enableHotKeys) {
+ event.preventDefault();
+ event.stopPropagation();
+ vm.modelservice.deselectAll();
+ }
+ }
+ })
+ .add({
+ combo: 'ctrl+s',
+ description: $translate.instant('action.apply'),
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ if (vm.enableHotKeys) {
+ event.preventDefault();
+ vm.saveRuleChain();
+ }
+ }
+ })
+ .add({
+ combo: 'ctrl+z',
+ description: $translate.instant('action.decline-changes'),
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ if (vm.enableHotKeys) {
+ event.preventDefault();
+ vm.revertRuleChain();
+ }
+ }
+ })
+ .add({
+ combo: 'del',
+ description: $translate.instant('rulenode.delete-selected-objects'),
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ if (vm.enableHotKeys) {
+ event.preventDefault();
+ vm.modelservice.deleteSelected();
+ }
+ }
+ })
+ }
+
+ vm.onEditRuleNodeClosed = function() {
+ vm.editingRuleNode = null;
+ };
+
+ vm.onEditRuleNodeLinkClosed = function() {
+ vm.editingRuleNodeLink = null;
+ };
+
+ vm.saveRuleNode = function(theForm) {
+ $scope.$broadcast('form-submit');
+ if (theForm.$valid) {
+ theForm.$setPristine();
+ if (vm.editingRuleNode.error) {
+ delete vm.editingRuleNode.error;
+ }
+ vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
+ vm.editingRuleNode = angular.copy(vm.editingRuleNode);
+ updateRuleNodesHighlight();
+ }
+ };
+
+ vm.saveRuleNodeLink = function(theForm) {
+ theForm.$setPristine();
+ vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex] = vm.editingRuleNodeLink;
+ vm.editingRuleNodeLink = angular.copy(vm.editingRuleNodeLink);
+ };
+
+ vm.onRevertRuleNodeEdit = function(theForm) {
+ theForm.$setPristine();
+ var node = vm.ruleChainModel.nodes[vm.editingRuleNodeIndex];
+ vm.editingRuleNode = angular.copy(node);
+ };
+
+ vm.onRevertRuleNodeLinkEdit = function(theForm) {
+ theForm.$setPristine();
+ var edge = vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex];
+ vm.editingRuleNodeLink = angular.copy(edge);
+ };
+
+ vm.nodeLibCallbacks = {
+ nodeCallbacks: {
+ 'mouseEnter': function (event, node) {
+ displayLibNodeDescriptionTooltip(event, node);
+ },
+ 'mouseLeave': function () {
+ destroyTooltips();
+ },
+ 'mouseDown': function () {
+ destroyTooltips();
+ }
+ }
+ };
+
+ vm.typeHeaderMouseEnter = function(event, typeId) {
+ var ruleNodeType = types.ruleNodeType[typeId];
+ displayTooltip(event,
+ '<div class="tb-rule-node-tooltip tb-lib-tooltip">' +
+ '<div id="tb-node-content" layout="column">' +
+ '<div class="tb-node-title">' + $translate.instant(ruleNodeType.name) + '</div>' +
+ '<div class="tb-node-details">' + $translate.instant(ruleNodeType.details) + '</div>' +
+ '</div>' +
+ '</div>'
+ );
+ };
+
+ vm.destroyTooltips = destroyTooltips;
+
+ function helpLinkIdForRuleNodeType() {
+ return helpLinks.getRuleNodeLink(vm.editingRuleNode);
+ }
+
+ function destroyTooltips() {
+ if (vm.tooltipTimeout) {
+ $timeout.cancel(vm.tooltipTimeout);
+ vm.tooltipTimeout = null;
+ }
+ var instances = angular.element.tooltipster.instances();
+ instances.forEach((instance) => {
+ if (!instance.isErrorTooltip) {
+ instance.destroy();
+ }
+ });
+ }
+
+ function displayLibNodeDescriptionTooltip(event, node) {
+ displayTooltip(event,
+ '<div class="tb-rule-node-tooltip tb-lib-tooltip">' +
+ '<div id="tb-node-content" layout="column">' +
+ '<div class="tb-node-title">' + node.component.name + '</div>' +
+ '<div class="tb-node-description">' + node.component.configurationDescriptor.nodeDefinition.description + '</div>' +
+ '<div class="tb-node-details">' + node.component.configurationDescriptor.nodeDefinition.details + '</div>' +
+ '</div>' +
+ '</div>'
+ );
+ }
+
+ function displayNodeDescriptionTooltip(event, node) {
+ if (!vm.errorTooltips[node.id]) {
+ var name, desc, details;
+ if (node.component.type == vm.types.ruleNodeType.INPUT.value) {
+ name = $translate.instant(vm.types.ruleNodeType.INPUT.name) + '';
+ desc = $translate.instant(vm.types.ruleNodeType.INPUT.details) + '';
+ } else {
+ name = node.name;
+ desc = $translate.instant(vm.types.ruleNodeType[node.component.type].name) + ' - ' + node.component.name;
+ if (node.additionalInfo) {
+ details = node.additionalInfo.description;
+ }
+ }
+ var tooltipContent = '<div class="tb-rule-node-tooltip">' +
+ '<div id="tb-node-content" layout="column">' +
+ '<div class="tb-node-title">' + name + '</div>' +
+ '<div class="tb-node-description">' + desc + '</div>';
+ if (details) {
+ tooltipContent += '<div class="tb-node-details">' + details + '</div>';
+ }
+ tooltipContent += '</div>' +
+ '</div>';
+ displayTooltip(event, tooltipContent);
+ }
+ }
+
+ function displayTooltip(event, content) {
+ destroyTooltips();
+ vm.tooltipTimeout = $timeout(() => {
+ var element = angular.element(event.target);
+ element.tooltipster(
+ {
+ theme: 'tooltipster-shadow',
+ delay: 100,
+ trigger: 'custom',
+ triggerOpen: {
+ click: false,
+ tap: false
+ },
+ triggerClose: {
+ click: true,
+ tap: true,
+ scroll: true
+ },
+ side: 'right',
+ trackOrigin: true
+ }
+ );
+ var contentElement = angular.element(content);
+ $compile(contentElement)($scope);
+ var tooltip = element.tooltipster('instance');
+ tooltip.content(contentElement);
+ tooltip.open();
+ }, 500);
+ }
+
+ function updateNodeErrorTooltip(node) {
+ if (node.error) {
+ var element = angular.element('#' + node.id);
+ var tooltip = vm.errorTooltips[node.id];
+ if (!tooltip || !element.hasClass("tooltipstered")) {
+ element.tooltipster(
+ {
+ theme: 'tooltipster-shadow',
+ delay: 0,
+ animationDuration: 0,
+ trigger: 'custom',
+ triggerOpen: {
+ click: false,
+ tap: false
+ },
+ triggerClose: {
+ click: false,
+ tap: false,
+ scroll: false
+ },
+ side: 'top',
+ trackOrigin: true
+ }
+ );
+ var content = '<div class="tb-rule-node-error-tooltip">' +
+ '<div id="tooltip-content" layout="column">' +
+ '<div class="tb-node-details">' + node.error + '</div>' +
+ '</div>' +
+ '</div>';
+ var contentElement = angular.element(content);
+ $compile(contentElement)($scope);
+ tooltip = element.tooltipster('instance');
+ tooltip.isErrorTooltip = true;
+ tooltip.content(contentElement);
+ vm.errorTooltips[node.id] = tooltip;
+ }
+ $mdUtil.nextTick(() => {
+ tooltip.open();
+ });
+ } else {
+ if (vm.errorTooltips[node.id]) {
+ tooltip = vm.errorTooltips[node.id];
+ tooltip.destroy();
+ delete vm.errorTooltips[node.id];
+ }
+ }
+ }
+
+ function updateErrorTooltips(hide) {
+ for (var nodeId in vm.errorTooltips) {
+ var tooltip = vm.errorTooltips[nodeId];
+ if (hide) {
+ tooltip.close();
+ } else {
+ tooltip.open();
+ }
+ }
+ }
+
+ $scope.$watch(function() {
+ return vm.isEditingRuleNode || vm.isEditingRuleNodeLink;
+ }, (val) => {
+ vm.enableHotKeys = !val;
+ updateErrorTooltips(val);
+ });
+
+ vm.editCallbacks = {
+ edgeDoubleClick: function (event, edge) {
+ openLinkDetails(edge);
+ },
+ edgeEdit: function(event, edge) {
+ openLinkDetails(edge);
+ },
+ nodeCallbacks: {
+ 'doubleClick': function (event, node) {
+ openNodeDetails(node);
+ },
+ 'nodeEdit': function (event, node) {
+ openNodeDetails(node);
+ },
+ 'mouseEnter': function (event, node) {
+ displayNodeDescriptionTooltip(event, node);
+ },
+ 'mouseLeave': function () {
+ destroyTooltips();
+ },
+ 'mouseDown': function () {
+ destroyTooltips();
+ }
+ },
+ isValidEdge: function (source, destination) {
+ return source.type === flowchartConstants.rightConnectorType && destination.type === flowchartConstants.leftConnectorType;
+ },
+ createEdge: function (event, edge) {
+ var deferred = $q.defer();
+ var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+ if (sourceNode.component.type == types.ruleNodeType.INPUT.value) {
+ var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
+ if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
+ deferred.reject();
+ } else {
+ var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId}, true);
+ if (res && res.length) {
+ vm.modelservice.edges.delete(res[0]);
+ }
+ deferred.resolve(edge);
+ }
+ } else {
+ if (edge.label) {
+ deferred.resolve(edge);
+ } else {
+ var labels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
+ vm.enableHotKeys = false;
+ addRuleNodeLink(event, edge, labels).then(
+ (link) => {
+ deferred.resolve(link);
+ vm.enableHotKeys = true;
+ },
+ () => {
+ deferred.reject();
+ vm.enableHotKeys = true;
+ }
+ );
+ }
+ }
+ return deferred.promise;
+ },
+ dropNode: function (event, node) {
+ addRuleNode(event, node);
+ }
+ };
+
+ function openNodeDetails(node) {
+ if (node.component.type != types.ruleNodeType.INPUT.value) {
+ vm.isEditingRuleNodeLink = false;
+ vm.editingRuleNodeLink = null;
+ vm.isEditingRuleNode = true;
+ vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node);
+ vm.editingRuleNode = angular.copy(node);
+ $mdUtil.nextTick(() => {
+ if (vm.ruleNodeForm) {
+ vm.ruleNodeForm.$setPristine();
+ }
+ });
+ }
+ }
+
+ function openLinkDetails(edge) {
+ var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+ if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
+ vm.isEditingRuleNode = false;
+ vm.editingRuleNode = null;
+ vm.editingRuleNodeLinkLabels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
+ vm.isEditingRuleNodeLink = true;
+ vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
+ vm.editingRuleNodeLink = angular.copy(edge);
+ $mdUtil.nextTick(() => {
+ if (vm.ruleNodeLinkForm) {
+ vm.ruleNodeLinkForm.$setPristine();
+ }
+ });
+ }
+ }
+
+ function copyNode(node) {
+ itembuffer.copyRuleNodes([node], []);
+ }
+
+ function copyRuleNodes() {
+ var nodes = vm.modelservice.nodes.getSelectedNodes();
+ var edges = vm.modelservice.edges.getSelectedEdges();
+ var connections = [];
+ for (var i=0;i<edges.length;i++) {
+ var edge = edges[i];
+ var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+ var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
+ var isInputSource = sourceNode.component.type == types.ruleNodeType.INPUT.value;
+ var fromIndex = nodes.indexOf(sourceNode);
+ var toIndex = nodes.indexOf(destNode);
+ if ( (isInputSource || fromIndex > -1) && toIndex > -1 ) {
+ var connection = {
+ isInputSource: isInputSource,
+ fromIndex: fromIndex,
+ toIndex: toIndex,
+ label: edge.label
+ };
+ connections.push(connection);
+ }
+ }
+ itembuffer.copyRuleNodes(nodes, connections);
+ }
+
+ function pasteRuleNodes(event) {
+ var canvas = angular.element(vm.canvasControl.modelservice.getCanvasHtmlElement());
+ var x,y;
+ if (event) {
+ var offset = canvas.offset();
+ x = Math.round(event.clientX - offset.left);
+ y = Math.round(event.clientY - offset.top);
+ } else {
+ var scrollParent = canvas.parent();
+ var scrollTop = scrollParent.scrollTop();
+ var scrollLeft = scrollParent.scrollLeft();
+ x = scrollLeft + scrollParent.width()/2;
+ y = scrollTop + scrollParent.height()/2;
+ }
+ var ruleNodes = itembuffer.pasteRuleNodes(x, y, event);
+ if (ruleNodes) {
+ vm.modelservice.deselectAll();
+ var nodes = [];
+ for (var i=0;i<ruleNodes.nodes.length;i++) {
+ var node = ruleNodes.nodes[i];
+ node.id = 'rule-chain-node-' + vm.nextNodeID++;
+ var component = node.component;
+ if (component.configurationDescriptor.nodeDefinition.inEnabled) {
+ node.connectors.push(
+ {
+ type: flowchartConstants.leftConnectorType,
+ id: vm.nextConnectorID++
+ }
+ );
+ }
+ if (component.configurationDescriptor.nodeDefinition.outEnabled) {
+ node.connectors.push(
+ {
+ type: flowchartConstants.rightConnectorType,
+ id: vm.nextConnectorID++
+ }
+ );
+ }
+ nodes.push(node);
+ vm.ruleChainModel.nodes.push(node);
+ vm.modelservice.nodes.select(node);
+ }
+ for (i=0;i<ruleNodes.connections.length;i++) {
+ var connection = ruleNodes.connections[i];
+ var sourceNode = nodes[connection.fromIndex];
+ var destNode = nodes[connection.toIndex];
+ if ( (connection.isInputSource || sourceNode) && destNode ) {
+ var source, destination;
+ if (connection.isInputSource) {
+ source = vm.inputConnectorId;
+ } else {
+ var sourceConnectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
+ if (sourceConnectors && sourceConnectors.length) {
+ source = sourceConnectors[0].id;
+ }
+ }
+ var destConnectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
+ if (destConnectors && destConnectors.length) {
+ destination = destConnectors[0].id;
+ }
+ if (source && destination) {
+ var edge = {
+ source: source,
+ destination: destination,
+ label: connection.label
+ };
+ vm.ruleChainModel.edges.push(edge);
+ vm.modelservice.edges.select(edge);
+ }
+ }
+ }
+
+ if (vm.canvasControl.adjustCanvasSize) {
+ vm.canvasControl.adjustCanvasSize();
+ }
+
+ updateRuleNodesHighlight();
+
+ validate();
+ }
+ }
+
+ loadRuleChainLibrary(ruleNodeComponents, true);
+
+ $scope.$watch('vm.ruleNodeSearch',
+ function (newVal, oldVal) {
+ if (!angular.equals(newVal, oldVal)) {
+ var res = $filter('filter')(ruleNodeComponents, {name: vm.ruleNodeSearch});
+ loadRuleChainLibrary(res);
+ }
+ }
+ );
+
+ $scope.$on('searchTextUpdated', function () {
+ updateRuleNodesHighlight();
+ });
+
+ function loadRuleChainLibrary(ruleNodeComponents, loadRuleChain) {
+ for (var componentType in vm.ruleNodeTypesModel) {
+ vm.ruleNodeTypesModel[componentType].model.nodes.length = 0;
+ }
+ for (var i=0;i<ruleNodeComponents.length;i++) {
+ var ruleNodeComponent = ruleNodeComponents[i];
+ componentType = ruleNodeComponent.type;
+ var model = vm.ruleNodeTypesModel[componentType].model;
+ var icon = vm.types.ruleNodeType[componentType].icon;
+ var iconUrl = null;
+ if (ruleNodeComponent.configurationDescriptor.nodeDefinition.icon) {
+ icon = ruleNodeComponent.configurationDescriptor.nodeDefinition.icon;
+ }
+ if (ruleNodeComponent.configurationDescriptor.nodeDefinition.iconUrl) {
+ iconUrl = ruleNodeComponent.configurationDescriptor.nodeDefinition.iconUrl;
+ }
+ var node = {
+ id: 'node-lib-' + componentType + '-' + model.nodes.length,
+ component: ruleNodeComponent,
+ name: '',
+ nodeClass: vm.types.ruleNodeType[componentType].nodeClass,
+ icon: icon,
+ iconUrl: iconUrl,
+ x: 30,
+ y: 10+50*model.nodes.length,
+ connectors: []
+ };
+ if (ruleNodeComponent.configurationDescriptor.nodeDefinition.inEnabled) {
+ node.connectors.push(
+ {
+ type: flowchartConstants.leftConnectorType,
+ id: model.nodes.length * 2
+ }
+ );
+ }
+ if (ruleNodeComponent.configurationDescriptor.nodeDefinition.outEnabled) {
+ node.connectors.push(
+ {
+ type: flowchartConstants.rightConnectorType,
+ id: model.nodes.length * 2 + 1
+ }
+ );
+ }
+ model.nodes.push(node);
+ }
+ vm.ruleChainLibraryLoaded = true;
+ if (loadRuleChain) {
+ prepareRuleChain();
+ }
+ $mdUtil.nextTick(() => {
+ for (componentType in vm.ruleNodeTypesCanvasControl) {
+ if (vm.ruleNodeTypesCanvasControl[componentType].adjustCanvasSize) {
+ vm.ruleNodeTypesCanvasControl[componentType].adjustCanvasSize(true);
+ }
+ }
+ for (componentType in vm.ruleNodeTypesModel) {
+ var panel = vm.$mdExpansionPanel(componentType);
+ if (panel) {
+ if (!vm.ruleNodeTypesModel[componentType].model.nodes.length) {
+ panel.collapse();
+ } else {
+ panel.expand();
+ }
+ }
+ }
+ });
+ }
+
+ function prepareRuleChain() {
+
+ if (vm.ruleChainWatch) {
+ vm.ruleChainWatch();
+ vm.ruleChainWatch = null;
+ }
+
+ vm.nextNodeID = 1;
+ vm.nextConnectorID = 1;
+
+ vm.selectedObjects.length = 0;
+ vm.ruleChainModel.nodes.length = 0;
+ vm.ruleChainModel.edges.length = 0;
+
+ vm.inputConnectorId = vm.nextConnectorID++;
+
+ vm.ruleChainModel.nodes.push(
+ {
+ id: 'rule-chain-node-' + vm.nextNodeID++,
+ component: types.inputNodeComponent,
+ name: "",
+ nodeClass: types.ruleNodeType.INPUT.nodeClass,
+ icon: types.ruleNodeType.INPUT.icon,
+ readonly: true,
+ x: 50,
+ y: 150,
+ connectors: [
+ {
+ type: flowchartConstants.rightConnectorType,
+ id: vm.inputConnectorId
+ },
+ ]
+
+ }
+ );
+ ruleChainService.resolveTargetRuleChains(vm.ruleChainMetaData.ruleChainConnections)
+ .then((ruleChainsMap) => {
+ createRuleChainModel(ruleChainsMap);
+ }
+ );
+ }
+
+ function createRuleChainModel(ruleChainsMap) {
+ var nodes = [];
+ for (var i=0;i<vm.ruleChainMetaData.nodes.length;i++) {
+ var ruleNode = vm.ruleChainMetaData.nodes[i];
+ var component = ruleChainService.getRuleNodeComponentByClazz(ruleNode.type);
+ if (component) {
+ var icon = vm.types.ruleNodeType[component.type].icon;
+ var iconUrl = null;
+ if (component.configurationDescriptor.nodeDefinition.icon) {
+ icon = component.configurationDescriptor.nodeDefinition.icon;
+ }
+ if (component.configurationDescriptor.nodeDefinition.iconUrl) {
+ iconUrl = component.configurationDescriptor.nodeDefinition.iconUrl;
+ }
+ var node = {
+ id: 'rule-chain-node-' + vm.nextNodeID++,
+ ruleNodeId: ruleNode.id,
+ additionalInfo: ruleNode.additionalInfo,
+ configuration: ruleNode.configuration,
+ debugMode: ruleNode.debugMode,
+ x: ruleNode.additionalInfo.layoutX,
+ y: ruleNode.additionalInfo.layoutY,
+ component: component,
+ name: ruleNode.name,
+ nodeClass: vm.types.ruleNodeType[component.type].nodeClass,
+ icon: icon,
+ iconUrl: iconUrl,
+ connectors: []
+ };
+ if (component.configurationDescriptor.nodeDefinition.inEnabled) {
+ node.connectors.push(
+ {
+ type: flowchartConstants.leftConnectorType,
+ id: vm.nextConnectorID++
+ }
+ );
+ }
+ if (component.configurationDescriptor.nodeDefinition.outEnabled) {
+ node.connectors.push(
+ {
+ type: flowchartConstants.rightConnectorType,
+ id: vm.nextConnectorID++
+ }
+ );
+ }
+ nodes.push(node);
+ vm.ruleChainModel.nodes.push(node);
+ }
+ }
+
+ if (vm.ruleChainMetaData.firstNodeIndex > -1) {
+ var destNode = nodes[vm.ruleChainMetaData.firstNodeIndex];
+ if (destNode) {
+ var connectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
+ if (connectors && connectors.length) {
+ var edge = {
+ source: vm.inputConnectorId,
+ destination: connectors[0].id
+ };
+ vm.ruleChainModel.edges.push(edge);
+ }
+ }
+ }
+
+ if (vm.ruleChainMetaData.connections) {
+ for (i = 0; i < vm.ruleChainMetaData.connections.length; i++) {
+ var connection = vm.ruleChainMetaData.connections[i];
+ var sourceNode = nodes[connection.fromIndex];
+ destNode = nodes[connection.toIndex];
+ if (sourceNode && destNode) {
+ var sourceConnectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
+ var destConnectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
+ if (sourceConnectors && sourceConnectors.length && destConnectors && destConnectors.length) {
+ edge = {
+ source: sourceConnectors[0].id,
+ destination: destConnectors[0].id,
+ label: connection.type
+ };
+ vm.ruleChainModel.edges.push(edge);
+ }
+ }
+ }
+ }
+
+ if (vm.ruleChainMetaData.ruleChainConnections) {
+ var ruleChainNodesMap = {};
+ for (i = 0; i < vm.ruleChainMetaData.ruleChainConnections.length; i++) {
+ var ruleChainConnection = vm.ruleChainMetaData.ruleChainConnections[i];
+ var ruleChain = ruleChainsMap[ruleChainConnection.targetRuleChainId.id];
+ if (ruleChainConnection.additionalInfo && ruleChainConnection.additionalInfo.ruleChainNodeId) {
+ var ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId];
+ if (!ruleChainNode) {
+ ruleChainNode = {
+ id: 'rule-chain-node-' + vm.nextNodeID++,
+ additionalInfo: ruleChainConnection.additionalInfo,
+ x: ruleChainConnection.additionalInfo.layoutX,
+ y: ruleChainConnection.additionalInfo.layoutY,
+ component: types.ruleChainNodeComponent,
+ nodeClass: vm.types.ruleNodeType.RULE_CHAIN.nodeClass,
+ icon: vm.types.ruleNodeType.RULE_CHAIN.icon,
+ connectors: [
+ {
+ type: flowchartConstants.leftConnectorType,
+ id: vm.nextConnectorID++
+ }
+ ]
+ };
+ if (ruleChain.name) {
+ ruleChainNode.name = ruleChain.name;
+ ruleChainNode.targetRuleChainId = ruleChainConnection.targetRuleChainId.id;
+ } else {
+ ruleChainNode.name = "Unresolved";
+ ruleChainNode.targetRuleChainId = null;
+ ruleChainNode.error = $translate.instant('rulenode.invalid-target-rulechain');
+ }
+ ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode;
+ vm.ruleChainModel.nodes.push(ruleChainNode);
+ }
+ sourceNode = nodes[ruleChainConnection.fromIndex];
+ if (sourceNode) {
+ connectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
+ if (connectors && connectors.length) {
+ var ruleChainEdge = {
+ source: connectors[0].id,
+ destination: ruleChainNode.connectors[0].id,
+ label: ruleChainConnection.type
+ };
+ vm.ruleChainModel.edges.push(ruleChainEdge);
+ }
+ }
+ }
+ }
+ }
+
+ if (vm.canvasControl.adjustCanvasSize) {
+ vm.canvasControl.adjustCanvasSize(true);
+ }
+
+ vm.isDirty = false;
+
+ updateRuleNodesHighlight();
+
+ validate();
+
+ $mdUtil.nextTick(() => {
+ vm.ruleChainWatch = $scope.$watch('vm.ruleChainModel',
+ function (newVal, oldVal) {
+ if (!angular.equals(newVal, oldVal)) {
+ validate();
+ if (!vm.isDirty) {
+ vm.isDirty = true;
+ }
+ }
+ }, true
+ );
+ });
+ }
+
+ function updateRuleNodesHighlight() {
+ for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) {
+ vm.ruleChainModel.nodes[i].highlighted = false;
+ }
+ if ($scope.searchConfig.searchText) {
+ var res = $filter('filter')(vm.ruleChainModel.nodes, {name: $scope.searchConfig.searchText});
+ if (res) {
+ for (i = 0; i < res.length; i++) {
+ res[i].highlighted = true;
+ }
+ }
+ }
+ }
+
+ function validate() {
+ $mdUtil.nextTick(() => {
+ vm.isInvalid = false;
+ for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) {
+ if (vm.ruleChainModel.nodes[i].error) {
+ vm.isInvalid = true;
+ }
+ updateNodeErrorTooltip(vm.ruleChainModel.nodes[i]);
+ }
+ });
+ }
+
+ function saveRuleChain() {
+ var saveRuleChainPromise;
+ if (vm.isImport) {
+ saveRuleChainPromise = ruleChainService.saveRuleChain(vm.ruleChain);
+ } else {
+ saveRuleChainPromise = $q.when(vm.ruleChain);
+ }
+ saveRuleChainPromise.then(
+ (ruleChain) => {
+ vm.ruleChain = ruleChain;
+ var ruleChainMetaData = {
+ ruleChainId: vm.ruleChain.id,
+ nodes: [],
+ connections: [],
+ ruleChainConnections: []
+ };
+
+ var nodes = [];
+
+ for (var i=0;i<vm.ruleChainModel.nodes.length;i++) {
+ var node = vm.ruleChainModel.nodes[i];
+ if (node.component.type != types.ruleNodeType.INPUT.value && node.component.type != types.ruleNodeType.RULE_CHAIN.value) {
+ var ruleNode = {};
+ if (node.ruleNodeId) {
+ ruleNode.id = node.ruleNodeId;
+ }
+ ruleNode.type = node.component.clazz;
+ ruleNode.name = node.name;
+ ruleNode.configuration = node.configuration;
+ ruleNode.additionalInfo = node.additionalInfo;
+ ruleNode.debugMode = node.debugMode;
+ if (!ruleNode.additionalInfo) {
+ ruleNode.additionalInfo = {};
+ }
+ ruleNode.additionalInfo.layoutX = node.x;
+ ruleNode.additionalInfo.layoutY = node.y;
+ ruleChainMetaData.nodes.push(ruleNode);
+ nodes.push(node);
+ }
+ }
+ var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId}, true);
+ if (res && res.length) {
+ var firstNodeEdge = res[0];
+ var firstNode = vm.modelservice.nodes.getNodeByConnectorId(firstNodeEdge.destination);
+ ruleChainMetaData.firstNodeIndex = nodes.indexOf(firstNode);
+ }
+ for (i=0;i<vm.ruleChainModel.edges.length;i++) {
+ var edge = vm.ruleChainModel.edges[i];
+ var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+ var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
+ if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
+ var fromIndex = nodes.indexOf(sourceNode);
+ if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
+ var ruleChainConnection = {
+ fromIndex: fromIndex,
+ targetRuleChainId: {entityType: vm.types.entityType.rulechain, id: destNode.targetRuleChainId},
+ additionalInfo: destNode.additionalInfo,
+ type: edge.label
+ };
+ if (!ruleChainConnection.additionalInfo) {
+ ruleChainConnection.additionalInfo = {};
+ }
+ ruleChainConnection.additionalInfo.layoutX = destNode.x;
+ ruleChainConnection.additionalInfo.layoutY = destNode.y;
+ ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id;
+ ruleChainMetaData.ruleChainConnections.push(ruleChainConnection);
+ } else {
+ var toIndex = nodes.indexOf(destNode);
+ var nodeConnection = {
+ fromIndex: fromIndex,
+ toIndex: toIndex,
+ type: edge.label
+ };
+ ruleChainMetaData.connections.push(nodeConnection);
+ }
+ }
+ }
+ ruleChainService.saveRuleChainMetaData(ruleChainMetaData).then(
+ (ruleChainMetaData) => {
+ vm.ruleChainMetaData = ruleChainMetaData;
+ if (vm.isImport) {
+ vm.isDirty = false;
+ vm.isImport = false;
+ $mdUtil.nextTick(() => {
+ $state.go('home.ruleChains.ruleChain', {ruleChainId: vm.ruleChain.id.id});
+ });
+ } else {
+ prepareRuleChain();
+ }
+ }
+ );
+ }
+ );
+ }
+
+ function revertRuleChain() {
+ prepareRuleChain();
+ }
+
+ function addRuleNode($event, ruleNode) {
+
+ ruleNode.configuration = angular.copy(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration);
+
+ var ruleChainId = vm.ruleChain.id ? vm.ruleChain.id.id : null;
+
+ vm.enableHotKeys = false;
+
+ $mdDialog.show({
+ controller: 'AddRuleNodeController',
+ controllerAs: 'vm',
+ templateUrl: addRuleNodeTemplate,
+ parent: angular.element($document[0].body),
+ locals: {ruleNode: ruleNode, ruleChainId: ruleChainId},
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function (ruleNode) {
+ ruleNode.id = 'rule-chain-node-' + vm.nextNodeID++;
+ ruleNode.connectors = [];
+ if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) {
+ ruleNode.connectors.push(
+ {
+ id: vm.nextConnectorID++,
+ type: flowchartConstants.leftConnectorType
+ }
+ );
+ }
+ if (ruleNode.component.configurationDescriptor.nodeDefinition.outEnabled) {
+ ruleNode.connectors.push(
+ {
+ id: vm.nextConnectorID++,
+ type: flowchartConstants.rightConnectorType
+ }
+ );
+ }
+ vm.ruleChainModel.nodes.push(ruleNode);
+ updateRuleNodesHighlight();
+ vm.enableHotKeys = true;
+ }, function () {
+ vm.enableHotKeys = true;
+ });
+ }
+
+ function addRuleNodeLink($event, link, labels) {
+ return $mdDialog.show({
+ controller: 'AddRuleNodeLinkController',
+ controllerAs: 'vm',
+ templateUrl: addRuleNodeLinkTemplate,
+ parent: angular.element($document[0].body),
+ locals: {link: link, labels: labels},
+ fullscreen: true,
+ targetEvent: $event
+ });
+ }
+
+ function objectsSelected() {
+ return vm.modelservice.nodes.getSelectedNodes().length > 0 ||
+ vm.modelservice.edges.getSelectedEdges().length > 0
+ }
+
+ function deleteSelected() {
+ vm.modelservice.deleteSelected();
+ }
+
+ function triggerResize() {
+ var w = angular.element($window);
+ w.triggerHandler('resize');
+ }
+}
+
+/*@ngInject*/
+export function AddRuleNodeController($scope, $mdDialog, ruleNode, ruleChainId, helpLinks) {
+
+ var vm = this;
+
+ vm.helpLinks = helpLinks;
+ vm.ruleNode = ruleNode;
+ vm.ruleChainId = ruleChainId;
+
+ vm.add = add;
+ vm.cancel = cancel;
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function add() {
+ $scope.theForm.$setPristine();
+ $mdDialog.hide(vm.ruleNode);
+ }
+}
+
+/*@ngInject*/
+export function AddRuleNodeLinkController($scope, $mdDialog, link, labels, helpLinks) {
+
+ var vm = this;
+
+ vm.helpLinks = helpLinks;
+ vm.link = link;
+ vm.labels = labels;
+
+ vm.add = add;
+ vm.cancel = cancel;
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function add() {
+ $scope.theForm.$setPristine();
+ $mdDialog.hide(vm.link);
+ }
+}
ui/src/app/rulechain/rulechain.directive.js 48(+48 -0)
diff --git a/ui/src/app/rulechain/rulechain.directive.js b/ui/src/app/rulechain/rulechain.directive.js
new file mode 100644
index 0000000..8d19229
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.directive.js
@@ -0,0 +1,48 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleChainFieldsetTemplate from './rulechain-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleChainDirective($compile, $templateCache, $mdDialog, $document, $q, $translate, types, toast) {
+ var linker = function (scope, element) {
+ var template = $templateCache.get(ruleChainFieldsetTemplate);
+ element.html(template);
+
+ scope.onRuleChainIdCopied = function() {
+ toast.showSuccess($translate.instant('rulechain.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+ };
+
+ $compile(element.contents())(scope);
+ }
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ ruleChain: '=',
+ isEdit: '=',
+ isReadOnly: '=',
+ theForm: '=',
+ onSetRootRuleChain: '&',
+ onExportRuleChain: '&',
+ onDeleteRuleChain: '&'
+ }
+ };
+}
ui/src/app/rulechain/rulechain.routes.js 127(+127 -0)
diff --git a/ui/src/app/rulechain/rulechain.routes.js b/ui/src/app/rulechain/rulechain.routes.js
new file mode 100644
index 0000000..f649f53
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.routes.js
@@ -0,0 +1,127 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleNodeTemplate from './rulenode.tpl.html';
+import ruleChainsTemplate from './rulechains.tpl.html';
+import ruleChainTemplate from './rulechain.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider) {
+
+ NodeTemplatePathProvider.setTemplatePath(ruleNodeTemplate);
+
+ $stateProvider
+ .state('home.ruleChains', {
+ url: '/ruleChains',
+ params: {'topIndex': 0},
+ module: 'private',
+ auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: ruleChainsTemplate,
+ controllerAs: 'vm',
+ controller: 'RuleChainsController'
+ }
+ },
+ data: {
+ searchEnabled: true,
+ pageTitle: 'rulechain.rulechains'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "settings_ethernet", "label": "rulechain.rulechains"}'
+ }
+ }).state('home.ruleChains.ruleChain', {
+ url: '/:ruleChainId',
+ reloadOnSearch: false,
+ module: 'private',
+ auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: ruleChainTemplate,
+ controller: 'RuleChainController',
+ controllerAs: 'vm'
+ }
+ },
+ resolve: {
+ ruleChain:
+ /*@ngInject*/
+ function($stateParams, ruleChainService) {
+ return ruleChainService.getRuleChain($stateParams.ruleChainId);
+ },
+ ruleChainMetaData:
+ /*@ngInject*/
+ function($stateParams, ruleChainService) {
+ return ruleChainService.getRuleChainMetaData($stateParams.ruleChainId);
+ },
+ ruleNodeComponents:
+ /*@ngInject*/
+ function($stateParams, ruleChainService) {
+ return ruleChainService.getRuleNodeComponents();
+ }
+ },
+ data: {
+ import: false,
+ searchEnabled: true,
+ pageTitle: 'rulechain.rulechain'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "settings_ethernet", "label": "{{ vm.ruleChain.name + (vm.ruleChain.root ? (\' (\' + (\'rulechain.root\' | translate) + \')\') : \'\') }}", "translate": "false"}'
+ }
+ }).state('home.ruleChains.importRuleChain', {
+ url: '/ruleChain/import',
+ reloadOnSearch: false,
+ module: 'private',
+ auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: ruleChainTemplate,
+ controller: 'RuleChainController',
+ controllerAs: 'vm'
+ }
+ },
+ params: {
+ ruleChainImport: {}
+ },
+ resolve: {
+ ruleChain:
+ /*@ngInject*/
+ function($stateParams) {
+ return $stateParams.ruleChainImport.ruleChain;
+ },
+ ruleChainMetaData:
+ /*@ngInject*/
+ function($stateParams) {
+ return $stateParams.ruleChainImport.metadata;
+ },
+ ruleNodeComponents:
+ /*@ngInject*/
+ function($stateParams, ruleChainService) {
+ return ruleChainService.getRuleNodeComponents();
+ }
+ },
+ data: {
+ import: true,
+ searchEnabled: true,
+ pageTitle: 'rulechain.rulechain'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "settings_ethernet", "label": "{{ (\'rulechain.import\' | translate) + \': \'+ vm.ruleChain.name }}", "translate": "false"}'
+ }
+ });
+}
ui/src/app/rulechain/rulechain.scss 512(+512 -0)
diff --git a/ui/src/app/rulechain/rulechain.scss b/ui/src/app/rulechain/rulechain.scss
new file mode 100644
index 0000000..c51a955
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.scss
@@ -0,0 +1,512 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.tb-rulechain {
+ .tb-fullscreen-button-style {
+ z-index: 1;
+ }
+ section.tb-header-buttons.tb-library-open {
+ pointer-events: none;
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ z-index: 1;
+ .md-button.tb-btn-open-library {
+ left: 0px;
+ top: 0px;
+ line-height: 36px;
+ width: 36px;
+ height: 36px;
+ margin: 4px 0 0 4px;
+ opacity: 0.5;
+ }
+ }
+ .tb-rulechain-library {
+ width: 250px;
+ min-width: 250px;
+ z-index: 1;
+ md-toolbar {
+ min-height: 48px;
+ height: 48px;
+ .md-toolbar-tools>.md-button:last-child {
+ margin-right: 0px;
+ }
+ .md-toolbar-tools {
+ font-size: 14px;
+ padding: 0px 6px;
+ height: 48px;
+ .md-button.md-icon-button {
+ margin: 0px;
+ &.tb-small {
+ height: 32px;
+ min-height: 32px;
+ line-height: 20px;
+ padding: 6px;
+ width: 32px;
+ md-icon {
+ line-height: 20px;
+ font-size: 20px;
+ height: 20px;
+ width: 20px;
+ min-height: 20px;
+ min-width: 20px;
+ }
+ }
+ }
+ }
+ }
+ .tb-rulechain-library-panel-group {
+ overflow-y: auto;
+ overflow-x: hidden;
+ .tb-panel-title {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ min-width: 150px;
+ }
+ .fc-canvas {
+ background: #f9f9f9;
+ }
+ md-icon.md-expansion-panel-icon {
+ margin-right: 0px;
+ }
+ md-expansion-panel-collapsed, .md-expansion-panel-header-container {
+ background: #e6e6e6;
+ border-color: #909090;
+ position: static;
+ }
+ md-expansion-panel {
+ &.md-open {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+ }
+ md-expansion-panel-content {
+ padding: 0px;
+ }
+ }
+ }
+ .tb-rulechain-graph {
+ z-index: 0;
+ overflow: auto;
+ }
+}
+
+#tb-rule-chain-context-menu {
+ padding-top: 0px;
+ border-radius: 8px;
+ max-height: 404px;
+ .tb-context-menu-header {
+ padding: 8px 5px 5px;
+ font-size: 14px;
+ display: flex;
+ flex-direction: row;
+ height: 36px;
+ min-height: 36px;
+ &.tb-rulechain {
+ background-color: #aac7e4;
+ }
+ &.tb-link {
+ background-color: #aac7e4;
+ }
+ md-icon {
+ padding-left: 2px;
+ padding-right: 10px;
+ }
+ .tb-context-menu-title {
+ font-weight: 500;
+ }
+ .tb-context-menu-subtitle {
+ font-size: 12px;
+ }
+ }
+}
+
+.fc-canvas {
+ min-width: 100%;
+ min-height: 100%;
+ outline: none;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ svg {
+ display: block;
+ }
+}
+
+.tb-rule-node, #tb-rule-chain-context-menu .tb-context-menu-header {
+ &.tb-filter-type {
+ background-color: #f1e861;
+ }
+ &.tb-enrichment-type {
+ background-color: #cdf14e;
+ }
+ &.tb-transformation-type {
+ background-color: #79cef1;
+ }
+ &.tb-action-type {
+ background-color: #f1928f;
+ }
+ &.tb-external-type {
+ background-color: #fbc766;
+ }
+ &.tb-rule-chain-type {
+ background-color: #d6c4f1;
+ }
+}
+
+.tb-rule-node {
+ display: flex;
+ flex-direction: row;
+ min-width: 150px;
+ max-width: 150px;
+ min-height: 32px;
+ max-height: 32px;
+ height: 32px;
+ padding: 5px 10px;
+ border-radius: 5px;
+ background-color: #F15B26;
+ pointer-events: none;
+ color: #333;
+ border: solid 1px #777;
+ font-size: 12px;
+ line-height: 16px;
+ &.tb-rule-node-highlighted:not(.tb-rule-node-invalid) {
+ box-shadow: 0 0 10px 6px #51cbee;
+ .tb-node-title {
+ text-decoration: underline;
+ font-weight: bold;
+ }
+ }
+ &.tb-rule-node-invalid {
+ box-shadow: 0 0 10px 6px #ff5c50;
+ }
+ &.tb-input-type {
+ background-color: #a3eaa9;
+ user-select: none;
+ }
+ md-icon {
+ font-size: 20px;
+ width: 20px;
+ height: 20px;
+ min-height: 20px;
+ min-width: 20px;
+ padding-right: 4px;
+ }
+ .tb-node-type {
+
+ }
+ .tb-node-title {
+ font-weight: 500;
+ }
+ .tb-node-type, .tb-node-title {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+}
+
+.fc-node {
+ z-index: 1;
+ outline: none;
+ border-radius: 8px;
+ &.fc-dragging {
+ z-index: 10;
+ }
+ p {
+ padding: 0 15px;
+ text-align: center;
+ }
+ .fc-node-overlay {
+ position: absolute;
+ pointer-events: none;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #000;
+ opacity: 0;
+ border-radius: 5px;
+ }
+ &.fc-hover {
+ .fc-node-overlay {
+ opacity: 0.25;
+ }
+ }
+ &.fc-selected {
+ .fc-node-overlay {
+ opacity: 0.25;
+ }
+ }
+ &.fc-selected {
+ &:not(.fc-edit) {
+ border: solid 3px red;
+ margin: -3px;
+ }
+ }
+}
+
+.fc-leftConnectors, .fc-rightConnectors {
+ position: absolute;
+ top: 0;
+ height: 100%;
+
+ display: flex;
+ flex-direction: column;
+
+ z-index: 0;
+ .fc-magnet {
+ align-items: center;
+ }
+}
+
+.fc-leftConnectors {
+ left: -20px;
+}
+
+.fc-rightConnectors {
+ right: -20px;
+}
+
+.fc-magnet {
+ display: flex;
+ flex-grow: 1;
+ height: 60px;
+ justify-content: center;
+}
+
+.fc-connector {
+ width: 14px;
+ height: 14px;
+ border: 1px solid #333;
+ margin: 10px;
+ border-radius: 5px;
+ background-color: #ccc;
+ pointer-events: all;
+}
+
+.fc-connector.fc-hover {
+ background-color: #000;
+}
+
+.fc-arrow-marker {
+ polygon {
+ stroke: gray;
+ fill: gray;
+ }
+}
+
+.fc-arrow-marker-selected {
+ polygon {
+ stroke: red;
+ fill: red;
+ }
+}
+
+.fc-edge {
+ outline: none;
+ stroke: gray;
+ stroke-width: 4;
+ fill: transparent;
+ transition: stroke-width .2s;
+ &.fc-selected {
+ stroke: red;
+ stroke-width: 4;
+ fill: transparent;
+ }
+ &.fc-active {
+ animation: dash 3s linear infinite;
+ stroke-dasharray: 20;
+ }
+ &.fc-hover {
+ stroke: gray;
+ stroke-width: 6;
+ fill: transparent;
+ }
+ &.fc-dragging {
+ pointer-events: none;
+ }
+}
+
+.edge-endpoint {
+ fill: gray;
+}
+
+.fc-nodedelete {
+ display: none;
+ font-size: 18px;
+}
+
+.fc-nodeedit {
+ display: none;
+ font-size: 15px;
+}
+
+.fc-edit {
+ .fc-nodedelete, .fc-nodeedit {
+ outline: none;
+ display: block;
+ position: absolute;
+ border: solid 2px white;
+ border-radius: 50%;
+ font-weight: 600;
+ line-height: 20px;
+ height: 20px;
+ padding-top: 2px;
+ width: 22px;
+ background: #f83e05;
+ color: #fff;
+ text-align: center;
+ vertical-align: bottom;
+ cursor: pointer;
+ }
+
+ .fc-nodeedit {
+ top: -24px;
+ right: 16px;
+ }
+
+ .fc-nodedelete {
+ top: -24px;
+ right: -13px;
+ }
+
+}
+
+.fc-noselect {
+ -webkit-touch-callout: none; /* iOS Safari */
+ -webkit-user-select: none; /* Safari */
+ -khtml-user-select: none; /* Konqueror HTML */
+ -moz-user-select: none; /* Firefox */
+ -ms-user-select: none; /* Internet Explorer/Edge */
+ user-select: none; /* Non-prefixed version, currently
+ supported by Chrome and Opera */
+}
+
+.fc-edge-label {
+ position: absolute;
+ transition: transform .2s;
+// opacity: 0.8;
+ &.ng-leave {
+ transition: 0s none;
+ }
+ &.fc-hover {
+ transform: scale(1.25);
+ }
+ &.fc-selected {
+ .fc-edge-label-text {
+ span {
+ border: solid red;
+ color: #fff;
+ font-weight: 600;
+ background-color: red;
+ }
+ }
+ }
+ .fc-nodeedit {
+ top: -30px;
+ right: 14px;
+ }
+ .fc-nodedelete {
+ top: -30px;
+ right: -13px;
+ }
+ &:focus {
+ outline: 0;
+ }
+}
+
+.fc-edge-label-text {
+ position: absolute;
+ -webkit-transform: translate(-50%, -50%);
+ transform: translate(-50%, -50%);
+ white-space: nowrap;
+ text-align: center;
+ font-size: 14px;
+ font-weight: 600;
+ span {
+ cursor: default;
+ border: solid 2px #003a79;
+ border-radius: 10px;
+ color: #003a79;
+ background-color: #fff;
+ padding: 3px 5px;
+ }
+}
+
+.fc-select-rectangle {
+ border: 2px dashed #5262ff;
+ position: absolute;
+ background: rgba(20,125,255,0.1);
+ z-index: 2;
+}
+
+@keyframes dash {
+ from {
+ stroke-dashoffset: 500;
+ }
+}
+
+.tb-rule-node-tooltip, .tb-rule-node-help {
+ color: #333;
+}
+
+.tb-rule-node-tooltip {
+ font-size: 14px;
+ max-width: 300px;
+ &.tb-lib-tooltip {
+ width: 300px;
+ }
+}
+
+.tb-rule-node-help {
+ font-size: 16px;
+}
+
+.tb-rule-node-error-tooltip {
+ font-size: 16px;
+ color: #ea0d0d;
+}
+
+.tb-rule-node-tooltip, .tb-rule-node-error-tooltip, .tb-rule-node-help {
+ #tb-node-content {
+ .tb-node-title {
+ font-weight: 600;
+ }
+ .tb-node-description {
+ font-style: italic;
+ color: #555;
+ }
+ .tb-node-details {
+ padding-top: 10px;
+ padding-bottom: 10px;
+ }
+ code {
+ padding: 0px 3px 2px 3px;
+ margin: 1px;
+ color: #AD1625;
+ white-space: nowrap;
+ background-color: #f7f7f9;
+ border: 1px solid #e1e1e8;
+ border-radius: 2px;
+ font-size: 12px;
+ }
+ }
+}
\ No newline at end of file
ui/src/app/rulechain/rulechain.tpl.html 246(+246 -0)
diff --git a/ui/src/app/rulechain/rulechain.tpl.html b/ui/src/app/rulechain/rulechain.tpl.html
new file mode 100644
index 0000000..a84df90
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.tpl.html
@@ -0,0 +1,246 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+
+<md-content flex tb-expand-fullscreen tb-confirm-on-exit is-dirty="vm.isConfirmOnExit"
+ expand-tooltip-direction="bottom" layout="column" class="tb-rulechain"
+ ng-keydown="vm.keyDown($event)"
+ ng-keyup="vm.keyUp($event)" on-fullscreen-changed="vm.isFullscreen = expanded">
+ <section class="tb-rulechain-container" flex layout="column">
+ <div class="tb-rulechain-layout" flex layout="row">
+ <section layout="row" layout-wrap
+ class="tb-header-buttons md-fab tb-library-open">
+ <md-button ng-show="!vm.isLibraryOpen"
+ class="tb-btn-header tb-btn-open-library md-primary md-fab md-fab-top-left"
+ aria-label="{{ 'rulenode.open-node-library' | translate }}"
+ ng-click="vm.isLibraryOpen = true">
+ <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+ {{ 'rulenode.open-node-library' | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="menu"></ng-md-icon>
+ </md-button>
+ </section>
+ <md-sidenav class="tb-rulechain-library md-sidenav-left md-whiteframe-4dp"
+ md-disable-backdrop
+ md-is-locked-open="vm.isLibraryOpenReadonly"
+ md-is-open="vm.isLibraryOpenReadonly"
+ md-component-id="rulechain-library-sidenav" layout="column">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <md-button class="md-icon-button tb-small" aria-label="{{ 'action.search' | translate }}">
+ <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+ <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+ {{'rulenode.search' | translate}}
+ </md-tooltip>
+ </md-button>
+ <div layout="row" md-theme="tb-dark" flex>
+ <md-input-container flex>
+ <label> </label>
+ <input ng-model="vm.ruleNodeSearch" placeholder="{{'rulenode.search' | translate}}"/>
+ </md-input-container>
+ </div>
+ <md-button class="md-icon-button tb-small" aria-label="Close"
+ ng-show="vm.ruleNodeSearch"
+ ng-click="vm.ruleNodeSearch = ''">
+ <md-icon aria-label="Close" class="material-icons">close</md-icon>
+ <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+ {{ 'action.clear-search' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button class="md-icon-button tb-small" aria-label="Close" ng-click="vm.isLibraryOpen = false">
+ <md-icon aria-label="Close" class="material-icons">chevron_left</md-icon>
+ <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+ {{ 'action.close' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-expansion-panel-group flex
+ ng-if="vm.ruleChainLibraryLoaded" class="tb-rulechain-library-panel-group"
+ md-component-id="libraryPanelGroup" auto-expand="true" multiple>
+ <md-expansion-panel md-component-id="{{typeId}}" id="{{typeId}}" ng-repeat="(typeId, typeModel) in vm.ruleNodeTypesModel">
+ <md-expansion-panel-collapsed ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)"
+ ng-mouseleave="vm.destroyTooltips()">
+ <md-icon aria-label="node-type-icon"
+ class="material-icons" style="margin-right: 8px;">{{vm.types.ruleNodeType[typeId].icon}}</md-icon>
+ <div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
+ <md-expansion-panel-icon></md-expansion-panel-icon>
+ </md-expansion-panel-collapsed>
+ <md-expansion-panel-expanded>
+ <md-expansion-panel-header ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)"
+ ng-mouseleave="vm.destroyTooltips()"
+ ng-click="vm.$mdExpansionPanel(typeId).collapse()">
+ <md-icon aria-label="node-type-icon"
+ class="material-icons" style="margin-right: 8px;">{{vm.types.ruleNodeType[typeId].icon}}</md-icon>
+ <div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
+ <md-expansion-panel-icon></md-expansion-panel-icon>
+ </md-expansion-panel-header>
+ <md-expansion-panel-content>
+ <fc-canvas id="tb-rulechain-{{typeId}}"
+ model="vm.ruleNodeTypesModel[typeId].model" selected-objects="vm.ruleNodeTypesModel[typeId].selectedObjects"
+ automatic-resize="false"
+ callbacks="vm.nodeLibCallbacks"
+ node-width="170"
+ node-height="50"
+ control="vm.ruleNodeTypesCanvasControl[typeId]"
+ drop-target-id="'tb-rulchain-canvas'"></fc-canvas>
+ </md-expansion-panel-content>
+ </md-expansion-panel-expanded>
+ </md-expansion-panel>
+ </md-expansion-panel-group>
+ </md-sidenav>
+ <md-menu flex style="position: relative;" md-position-mode="target target" tb-offset-x="-20" tb-offset-y="-45" tb-mousepoint-menu>
+ <div class="tb-absolute-fill tb-rulechain-graph" ng-click="" tb-contextmenu="vm.openRuleChainContextMenu($event, $mdOpenMousepointMenu)">
+ <fc-canvas id="tb-rulchain-canvas"
+ model="vm.ruleChainModel"
+ selected-objects="vm.selectedObjects"
+ edge-style="curved"
+ node-width="170"
+ node-height="50"
+ automatic-resize="true"
+ control="vm.canvasControl"
+ callbacks="vm.editCallbacks">
+ </fc-canvas>
+ </div>
+ <md-menu-content id="tb-rule-chain-context-menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
+ <div class="tb-context-menu-header {{vm.contextInfo.headerClass}}">
+ <md-icon ng-if="!vm.contextInfo.iconUrl" aria-label="node-type-icon"
+ class="material-icons">{{vm.contextInfo.icon}}</md-icon>
+ <md-icon ng-if="vm.contextInfo.iconUrl" aria-label="node-type-icon"
+ md-svg-icon="{{vm.contextInfo.iconUrl}}"></md-icon>
+ <div flex>
+ <div class="tb-context-menu-title">{{vm.contextInfo.title}}</div>
+ <div class="tb-context-menu-subtitle">{{vm.contextInfo.subtitle}}</div>
+ </div>
+ </div>
+ <div ng-repeat="item in vm.contextInfo.items">
+ <md-divider ng-if="item.divider"></md-divider>
+ <md-menu-item ng-if="!item.divider">
+ <md-button ng-disabled="!item.enabled" ng-click="item.action(vm.contextMenuEvent)">
+ <span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
+ <md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
+ <span translate>{{item.value}}</span>
+ </md-button>
+ </md-menu-item>
+ </div>
+ </md-menu-content>
+ </md-menu>
+ </div>
+ <tb-details-sidenav class="tb-rulenode-details-sidenav"
+ header-title="{{vm.editingRuleNode.name}}"
+ header-subtitle="{{(vm.types.ruleNodeType[vm.editingRuleNode.component.type].name | translate)
+ + ' - ' + vm.editingRuleNode.component.name}}"
+ is-read-only="vm.selectedRuleNodeTabIndex > 0"
+ is-open="vm.isEditingRuleNode"
+ tb-enable-backdrop
+ is-always-edit="true"
+ on-close-details="vm.onEditRuleNodeClosed()"
+ on-toggle-details-edit-mode="vm.onRevertRuleNodeEdit(vm.ruleNodeForm)"
+ on-apply-details="vm.saveRuleNode(vm.ruleNodeForm)"
+ the-form="vm.ruleNodeForm">
+ <details-buttons tb-help="vm.helpLinkIdForRuleNodeType()" help-container-id="help-container">
+ <div id="help-container"></div>
+ </details-buttons>
+ <md-tabs md-selected="vm.selectedRuleNodeTabIndex"
+ id="ruleNodeTabs" md-border-bottom flex class="tb-absolute-fill" ng-if="vm.isEditingRuleNode">
+ <md-tab label="{{ 'rulenode.details' | translate }}">
+ <form name="vm.ruleNodeForm">
+ <tb-rule-node
+ rule-node="vm.editingRuleNode"
+ rule-chain-id="vm.ruleChain.id.id"
+ is-edit="true"
+ is-read-only="false"
+ on-delete-rule-node="vm.deleteRuleNode(event, vm.editingRuleNode)"
+ the-form="vm.ruleNodeForm">
+ </tb-rule-node>
+ </form>
+ </md-tab>
+ <md-tab ng-if="vm.isEditingRuleNode && vm.editingRuleNode.ruleNodeId"
+ md-on-select="vm.triggerResize()" label="{{ 'rulenode.events' | translate }}">
+ <tb-event-table flex entity-type="vm.types.entityType.rulenode"
+ entity-id="vm.editingRuleNode.ruleNodeId.id"
+ tenant-id="vm.ruleChain.tenantId.id"
+ debug-event-types="{{vm.types.debugEventType.debugRuleNode.value}}"
+ default-event-type="{{vm.types.debugEventType.debugRuleNode.value}}">
+ </tb-event-table>
+ </md-tab>
+ <md-tab label="{{ 'rulenode.help' | translate }}">
+ <div class="tb-rule-node-help">
+ <div id="tb-node-content" class="md-padding" layout="column">
+ <div class="tb-node-title">{{vm.editingRuleNode.component.name}}</div>
+ <div> </div>
+ <div class="tb-node-description">{{vm.editingRuleNode.component.configurationDescriptor.nodeDefinition.description}}</div>
+ <div> </div>
+ <div class="tb-node-details" ng-bind-html="vm.editingRuleNode.component.configurationDescriptor.nodeDefinition.details"></div>
+ </div>
+ </div>
+ </md-tab>
+ </md-tabs>
+ </tb-details-sidenav>
+ <tb-details-sidenav class="tb-rulenode-link-details-sidenav"
+ header-title="{{vm.editingRuleNodeLink.label}}"
+ header-subtitle="{{'rulenode.link-details' | translate}}"
+ is-read-only="false"
+ is-open="vm.isEditingRuleNodeLink"
+ tb-enable-backdrop
+ is-always-edit="true"
+ on-close-details="vm.onEditRuleNodeLinkClosed()"
+ on-toggle-details-edit-mode="vm.onRevertRuleNodeLinkEdit(vm.ruleNodeLinkForm)"
+ on-apply-details="vm.saveRuleNodeLink(vm.ruleNodeLinkForm)"
+ the-form="vm.ruleNodeLinkForm">
+ <details-buttons tb-help="'ruleEngine'" help-container-id="link-help-container">
+ <div id="link-help-container"></div>
+ </details-buttons>
+ <form name="vm.ruleNodeLinkForm" ng-if="vm.isEditingRuleNodeLink">
+ <tb-rule-node-link
+ link="vm.editingRuleNodeLink"
+ labels="vm.editingRuleNodeLinkLabels"
+ is-edit="true"
+ is-read-only="false"
+ the-form="vm.ruleNodeLinkForm">
+ </tb-rule-node-link>
+ </form>
+ </tb-details-sidenav>
+ </section>
+ <section layout="row" layout-wrap class="tb-footer-buttons md-fab" layout-align="start end">
+ <md-button ng-disabled="$root.loading" ng-show="vm.objectsSelected()" class="tb-btn-footer md-accent md-hue-2 md-fab"
+ ng-click="vm.deleteSelected()" aria-label="{{ 'action.delete' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'rulenode.delete-selected-objects' | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="delete"></ng-md-icon>
+ </md-button>
+ <md-button ng-disabled="$root.loading || vm.isInvalid || (!vm.isDirty && !vm.isImport)"
+ class="tb-btn-footer md-accent md-hue-2 md-fab"
+ aria-label="{{ 'action.apply' | translate }}"
+ ng-click="vm.saveRuleChain()">
+ <md-tooltip md-direction="top">
+ {{ 'action.apply-changes' | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="done"></ng-md-icon>
+ </md-button>
+ <md-button ng-disabled="$root.loading || !vm.isDirty"
+ class="tb-btn-footer md-accent md-hue-2 md-fab"
+ aria-label="{{ 'action.decline-changes' | translate }}"
+ ng-click="vm.revertRuleChain()">
+ <md-tooltip md-direction="top">
+ {{ 'action.decline-changes' | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="close"></ng-md-icon>
+ </md-button>
+ </section>
+</md-content>
ui/src/app/rulechain/rulechain-card.tpl.html 18(+18 -0)
diff --git a/ui/src/app/rulechain/rulechain-card.tpl.html b/ui/src/app/rulechain/rulechain-card.tpl.html
new file mode 100644
index 0000000..a3ebfff
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain-card.tpl.html
@@ -0,0 +1,18 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div ng-if="item && item.root" translate>rulechain.root</div>
diff --git a/ui/src/app/rulechain/rulechain-fieldset.tpl.html b/ui/src/app/rulechain/rulechain-fieldset.tpl.html
new file mode 100644
index 0000000..9f786ab
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain-fieldset.tpl.html
@@ -0,0 +1,57 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-button ng-click="onExportRuleChain({event: $event})"
+ ng-show="!isEdit"
+ class="md-raised md-primary">{{ 'rulechain.export' | translate }}</md-button>
+<md-button ng-click="onSetRootRuleChain({event: $event})"
+ ng-show="!isEdit && !ruleChain.root"
+ class="md-raised md-primary">{{ 'rulechain.set-root' | translate }}</md-button>
+<md-button ng-click="onDeleteRuleChain({event: $event})"
+ ng-show="!isEdit && !ruleChain.root"
+ class="md-raised md-primary">{{ 'rulechain.delete' | translate }}</md-button>
+
+<div layout="row">
+ <md-button ngclipboard data-clipboard-action="copy"
+ ngclipboard-success="onRuleChainIdCopied(e)"
+ data-clipboard-text="{{ruleChain.id.id}}" ng-show="!isEdit"
+ class="md-raised">
+ <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+ <span translate>rulechain.copyId</span>
+ </md-button>
+</div>
+
+<md-content class="md-padding tb-rulechain-fieldset" layout="column">
+ <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
+ <md-input-container class="md-block">
+ <label translate>rulechain.name</label>
+ <input required name="name" ng-model="ruleChain.name">
+ <div ng-messages="theForm.name.$error">
+ <div translate ng-message="required">rulechain.name-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <md-checkbox ng-disabled="$root.loading || !isEdit" aria-label="{{ 'rulechain.debug-mode' | translate }}"
+ ng-model="ruleChain.debugMode">{{ 'rulechain.debug-mode' | translate }}
+ </md-checkbox>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>rulechain.description</label>
+ <textarea ng-model="ruleChain.additionalInfo.description" rows="2"></textarea>
+ </md-input-container>
+ </fieldset>
+</md-content>
ui/src/app/rulechain/rulechains.controller.js 215(+215 -0)
diff --git a/ui/src/app/rulechain/rulechains.controller.js b/ui/src/app/rulechain/rulechains.controller.js
new file mode 100644
index 0000000..3da5a51
--- /dev/null
+++ b/ui/src/app/rulechain/rulechains.controller.js
@@ -0,0 +1,215 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import addRuleChainTemplate from './add-rulechain.tpl.html';
+import ruleChainCard from './rulechain-card.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleChainsController(ruleChainService, userService, importExport, $state,
+ $stateParams, $filter, $translate, $mdDialog, types) {
+
+ var ruleChainActionsList = [
+ {
+ onAction: function ($event, item) {
+ vm.grid.openItem($event, item);
+ },
+ name: function() { return $translate.instant('rulechain.details') },
+ details: function() { return $translate.instant('rulechain.rulechain-details') },
+ icon: "edit"
+ },
+ {
+ onAction: function ($event, item) {
+ exportRuleChain($event, item);
+ },
+ name: function() { $translate.instant('action.export') },
+ details: function() { return $translate.instant('rulechain.export') },
+ icon: "file_download"
+ },
+ {
+ onAction: function ($event, item) {
+ setRootRuleChain($event, item);
+ },
+ name: function() { return $translate.instant('rulechain.set-root') },
+ details: function() { return $translate.instant('rulechain.set-root') },
+ icon: "flag",
+ isEnabled: isNonRootRuleChain
+ },
+ {
+ onAction: function ($event, item) {
+ vm.grid.deleteItem($event, item);
+ },
+ name: function() { return $translate.instant('action.delete') },
+ details: function() { return $translate.instant('rulechain.delete') },
+ icon: "delete",
+ isEnabled: isNonRootRuleChain
+ }
+ ];
+
+ var ruleChainAddItemActionsList = [
+ {
+ onAction: function ($event) {
+ vm.grid.addItem($event);
+ },
+ name: function() { return $translate.instant('action.create') },
+ details: function() { return $translate.instant('rulechain.create-new-rulechain') },
+ icon: "insert_drive_file"
+ },
+ {
+ onAction: function ($event) {
+ importExport.importRuleChain($event).then(
+ function(ruleChainImport) {
+ $state.go('home.ruleChains.importRuleChain', {ruleChainImport:ruleChainImport});
+ }
+ );
+ },
+ name: function() { return $translate.instant('action.import') },
+ details: function() { return $translate.instant('rulechain.import') },
+ icon: "file_upload"
+ }
+ ];
+
+ var vm = this;
+
+ vm.types = types;
+
+ vm.ruleChainGridConfig = {
+
+ refreshParamsFunc: null,
+
+ deleteItemTitleFunc: deleteRuleChainTitle,
+ deleteItemContentFunc: deleteRuleChainText,
+ deleteItemsTitleFunc: deleteRuleChainsTitle,
+ deleteItemsActionTitleFunc: deleteRuleChainsActionTitle,
+ deleteItemsContentFunc: deleteRuleChainsText,
+
+ fetchItemsFunc: fetchRuleChains,
+ saveItemFunc: saveRuleChain,
+ clickItemFunc: openRuleChain,
+ deleteItemFunc: deleteRuleChain,
+
+ getItemTitleFunc: getRuleChainTitle,
+ itemCardTemplateUrl: ruleChainCard,
+ parentCtl: vm,
+
+ actionsList: ruleChainActionsList,
+ addItemActions: ruleChainAddItemActionsList,
+
+ onGridInited: gridInited,
+
+ addItemTemplateUrl: addRuleChainTemplate,
+
+ addItemText: function() { return $translate.instant('rulechain.add-rulechain-text') },
+ noItemsText: function() { return $translate.instant('rulechain.no-rulechains-text') },
+ itemDetailsText: function() { return $translate.instant('rulechain.rulechain-details') },
+ isSelectionEnabled: isNonRootRuleChain
+ };
+
+ if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
+ vm.ruleChainGridConfig.items = $stateParams.items;
+ }
+
+ if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
+ vm.ruleChainGridConfig.topIndex = $stateParams.topIndex;
+ }
+
+ vm.isRootRuleChain = isRootRuleChain;
+ vm.isNonRootRuleChain = isNonRootRuleChain;
+
+ vm.exportRuleChain = exportRuleChain;
+ vm.setRootRuleChain = setRootRuleChain;
+
+ function deleteRuleChainTitle(ruleChain) {
+ return $translate.instant('rulechain.delete-rulechain-title', {ruleChainName: ruleChain.name});
+ }
+
+ function deleteRuleChainText() {
+ return $translate.instant('rulechain.delete-rulechain-text');
+ }
+
+ function deleteRuleChainsTitle(selectedCount) {
+ return $translate.instant('rulechain.delete-rulechains-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteRuleChainsActionTitle(selectedCount) {
+ return $translate.instant('rulechain.delete-rulechains-action-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteRuleChainsText() {
+ return $translate.instant('rulechain.delete-rulechains-text');
+ }
+
+ function gridInited(grid) {
+ vm.grid = grid;
+ }
+
+ function fetchRuleChains(pageLink) {
+ return ruleChainService.getRuleChains(pageLink);
+ }
+
+ function saveRuleChain(ruleChain) {
+ return ruleChainService.saveRuleChain(ruleChain);
+ }
+
+ function openRuleChain($event, ruleChain) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ $state.go('home.ruleChains.ruleChain', {ruleChainId: ruleChain.id.id});
+ }
+
+ function deleteRuleChain(ruleChainId) {
+ return ruleChainService.deleteRuleChain(ruleChainId);
+ }
+
+ function getRuleChainTitle(ruleChain) {
+ return ruleChain ? ruleChain.name : '';
+ }
+
+ function isRootRuleChain(ruleChain) {
+ return ruleChain && ruleChain.root;
+ }
+
+ function isNonRootRuleChain(ruleChain) {
+ return ruleChain && !ruleChain.root;
+ }
+
+ function exportRuleChain($event, ruleChain) {
+ $event.stopPropagation();
+ importExport.exportRuleChain(ruleChain.id.id);
+ }
+
+ function setRootRuleChain($event, ruleChain) {
+ $event.stopPropagation();
+ var confirm = $mdDialog.confirm()
+ .targetEvent($event)
+ .title($translate.instant('rulechain.set-root-rulechain-title', {ruleChainName: ruleChain.name}))
+ .htmlContent($translate.instant('rulechain.set-root-rulechain-text'))
+ .ariaLabel($translate.instant('rulechain.set-root'))
+ .cancel($translate.instant('action.no'))
+ .ok($translate.instant('action.yes'));
+ $mdDialog.show(confirm).then(function () {
+ ruleChainService.setRootRuleChain(ruleChain.id.id).then(
+ () => {
+ vm.grid.refreshList();
+ }
+ );
+ });
+
+ }
+}
ui/src/app/rulechain/rulechains.tpl.html 78(+78 -0)
diff --git a/ui/src/app/rulechain/rulechains.tpl.html b/ui/src/app/rulechain/rulechains.tpl.html
new file mode 100644
index 0000000..c780b46
--- /dev/null
+++ b/ui/src/app/rulechain/rulechains.tpl.html
@@ -0,0 +1,78 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<tb-grid grid-configuration="vm.ruleChainGridConfig">
+ <details-buttons tb-help="'rulechains'" help-container-id="help-container">
+ <div id="help-container"></div>
+ </details-buttons>
+ <md-tabs ng-class="{'tb-headless': (vm.grid.detailsConfig.isDetailsEditMode || !vm.isRuleChainEditable(vm.grid.operatingItem()))}"
+ id="tabs" md-border-bottom flex class="tb-absolute-fill">
+ <md-tab label="{{ 'rulechain.details' | translate }}">
+ <tb-rule-chain rule-chain="vm.grid.operatingItem()"
+ is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+ is-read-only="vm.grid.isDetailsReadOnly(vm.grid.operatingItem())"
+ the-form="vm.grid.detailsForm"
+ on-set-root-rule-chain="vm.setRootRuleChain(event, vm.grid.detailsConfig.currentItem)"
+ on-export-rule-chain="vm.exportRuleChain(event, vm.grid.detailsConfig.currentItem)"
+ on-delete-rule-chain="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)">
+ </tb-rule-chain>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
+ <tb-attribute-table flex
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.rulechain}}"
+ entity-name="vm.grid.operatingItem().name"
+ default-attribute-scope="{{vm.types.attributesScope.server.value}}">
+ </tb-attribute-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
+ <tb-attribute-table flex
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.rulechain}}"
+ entity-name="vm.grid.operatingItem().name"
+ default-attribute-scope="{{vm.types.latestTelemetry.value}}"
+ disable-attribute-scope-selection="true">
+ </tb-attribute-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
+ <tb-alarm-table flex entity-type="vm.types.entityType.rulechain"
+ entity-id="vm.grid.operatingItem().id.id">
+ </tb-alarm-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'rulechain.events' | translate }}">
+ <tb-event-table flex entity-type="vm.types.entityType.rulechain"
+ entity-id="vm.grid.operatingItem().id.id"
+ tenant-id="vm.grid.operatingItem().tenantId.id"
+ debug-event-types="{{vm.types.debugEventType.debugRuleChain.value}}"
+ default-event-type="{{vm.types.debugEventType.debugRuleChain.value}}">
+ </tb-event-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
+ <tb-relation-table flex
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.rulechain}}">
+ </tb-relation-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem()) && vm.grid.isTenantAdmin()"
+ md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+ <tb-audit-log-table flex entity-type="vm.types.entityType.rulechain"
+ entity-id="vm.grid.operatingItem().id.id"
+ audit-log-mode="{{vm.types.auditLogMode.entity}}">
+ </tb-audit-log-table>
+ </md-tab>
+ </md-tabs>
+</tb-grid>
ui/src/app/rulechain/rulenode.directive.js 81(+81 -0)
diff --git a/ui/src/app/rulechain/rulenode.directive.js b/ui/src/app/rulechain/rulenode.directive.js
new file mode 100644
index 0000000..be3e9c3
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode.directive.js
@@ -0,0 +1,81 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './rulenode.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleNodeFieldsetTemplate from './rulenode-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleNodeDirective($compile, $templateCache, ruleChainService, types) {
+ var linker = function (scope, element) {
+ var template = $templateCache.get(ruleNodeFieldsetTemplate);
+ element.html(template);
+
+ scope.types = types;
+
+ scope.params = {
+ targetRuleChainId: null
+ };
+
+ scope.$watch('ruleNode', function() {
+ if (scope.ruleNode && scope.ruleNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
+ scope.params.targetRuleChainId = scope.ruleNode.targetRuleChainId;
+ watchTargetRuleChain();
+ } else {
+ if (scope.targetRuleChainWatch) {
+ scope.targetRuleChainWatch();
+ scope.targetRuleChainWatch = null;
+ }
+ }
+ });
+
+ function watchTargetRuleChain() {
+ scope.targetRuleChainWatch = scope.$watch('params.targetRuleChainId',
+ function(targetRuleChainId) {
+ if (scope.ruleNode.targetRuleChainId != targetRuleChainId) {
+ scope.ruleNode.targetRuleChainId = targetRuleChainId;
+ if (targetRuleChainId) {
+ ruleChainService.getRuleChain(targetRuleChainId).then(
+ (ruleChain) => {
+ scope.ruleNode.name = ruleChain.name;
+ }
+ );
+ } else {
+ scope.ruleNode.name = "";
+ }
+ }
+ }
+ );
+ }
+ $compile(element.contents())(scope);
+ }
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ ruleChainId: '=',
+ ruleNode: '=',
+ isEdit: '=',
+ isReadOnly: '=',
+ theForm: '=',
+ onDeleteRuleNode: '&'
+ }
+ };
+}
ui/src/app/rulechain/rulenode.tpl.html 54(+54 -0)
diff --git a/ui/src/app/rulechain/rulenode.tpl.html b/ui/src/app/rulechain/rulenode.tpl.html
new file mode 100644
index 0000000..6a82a7f
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode.tpl.html
@@ -0,0 +1,54 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div
+ id="{{node.id}}"
+ ng-attr-style="position: absolute; top: {{ node.y }}px; left: {{ node.x }}px;"
+ ng-dblclick="callbacks.doubleClick($event, node)"
+ ng-mousedown="callbacks.mouseDown($event, node)"
+ ng-mouseenter="callbacks.mouseEnter($event, node)"
+ ng-mouseleave="callbacks.mouseLeave($event, node)">
+ <div class="{{flowchartConstants.nodeOverlayClass}}"></div>
+ <div class="tb-rule-node {{node.nodeClass}}" ng-class="{'tb-rule-node-highlighted' : node.highlighted, 'tb-rule-node-invalid': node.error }">
+ <md-icon ng-if="!node.iconUrl" aria-label="node-type-icon" flex="15"
+ class="material-icons">{{node.icon}}</md-icon>
+ <md-icon ng-if="node.iconUrl" aria-label="node-type-icon" flex="15"
+ md-svg-icon="{{node.iconUrl}}"></md-icon>
+ <div layout="column" flex="85" layout-align="center">
+ <span class="tb-node-type">{{ node.component.name }}</span>
+ <span class="tb-node-title" ng-if="node.name">{{ node.name }}</span>
+ </div>
+ <div class="{{flowchartConstants.leftConnectorClass}}">
+ <div fc-magnet
+ ng-repeat="connector in modelservice.nodes.getConnectorsByType(node, flowchartConstants.leftConnectorType)">
+ <div fc-connector></div>
+ </div>
+ </div>
+ <div class="{{flowchartConstants.rightConnectorClass}}">
+ <div fc-magnet
+ ng-repeat="connector in modelservice.nodes.getConnectorsByType(node, flowchartConstants.rightConnectorType)">
+ <div fc-connector></div>
+ </div>
+ </div>
+ </div>
+ <div ng-if="modelservice.isEditable() && !node.readonly" class="fc-nodeedit" ng-click="callbacks.nodeEdit($event, node)">
+ <i class="fa fa-pencil" aria-hidden="true"></i>
+ </div>
+ <div ng-if="modelservice.isEditable() && !node.readonly" class="fc-nodedelete" ng-click="modelservice.nodes.delete(node)">
+ ×
+ </div>
+</div>
diff --git a/ui/src/app/rulechain/rulenode-config.directive.js b/ui/src/app/rulechain/rulenode-config.directive.js
new file mode 100644
index 0000000..cf13e96
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-config.directive.js
@@ -0,0 +1,79 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleNodeConfigTemplate from './rulenode-config.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleNodeConfigDirective($compile, $templateCache, $injector, $translate) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+ var template = $templateCache.get(ruleNodeConfigTemplate);
+ element.html(template);
+
+ scope.$watch('configuration', function (newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal)) {
+ ngModelCtrl.$setViewValue(scope.configuration);
+ }
+ });
+
+ ngModelCtrl.$render = function () {
+ scope.configuration = ngModelCtrl.$viewValue;
+ };
+
+ scope.useDefinedDirective = function() {
+ return scope.nodeDefinition &&
+ scope.nodeDefinition.configDirective && !scope.definedDirectiveError;
+ };
+
+ scope.$watch('nodeDefinition', () => {
+ if (scope.nodeDefinition) {
+ validateDefinedDirective();
+ }
+ });
+
+ function validateDefinedDirective() {
+ if (scope.nodeDefinition.uiResourceLoadError && scope.nodeDefinition.uiResourceLoadError.length) {
+ scope.definedDirectiveError = scope.nodeDefinition.uiResourceLoadError;
+ } else {
+ var definedDirective = scope.nodeDefinition.configDirective;
+ if (definedDirective && definedDirective.length) {
+ if (!$injector.has(definedDirective + 'Directive')) {
+ scope.definedDirectiveError = $translate.instant('rulenode.directive-is-not-loaded', {directiveName: definedDirective});
+ }
+ }
+ }
+ }
+
+ $compile(element.contents())(scope);
+ };
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {
+ ruleNodeId:'=',
+ nodeDefinition:'=',
+ required:'=ngRequired',
+ readonly:'=ngReadonly'
+ },
+ link: linker
+ };
+
+}
diff --git a/ui/src/app/rulechain/rulenode-config.tpl.html b/ui/src/app/rulechain/rulenode-config.tpl.html
new file mode 100644
index 0000000..3148c39
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-config.tpl.html
@@ -0,0 +1,33 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+
+<tb-rule-node-defined-config ng-if="useDefinedDirective()"
+ rule-node-id="ruleNodeId"
+ ng-model="configuration"
+ rule-node-directive="{{nodeDefinition.configDirective}}"
+ ng-required="required"
+ ng-readonly="readonly">
+</tb-rule-node-defined-config>
+<div class="tb-rulenode-directive-error" ng-if="definedDirectiveError">{{definedDirectiveError}}</div>
+<tb-json-object-edit ng-if="!useDefinedDirective()"
+ class="tb-rule-node-configuration-json"
+ ng-model="configuration"
+ label="{{ 'rulenode.configuration' | translate }}"
+ ng-required="required"
+ fill-height="true">
+</tb-json-object-edit>
diff --git a/ui/src/app/rulechain/rulenode-defined-config.directive.js b/ui/src/app/rulechain/rulenode-defined-config.directive.js
new file mode 100644
index 0000000..d358955
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-defined-config.directive.js
@@ -0,0 +1,68 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const SNAKE_CASE_REGEXP = /[A-Z]/g;
+
+/*@ngInject*/
+export default function RuleNodeDefinedConfigDirective($compile) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+
+ attrs.$observe('ruleNodeDirective', function() {
+ loadTemplate();
+ });
+
+ scope.$watch('configuration', function (newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal)) {
+ ngModelCtrl.$setViewValue(scope.configuration);
+ }
+ });
+
+ ngModelCtrl.$render = function () {
+ scope.configuration = ngModelCtrl.$viewValue;
+ };
+
+ function loadTemplate() {
+ if (scope.ruleNodeConfigScope) {
+ scope.ruleNodeConfigScope.$destroy();
+ }
+ var directive = snake_case(attrs.ruleNodeDirective, '-');
+ var template = `<${directive} rule-node-id="ruleNodeId" ng-model="configuration" ng-required="required" ng-readonly="readonly"></${directive}>`;
+ element.html(template);
+ scope.ruleNodeConfigScope = scope.$new();
+ $compile(element.contents())(scope.ruleNodeConfigScope);
+ }
+
+ function snake_case(name, separator) {
+ separator = separator || '_';
+ return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
+ return (pos ? separator : '') + letter.toLowerCase();
+ });
+ }
+ };
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {
+ ruleNodeId:'=',
+ required:'=ngRequired',
+ readonly:'=ngReadonly'
+ },
+ link: linker
+ };
+
+}
diff --git a/ui/src/app/rulechain/rulenode-fieldset.tpl.html b/ui/src/app/rulechain/rulenode-fieldset.tpl.html
new file mode 100644
index 0000000..12ff320
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-fieldset.tpl.html
@@ -0,0 +1,64 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-button ng-click="onDeleteRuleNode({event: $event})"
+ ng-show="!isEdit && !isReadOnly"
+ class="md-raised md-primary">{{ 'rulenode.delete' | translate }}</md-button>
+
+<md-content class="md-padding tb-rulenode" layout="column">
+ <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
+ <section ng-if="ruleNode.component.type != types.ruleNodeType.RULE_CHAIN.value">
+ <section layout="column" layout-gt-sm="row">
+ <md-input-container flex class="md-block">
+ <label translate>rulenode.name</label>
+ <input required name="name" ng-model="ruleNode.name">
+ <div ng-messages="theForm.name.$error">
+ <div translate ng-message="required">rulenode.name-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <md-checkbox ng-disabled="$root.loading || !isEdit" aria-label="{{ 'rulenode.debug-mode' | translate }}"
+ ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }}
+ </md-checkbox>
+ </md-input-container>
+ </section>
+ <tb-rule-node-config ng-model="ruleNode.configuration"
+ rule-node-id="ruleNode.ruleNodeId.id"
+ ng-required="true"
+ node-definition="ruleNode.component.configurationDescriptor.nodeDefinition"
+ ng-readonly="$root.loading || !isEdit || isReadOnly">
+ </tb-rule-node-config>
+ <md-input-container class="md-block">
+ <label translate>rulenode.description</label>
+ <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
+ </md-input-container>
+ </section>
+ <section ng-if="ruleNode.component.type == types.ruleNodeType.RULE_CHAIN.value">
+ <tb-entity-autocomplete the-form="theForm"
+ ng-disabled="$root.loading || !isEdit || isReadOnly"
+ tb-required="true"
+ exclude-entity-ids="[ruleChainId]"
+ entity-type="types.entityType.rulechain"
+ ng-model="params.targetRuleChainId">
+ </tb-entity-autocomplete>
+ <md-input-container class="md-block">
+ <label translate>rulenode.description</label>
+ <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
+ </md-input-container>
+ </section>
+ </fieldset>
+</md-content>
diff --git a/ui/src/app/rulechain/script/node-script-test.controller.js b/ui/src/app/rulechain/script/node-script-test.controller.js
new file mode 100644
index 0000000..487d11f
--- /dev/null
+++ b/ui/src/app/rulechain/script/node-script-test.controller.js
@@ -0,0 +1,177 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './node-script-test.scss';
+
+import Split from 'split.js';
+
+import beautify from 'js-beautify';
+
+const js_beautify = beautify.js;
+
+/*@ngInject*/
+export default function NodeScriptTestController($scope, $mdDialog, $window, $document, $timeout,
+ $q, $mdUtil, $translate, toast, types, utils,
+ ruleChainService, onShowingCallback, msg, msgType, metadata,
+ functionTitle, inputParams) {
+
+ var vm = this;
+
+ vm.types = types;
+ vm.functionTitle = functionTitle;
+ vm.inputParams = inputParams;
+ vm.inputParams.msg = js_beautify(angular.toJson(msg), {indent_size: 4});
+ vm.inputParams.metadata = metadata;
+ vm.inputParams.msgType = msgType;
+
+ vm.output = '';
+
+ vm.test = test;
+ vm.save = save;
+ vm.cancel = cancel;
+
+ $scope.$watch('theForm.metadataForm.$dirty', (newVal) => {
+ if (newVal) {
+ toast.hide();
+ }
+ });
+
+ onShowingCallback.onShowed = () => {
+ vm.nodeScriptTestDialogElement = angular.element('.tb-node-script-test-dialog');
+ var w = vm.nodeScriptTestDialogElement.width();
+ if (w > 0) {
+ initSplitLayout();
+ } else {
+ $scope.$watch(
+ function () {
+ return vm.nodeScriptTestDialogElement[0].offsetWidth || parseInt(vm.nodeScriptTestDialogElement.css('width'), 10);
+ },
+ function (newSize) {
+ if (newSize > 0) {
+ initSplitLayout();
+ }
+ }
+ );
+ }
+ };
+
+ function onDividerDrag() {
+ $scope.$broadcast('update-ace-editor-size');
+ }
+
+ function initSplitLayout() {
+ if (!vm.layoutInited) {
+ Split([angular.element('#top_panel', vm.nodeScriptTestDialogElement)[0], angular.element('#bottom_panel', vm.nodeScriptTestDialogElement)[0]], {
+ sizes: [35, 65],
+ gutterSize: 8,
+ cursor: 'row-resize',
+ direction: 'vertical',
+ onDrag: function () {
+ onDividerDrag()
+ }
+ });
+
+ Split([angular.element('#top_left_panel', vm.nodeScriptTestDialogElement)[0], angular.element('#top_right_panel', vm.nodeScriptTestDialogElement)[0]], {
+ sizes: [50, 50],
+ gutterSize: 8,
+ cursor: 'col-resize',
+ onDrag: function () {
+ onDividerDrag()
+ }
+ });
+
+ Split([angular.element('#bottom_left_panel', vm.nodeScriptTestDialogElement)[0], angular.element('#bottom_right_panel', vm.nodeScriptTestDialogElement)[0]], {
+ sizes: [50, 50],
+ gutterSize: 8,
+ cursor: 'col-resize',
+ onDrag: function () {
+ onDividerDrag()
+ }
+ });
+
+ onDividerDrag();
+
+ $scope.$applyAsync(function () {
+ vm.layoutInited = true;
+ var w = angular.element($window);
+ $timeout(function () {
+ w.triggerHandler('resize')
+ });
+ });
+
+ }
+ }
+
+ function test() {
+ testNodeScript().then(
+ (output) => {
+ vm.output = js_beautify(output, {indent_size: 4});
+ }
+ );
+ }
+
+ function checkInputParamErrors() {
+ $scope.theForm.metadataForm.$setPristine();
+ $scope.$broadcast('form-submit', 'validatePayload');
+ if (!$scope.theForm.payloadForm.$valid) {
+ return false;
+ } else if (!$scope.theForm.metadataForm.$valid) {
+ showMetadataError($translate.instant('rulenode.metadata-required'));
+ return false;
+ }
+ return true;
+ }
+
+ function showMetadataError(error) {
+ var toastParent = angular.element('#metadata-panel', vm.nodeScriptTestDialogElement);
+ toast.showError(error, toastParent, 'bottom left');
+ }
+
+ function testNodeScript() {
+ var deferred = $q.defer();
+ if (checkInputParamErrors()) {
+ $mdUtil.nextTick(() => {
+ ruleChainService.testScript(vm.inputParams).then(
+ (result) => {
+ if (result.error) {
+ toast.showError(result.error);
+ deferred.reject();
+ } else {
+ deferred.resolve(result.output);
+ }
+ },
+ () => {
+ deferred.reject();
+ }
+ );
+ });
+ } else {
+ deferred.reject();
+ }
+ return deferred.promise;
+ }
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function save() {
+ testNodeScript().then(() => {
+ $scope.theForm.funcBodyForm.$setPristine();
+ $mdDialog.hide(vm.inputParams.script);
+ });
+ }
+}
ui/src/app/rulechain/script/node-script-test.scss 115(+115 -0)
diff --git a/ui/src/app/rulechain/script/node-script-test.scss b/ui/src/app/rulechain/script/node-script-test.scss
new file mode 100644
index 0000000..a75ae59
--- /dev/null
+++ b/ui/src/app/rulechain/script/node-script-test.scss
@@ -0,0 +1,115 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import '../../../scss/constants';
+@import "~compass-sass-mixins/lib/compass";
+
+md-dialog.tb-node-script-test-dialog {
+ &.md-dialog-fullscreen {
+ min-height: 100%;
+ min-width: 100%;
+ max-height: 100%;
+ max-width: 100%;
+ width: 100%;
+ height: 100%;
+ border-radius: 0;
+ }
+
+ .tb-split {
+ @include box-sizing(border-box);
+ overflow-y: auto;
+ overflow-x: hidden;
+ }
+
+ .ace_editor {
+ font-size: 14px !important;
+ }
+
+ .tb-content {
+ border: 1px solid #C0C0C0;
+ padding-top: 5px;
+ padding-left: 5px;
+ }
+
+ .gutter {
+ background-color: #eeeeee;
+
+ background-repeat: no-repeat;
+ background-position: 50%;
+ }
+
+ .gutter.gutter-horizontal {
+ cursor: col-resize;
+ background-image: url('../../../../node_modules/split.js/grips/vertical.png');
+ }
+
+ .gutter.gutter-vertical {
+ cursor: row-resize;
+ background-image: url('../../../../node_modules/split.js/grips/horizontal.png');
+ }
+
+ .tb-split.tb-split-horizontal, .gutter.gutter-horizontal {
+ height: 100%;
+ float: left;
+ }
+
+ .tb-split.tb-split-vertical {
+ display: flex;
+ .tb-split.tb-content {
+ height: 100%;
+ }
+ }
+
+ div.tb-editor-area-title-panel {
+ position: absolute;
+ font-size: 0.800rem;
+ font-weight: 500;
+ top: 13px;
+ right: 40px;
+ z-index: 5;
+ &.tb-js-function {
+ right: 80px;
+ }
+ label {
+ color: #00acc1;
+ background: rgba(220, 220, 220, 0.35);
+ border-radius: 5px;
+ padding: 4px;
+ text-transform: uppercase;
+ }
+ .md-button {
+ color: #7B7B7B;
+ min-width: 32px;
+ min-height: 15px;
+ line-height: 15px;
+ font-size: 0.800rem;
+ margin: 0;
+ padding: 4px;
+ background: rgba(220, 220, 220, 0.35);
+ }
+ }
+
+ .tb-resize-container {
+ overflow-y: auto;
+ height: 100%;
+ width: 100%;
+ position: relative;
+
+ .ace_editor {
+ height: 100%;
+ }
+ }
+
+}
diff --git a/ui/src/app/rulechain/script/node-script-test.service.js b/ui/src/app/rulechain/script/node-script-test.service.js
new file mode 100644
index 0000000..81d81c1
--- /dev/null
+++ b/ui/src/app/rulechain/script/node-script-test.service.js
@@ -0,0 +1,140 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import nodeScriptTestTemplate from './node-script-test.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function NodeScriptTest($q, $mdDialog, $document, ruleChainService) {
+
+ var service = {
+ testNodeScript: testNodeScript
+ };
+
+ return service;
+
+ function testNodeScript($event, script, scriptType, functionTitle, functionName, argNames, ruleNodeId) {
+ var deferred = $q.defer();
+ if ($event) {
+ $event.stopPropagation();
+ }
+
+ var msg, metadata, msgType;
+ if (ruleNodeId) {
+ ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).then(
+ (debugIn) => {
+ if (debugIn) {
+ if (debugIn.data) {
+ msg = angular.fromJson(debugIn.data);
+ }
+ if (debugIn.metadata) {
+ metadata = angular.fromJson(debugIn.metadata);
+ }
+ msgType = debugIn.msgType;
+ }
+ openTestScriptDialog($event, script, scriptType, functionTitle,
+ functionName, argNames, msg, metadata, msgType).then(
+ (script) => {
+ deferred.resolve(script);
+ },
+ () => {
+ deferred.reject();
+ }
+ );
+ },
+ () => {
+ deferred.reject();
+ }
+ );
+ } else {
+ openTestScriptDialog($event, script, scriptType, functionTitle,
+ functionName, argNames).then(
+ (script) => {
+ deferred.resolve(script);
+ },
+ () => {
+ deferred.reject();
+ }
+ );
+ }
+ return deferred.promise;
+ }
+
+ function openTestScriptDialog($event, script, scriptType, functionTitle, functionName, argNames, msg, metadata, msgType) {
+ var deferred = $q.defer();
+ if (!msg) {
+ msg = {
+ temperature: 22.4,
+ humidity: 78
+ };
+ }
+ if (!metadata) {
+ metadata = {
+ deviceType: "default",
+ deviceName: "Test Device",
+ ts: new Date().getTime() + ""
+ };
+ }
+ if (!msgType) {
+ msgType = "POST_TELEMETRY_REQUEST";
+ }
+
+ var onShowingCallback = {
+ onShowed: () => {
+ }
+ };
+
+ var inputParams = {
+ script: script,
+ scriptType: scriptType,
+ functionName: functionName,
+ argNames: argNames
+ };
+
+ $mdDialog.show({
+ controller: 'NodeScriptTestController',
+ controllerAs: 'vm',
+ templateUrl: nodeScriptTestTemplate,
+ parent: angular.element($document[0].body),
+ locals: {
+ msg: msg,
+ metadata: metadata,
+ msgType: msgType,
+ functionTitle: functionTitle,
+ inputParams: inputParams,
+ onShowingCallback: onShowingCallback
+ },
+ fullscreen: true,
+ skipHide: true,
+ targetEvent: $event,
+ onComplete: () => {
+ onShowingCallback.onShowed();
+ }
+ }).then(
+ (script) => {
+ deferred.resolve(script);
+ },
+ () => {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+}
\ No newline at end of file
diff --git a/ui/src/app/rulechain/script/node-script-test.tpl.html b/ui/src/app/rulechain/script/node-script-test.tpl.html
new file mode 100644
index 0000000..0ce57e8
--- /dev/null
+++ b/ui/src/app/rulechain/script/node-script-test.tpl.html
@@ -0,0 +1,119 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog class="tb-node-script-test-dialog"
+ aria-label="{{ 'rulenode.test-script-function' | translate }}" style="width: 800px;">
+ <form flex name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2>{{ 'rulenode.test-script-function' | translate }}</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-dialog-content flex style="position: relative;">
+ <div class="tb-absolute-fill">
+ <div id="top_panel" class="tb-split tb-split-vertical">
+ <div id="top_left_panel" class="tb-split tb-content">
+ <div class="tb-resize-container">
+ <div class="tb-editor-area-title-panel">
+ <label translate>rulenode.message</label>
+ </div>
+ <ng-form name="payloadForm">
+ <div layout="column" style="height: 100%;">
+ <div layout="row">
+ <md-input-container class="md-block" style="margin-bottom: 0px; min-width: 300px;">
+ <label translate>rulenode.message-type</label>
+ <input required name="msgType" ng-model="vm.inputParams.msgType">
+ <div ng-messages="payloadForm.msgType.$error">
+ <div translate ng-message="required">rulenode.message-type-required</div>
+ </div>
+ </md-input-container>
+ </div>
+ <tb-json-content flex
+ ng-model="vm.inputParams.msg"
+ label="{{ 'rulenode.message' | translate }}"
+ content-type="vm.types.contentType.JSON.value"
+ validate-content="true"
+ validation-trigger-arg="validatePayload"
+ fill-height="true">
+ </tb-json-content>
+ </div>
+ </ng-form>
+ </div>
+ </div>
+ <div id="top_right_panel" class="tb-split tb-content">
+ <div class="tb-resize-container" id="metadata-panel">
+ <div class="tb-editor-area-title-panel">
+ <label translate>rulenode.metadata</label>
+ </div>
+ <ng-form name="metadataForm">
+ <tb-key-val-map title-text="rulenode.metadata" ng-disabled="$root.loading"
+ key-val-map="vm.inputParams.metadata"></tb-key-val-map>
+ </ng-form>
+ </div>
+ </div>
+ </div>
+ <div id="bottom_panel" class="tb-split tb-split-vertical">
+ <div id="bottom_left_panel" class="tb-split tb-content">
+ <div class="tb-resize-container">
+ <div class="tb-editor-area-title-panel tb-js-function">
+ <label>{{ vm.functionTitle }}</label>
+ </div>
+ <ng-form name="funcBodyForm">
+ <tb-js-func id="funcBodyInput" ng-model="vm.inputParams.script"
+ function-name="{{vm.inputParams.functionName}}"
+ function-args="{{ vm.inputParams.argNames }}"
+ validation-args="{{ [[vm.inputParams.msg, vm.inputParams.metadata, vm.inputParams.msgType]] }}"
+ validation-trigger-arg="validateFuncBody"
+ result-type="object"
+ fill-height="true">
+ </tb-js-func>
+ </ng-form>
+ </div>
+ </div>
+ <div id="bottom_right_panel" class="tb-split tb-content">
+ <div class="tb-resize-container">
+ <div class="tb-editor-area-title-panel">
+ <label translate>rulenode.output</label>
+ </div>
+ <tb-json-content ng-model="vm.output"
+ label="{{ 'rulenode.output' | translate }}"
+ content-type="vm.types.contentType.JSON.value"
+ validate-content="false"
+ ng-readonly="true"
+ fill-height="true">
+ </tb-json-content>
+ </div>
+ </div>
+ </div>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <md-button ng-disabled="$root.loading" ng-click="vm.test()" class="md-raised md-primary">
+ {{ 'rulenode.test' | translate }}
+ </md-button>
+ <span flex></span>
+ <md-button ng-disabled="$root.loading || theForm.funcBodyForm.$invalid || !theForm.funcBodyForm.$dirty" type="submit" class="md-raised md-primary">
+ {{ 'action.save' | translate }}
+ </md-button>
+ <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/services/item-buffer.service.js 83(+81 -2)
diff --git a/ui/src/app/services/item-buffer.service.js b/ui/src/app/services/item-buffer.service.js
index 9fce811..a9fe348 100644
--- a/ui/src/app/services/item-buffer.service.js
+++ b/ui/src/app/services/item-buffer.service.js
@@ -24,10 +24,11 @@ export default angular.module('thingsboard.itembuffer', [angularStorage])
.name;
/*@ngInject*/
-function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
+function ItemBuffer($q, bufferStore, types, utils, dashboardUtils, ruleChainService) {
const WIDGET_ITEM = "widget_item";
const WIDGET_REFERENCE = "widget_reference";
+ const RULE_NODES = "rule_nodes";
var service = {
prepareWidgetItem: prepareWidgetItem,
@@ -37,7 +38,10 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
canPasteWidgetReference: canPasteWidgetReference,
pasteWidget: pasteWidget,
pasteWidgetReference: pasteWidgetReference,
- addWidgetToDashboard: addWidgetToDashboard
+ addWidgetToDashboard: addWidgetToDashboard,
+ copyRuleNodes: copyRuleNodes,
+ hasRuleNodes: hasRuleNodes,
+ pasteRuleNodes: pasteRuleNodes
}
return service;
@@ -151,6 +155,81 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
};
}
+ function copyRuleNodes(nodes, connections) {
+ var ruleNodes = {
+ nodes: [],
+ connections: []
+ };
+ var top = -1, left = -1, bottom = -1, right = -1;
+ for (var i=0;i<nodes.length;i++) {
+ var origNode = nodes[i];
+ var node = {
+ additionalInfo: origNode.additionalInfo,
+ configuration: origNode.configuration,
+ debugMode: origNode.debugMode,
+ x: origNode.x,
+ y: origNode.y,
+ name: origNode.name,
+ componentClazz: origNode.component.clazz,
+ };
+ if (origNode.targetRuleChainId) {
+ node.targetRuleChainId = origNode.targetRuleChainId;
+ }
+ if (origNode.error) {
+ node.error = origNode.error;
+ }
+ ruleNodes.nodes.push(node);
+ if (i==0) {
+ top = node.y;
+ left = node.x;
+ bottom = node.y + 50;
+ right = node.x + 170;
+ } else {
+ top = Math.min(top, node.y);
+ left = Math.min(left, node.x);
+ bottom = Math.max(bottom, node.y + 50);
+ right = Math.max(right, node.x + 170);
+ }
+ }
+ ruleNodes.originX = left + (right-left)/2;
+ ruleNodes.originY = top + (bottom-top)/2;
+ for (i=0;i<connections.length;i++) {
+ var connection = connections[i];
+ ruleNodes.connections.push(connection);
+ }
+ bufferStore.set(RULE_NODES, angular.toJson(ruleNodes));
+ }
+
+ function hasRuleNodes() {
+ return bufferStore.get(RULE_NODES);
+ }
+
+ function pasteRuleNodes(x, y) {
+ var ruleNodesJson = bufferStore.get(RULE_NODES);
+ if (ruleNodesJson) {
+ var ruleNodes = angular.fromJson(ruleNodesJson);
+ var deltaX = x - ruleNodes.originX;
+ var deltaY = y - ruleNodes.originY;
+ for (var i=0;i<ruleNodes.nodes.length;i++) {
+ var node = ruleNodes.nodes[i];
+ var component = ruleChainService.getRuleNodeComponentByClazz(node.componentClazz);
+ if (component) {
+ delete node.componentClazz;
+ node.component = component;
+ node.nodeClass = types.ruleNodeType[component.type].nodeClass;
+ node.icon = types.ruleNodeType[component.type].icon;
+ node.connectors = [];
+ node.x = Math.round(node.x + deltaX);
+ node.y = Math.round(node.y + deltaY);
+ } else {
+ return null;
+ }
+ }
+ return ruleNodes;
+ }
+ return null;
+ }
+
function copyWidget(dashboard, sourceState, sourceLayout, widget) {
var widgetItem = prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem));
ui/src/app/services/menu.service.js 48(+5 -43)
diff --git a/ui/src/app/services/menu.service.js b/ui/src/app/services/menu.service.js
index 9dbddd9..1c33d1f 100644
--- a/ui/src/app/services/menu.service.js
+++ b/ui/src/app/services/menu.service.js
@@ -67,18 +67,6 @@ function Menu(userService, $state, $rootScope) {
icon: 'home'
},
{
- name: 'plugin.plugins',
- type: 'link',
- state: 'home.plugins',
- icon: 'extension'
- },
- {
- name: 'rule.rules',
- type: 'link',
- state: 'home.rules',
- icon: 'settings_ethernet'
- },
- {
name: 'tenant.tenants',
type: 'link',
state: 'home.tenants',
@@ -113,21 +101,6 @@ function Menu(userService, $state, $rootScope) {
}];
homeSections =
[{
- name: 'rule-plugin.management',
- places: [
- {
- name: 'plugin.plugins',
- icon: 'extension',
- state: 'home.plugins'
- },
- {
- name: 'rule.rules',
- icon: 'settings_ethernet',
- state: 'home.rules'
- }
- ]
- },
- {
name: 'tenant.management',
places: [
{
@@ -171,15 +144,9 @@ function Menu(userService, $state, $rootScope) {
icon: 'home'
},
{
- name: 'plugin.plugins',
+ name: 'rulechain.rulechains',
type: 'link',
- state: 'home.plugins',
- icon: 'extension'
- },
- {
- name: 'rule.rules',
- type: 'link',
- state: 'home.rules',
+ state: 'home.ruleChains',
icon: 'settings_ethernet'
},
{
@@ -221,17 +188,12 @@ function Menu(userService, $state, $rootScope) {
homeSections =
[{
- name: 'rule-plugin.management',
+ name: 'rulechain.management',
places: [
{
- name: 'plugin.plugins',
- icon: 'extension',
- state: 'home.plugins'
- },
- {
- name: 'rule.rules',
+ name: 'rulechain.rulechains',
icon: 'settings_ethernet',
- state: 'home.rules'
+ state: 'home.ruleChains'
}
]
},
ui/src/scss/main.scss 34(+34 -0)
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 93ff320..8fed892 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -203,6 +203,12 @@ md-sidenav {
* THINGSBOARD SPECIFIC
***********************/
+$swift-ease-out-duration: 0.4s !default;
+$swift-ease-out-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
+
+$input-label-float-offset: 6px !default;
+$input-label-float-scale: 0.75 !default;
+
label {
&.tb-title {
pointer-events: none;
@@ -213,6 +219,18 @@ label {
&.no-padding {
padding-bottom: 0px;
}
+ &.tb-required:after {
+ content: ' *';
+ font-size: 13px;
+ vertical-align: top;
+ color: rgba(0,0,0,0.54);
+ }
+ &.tb-error {
+ color: rgb(221,44,0);
+ &.tb-required:after {
+ color: rgb(221,44,0);
+ }
+ }
}
}
@@ -249,6 +267,22 @@ div {
}
}
+.tb-hint {
+ font-size: 12px;
+ line-height: 14px;
+ transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2);
+ color: grey;
+ padding-bottom: 15px;
+ &.ng-hide, &.ng-enter, &.ng-leave.ng-leave-active {
+ bottom: 26px;
+ opacity: 0;
+ }
+ &.ng-leave, &.ng-enter.ng-enter-active {
+ bottom: 7px;
+ opacity: 1;
+ }
+}
+
.md-caption {
&.tb-required:after {
content: ' *';