thingsboard-developers
Changes
.gitignore 32(+32 -0)
application/.gitignore 1(+1 -0)
application/build.gradle 134(+134 -0)
application/pom.xml 417(+417 -0)
application/src/main/conf/logback.xml 44(+44 -0)
application/src/main/conf/thingsboard.conf 19(+19 -0)
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java 366(+366 -0)
application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java 28(+28 -0)
application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java 233(+233 -0)
application/src/main/java/org/thingsboard/server/actors/plugin/PluginCallbackMessage.java 53(+53 -0)
application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java 306(+306 -0)
application/src/main/java/org/thingsboard/server/actors/plugin/RuleToPluginMsgWrapper.java 66(+66 -0)
application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java 111(+111 -0)
application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java 170(+170 -0)
application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionCreateRequestMsg.java 35(+35 -0)
application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionDisconnectedMsg.java 29(+29 -0)
application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingContext.java 104(+104 -0)
application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingMetaData.java 42(+42 -0)
application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java 339(+339 -0)
application/src/main/java/org/thingsboard/server/actors/rule/RuleContextAwareMsgProcessor.java 33(+33 -0)
application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java 234(+234 -0)
application/src/main/java/org/thingsboard/server/actors/service/WebSocketMsgProcessor.java 24(+24 -0)
application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java 119(+119 -0)
application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java 154(+154 -0)
application/src/main/java/org/thingsboard/server/actors/session/SessionTerminationMsg.java 26(+26 -0)
application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java 135(+135 -0)
application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java 55(+55 -0)
application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java 41(+41 -0)
application/src/main/java/org/thingsboard/server/actors/shared/plugin/TenantPluginManager.java 41(+41 -0)
application/src/main/java/org/thingsboard/server/actors/shared/rule/SystemRuleManager.java 35(+35 -0)
application/src/main/java/org/thingsboard/server/actors/shared/rule/TenantRuleManager.java 34(+34 -0)
application/src/main/java/org/thingsboard/server/service/cluster/discovery/CurrentServerInstanceService.java 55(+55 -0)
application/src/main/java/org/thingsboard/server/service/cluster/discovery/DiscoveryService.java 37(+37 -0)
application/src/main/java/org/thingsboard/server/service/cluster/discovery/DiscoveryServiceListener.java 28(+28 -0)
application/src/main/java/org/thingsboard/server/service/cluster/discovery/DummyDiscoveryService.java 75(+75 -0)
application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstance.java 52(+52 -0)
application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstanceService.java 24(+24 -0)
application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java 207(+207 -0)
application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java 33(+33 -0)
application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java 142(+142 -0)
application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterGrpcService.java 273(+273 -0)
application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterRpcService.java 53(+53 -0)
application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSessionListener.java 45(+45 -0)
application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcSessionCreationFuture.java 63(+63 -0)
application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java 190(+190 -0)
application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java 35(+35 -0)
application/src/main/proto/cluster.proto 97(+97 -0)
application/src/main/proto/discovery.proto 26(+26 -0)
application/src/main/resources/actor-system.conf 163(+163 -0)
application/src/main/resources/logback.xml 35(+35 -0)
application/src/main/resources/templates/test.vm 112(+112 -0)
application/src/main/resources/thingsboard.yml 176(+176 -0)
application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java 375(+375 -0)
application/src/test/java/org/thingsboard/server/controller/ComponentDescriptorControllerTest.java 106(+106 -0)
application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java 366(+366 -0)
application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java 431(+431 -0)
application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java 316(+316 -0)
application/src/test/java/org/thingsboard/server/controller/WidgetTypeControllerTest.java 233(+233 -0)
application/src/test/resources/logback.xml 21(+21 -0)
common/data/pom.xml 76(+76 -0)
common/data/src/main/java/org/thingsboard/server/common/data/id/ComponentDescriptorId.java 31(+31 -0)
common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java 104(+104 -0)
common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java 87(+87 -0)
common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java 25(+25 -0)
common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleState.java 23(+23 -0)
common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java 175(+175 -0)
common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java 128(+128 -0)
common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsFilter.java 27(+27 -0)
common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java 22(+22 -0)
common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceTokenCredentials.java 42(+42 -0)
common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java 156(+156 -0)
common/message/pom.xml 76(+76 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/aware/CustomerAwareMsg.java 24(+24 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/aware/SessionAwareMsg.java 24(+24 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ClusterEventMsg.java 29(+29 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ServerAddress.java 46(+46 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ToAllNodesMsg.java 24(+24 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesSubscribeMsg.java 29(+29 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUnsubscribeMsg.java 29(+29 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUpdateNotification.java 47(+47 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicCommandAckResponse.java 44(+44 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java 52(+52 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesResponse.java 39(+39 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicResponseMsg.java 79(+79 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicStatusCodeResponse.java 41(+41 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicTelemetryUploadRequest.java 65(+65 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicToDeviceSessionActorMsg.java 47(+47 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicUpdateAttributesRequest.java 63(+63 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesRequest.java 28(+28 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesResponse.java 22(+22 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcUnsubscribeMsg.java 29(+29 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineErrorMsg.java 61(+61 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/StatusCodeResponse.java 20(+20 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/TelemetryUploadRequest.java 29(+29 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcRequestMsg.java 41(+41 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcResponseMsg.java 35(+35 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceSessionActorMsg.java 29(+29 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java 36(+36 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcResponseMsg.java 41(+41 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/UpdateAttributesRequest.java 28(+28 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/device/BasicToDeviceActorMsg.java 101(+101 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/device/ToDeviceActorMsg.java 40(+40 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/kv/BasicAttributeKVMsg.java 52(+52 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java 65(+65 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/AdaptorToSessionActorMsg.java 22(+22 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicAdaptorToSessionActorMsg.java 32(+32 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicSessionActorToAdaptorMsg.java 34(+34 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicSessionMsg.java 44(+44 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicToDeviceActorSessionMsg.java 76(+76 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java 41(+41 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/ProcessingTimeoutException.java 22(+22 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/SessionAuthException.java 26(+26 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/SessionException.java 35(+35 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceMsg.java 24(+24 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceRequestMsg.java 25(+25 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionActorToAdaptorMsg.java 22(+22 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionContext.java 36(+36 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionCtrlMsg.java 22(+22 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceActorSessionMsg.java 27(+27 -0)
common/pom.xml 43(+43 -0)
common/transport/pom.xml 79(+79 -0)
common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java 34(+34 -0)
common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java 199(+199 -0)
common/transport/src/main/java/org/thingsboard/server/common/transport/auth/DeviceAuthResult.java 58(+58 -0)
common/transport/src/main/java/org/thingsboard/server/common/transport/auth/DeviceAuthService.java 30(+30 -0)
common/transport/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java 62(+62 -0)
common/transport/src/main/java/org/thingsboard/server/common/transport/SessionMsgProcessor.java 25(+25 -0)
common/transport/src/main/java/org/thingsboard/server/common/transport/TransportAdaptor.java 32(+32 -0)
extensions-api/pom.xml 95(+95 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/ConfigurableComponent.java 25(+25 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/EmptyComponentConfiguration.java 22(+22 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Processor.java 40(+40 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/Configurable.java 26(+26 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/Configuration.java 24(+24 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/ConfigurationValidationException.java 26(+26 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributes.java 68(+68 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributesEventNotificationMsg.java 51(+51 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/ToDeviceActorNotificationMsg.java 28(+28 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/AbstractPlugin.java 88(+88 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRestMsgHandler.java 72(+72 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRpcMsgHandler.java 30(+30 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRuleMsgHandler.java 63(+63 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java 103(+103 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RestMsgHandler.java 28(+28 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RpcMsgHandler.java 28(+28 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RuleMsgHandler.java 31(+31 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/WebsocketMsgHandler.java 28(+28 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractPluginToRuleMsg.java 63(+63 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractRuleToPluginMsg.java 72(+72 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/FromDeviceRpcResponse.java 44(+44 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/GetAttributesRequestRuleToPluginMsg.java 33(+33 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/GetRequestRuleToPluginMsg.java 31(+31 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/PluginToRuleMsg.java 64(+64 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ResponsePluginToRuleMsg.java 32(+32 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcError.java 23(+23 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcRequestRuleToPluginMsg.java 31(+31 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcResponsePluginToRuleMsg.java 32(+32 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RuleToPluginMsg.java 61(+61 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TelemetryUploadRequestRuleToPluginMsg.java 31(+31 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutIntMsg.java 27(+27 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutMsg.java 27(+27 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutUUIDMsg.java 29(+29 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java 37(+37 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequestBody.java 29(+29 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequestPluginMsg.java 62(+62 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToPluginActorMsg.java 25(+25 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToPluginRpcResponseDeviceMsg.java 31(+31 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/UpdateAttributesRequestRuleToPluginMsg.java 31(+31 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginAction.java 37(+37 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java 70(+70 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginCallback.java 26(+26 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginConstants.java 23(+23 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java 106(+106 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginException.java 26(+26 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginInitializationException.java 29(+29 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/BasicPluginRestMsg.java 63(+63 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/PluginRestMsg.java 38(+38 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/RestRequest.java 87(+87 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rpc/PluginRpcMsg.java 46(+46 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rpc/RpcMsg.java 35(+35 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/BasicPluginWebsocketSessionRef.java 111(+111 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/AbstractPluginWebSocketMsg.java 64(+64 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/BinaryPluginWebSocketMsg.java 29(+29 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/EmptyPluginWebsocketMsg.java 30(+30 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PingPluginWebsocketMsg.java 27(+27 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PluginWebsocketMsg.java 31(+31 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PongPluginWebsocketMsg.java 27(+27 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/SessionEventPluginWebSocketMsg.java 29(+29 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/TextPluginWebSocketMsg.java 28(+28 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/PluginWebsocketSessionRef.java 45(+45 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/SessionEvent.java 53(+53 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/WsSessionMetaData.java 50(+50 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleContext.java 36(+36 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleException.java 30(+30 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleInitializationException.java 29(+29 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleLifecycleComponent.java 29(+29 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessingMetaData.java 43(+43 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessor.java 27(+27 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/SimpleRuleLifecycleComponent.java 41(+41 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/ToRuleActorMsg.java 25(+25 -0)
extensions-core/pom.xml 131(+131 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailAction.java 109(+109 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailActionConfiguration.java 34(+34 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailActionMsg.java 36(+36 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailRuleToPluginActionMsg.java 35(+35 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/rpc/RpcPluginAction.java 67(+67 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/telemetry/TelemetryPluginAction.java 73(+73 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/template/AbstractTemplatePluginAction.java 84(+84 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/template/TemplateActionConfiguration.java 25(+25 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/BasicJsFilter.java 92(+92 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilter.java 93(+93 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterConfiguration.java 27(+27 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceTelemetryFilter.java 60(+60 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/JsFilterConfiguration.java 27(+27 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java 54(+54 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilterConfiguration.java 33(+33 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java 67(+67 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilterConfiguration.java 28(+28 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/NashornJsEvaluator.java 63(+63 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/KeyValuePluginProperties.java 27(+27 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java 121(+121 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPluginConfiguration.java 33(+33 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPlugin.java 69(+69 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPluginConfiguration.java 30(+30 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java 228(+228 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/PendingRpcRequestMetadata.java 37(+37 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/cmd/RpcRequest.java 28(+28 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java 127(+127 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/LocalRequestMetaData.java 30(+30 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java 69(+69 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPlugin.java 77(+77 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPluginConfiguration.java 26(+26 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/AttributeData.java 48(+48 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java 36(+36 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java 70(+70 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java 70(+70 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmd.java 29(+29 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmdsWrapper.java 57(+57 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java 46(+46 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java 201(+201 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java 185(+185 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java 130(+130 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java 314(+314 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java 73(+73 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionErrorCode.java 50(+50 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java 47(+47 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionType.java 23(+23 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java 97(+97 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java 284(+284 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TelemetryStoragePlugin.java 106(+106 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TsData.java 42(+42 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java 92(+92 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePluginConfiguration.java 26(+26 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessor.java 84(+84 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessorConfiguration.java 29(+29 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/utils/VelocityUtils.java 96(+96 -0)
extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java 126(+126 -0)
LICENSE 201(+201 -0)
license-header-template.txt 13(+13 -0)
pom.xml 710(+710 -0)
README.md 29(+29 -0)
tools/pom.xml 83(+83 -0)
tools/src/main/java/MqttStressTestTool.java 88(+88 -0)
tools/src/main/java/RestClient.java 72(+72 -0)
tools/src/main/java/ResultAccumulator.java 85(+85 -0)
tools/src/main/shell/keygen.sh 57(+57 -0)
tools/src/main/shell/securemqttclient.py 55(+55 -0)
tools/src/main/shell/simplemqttclient.py 50(+50 -0)
transport/coap/pom.xml 84(+84 -0)
transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java 25(+25 -0)
transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java 265(+265 -0)
transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DeviceEmulator.java 179(+179 -0)
transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java 222(+222 -0)
transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java 96(+96 -0)
transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapExchangeObserverProxy.java 38(+38 -0)
transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java 143(+143 -0)
transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionId.java 77(+77 -0)
transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTestConfiguration.java 35(+35 -0)
transport/http/pom.xml 81(+81 -0)
transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java 195(+195 -0)
transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionCtx.java 162(+162 -0)
transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionId.java 37(+37 -0)
transport/mqtt/pom.xml 90(+90 -0)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java 238(+238 -0)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java 26(+26 -0)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java 91(+91 -0)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java 260(+260 -0)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java 69(+69 -0)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java 108(+108 -0)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java 116(+116 -0)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionId.java 56(+56 -0)
transport/pom.xml 50(+50 -0)
Details
.gitignore 32(+32 -0)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e14c866
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,32 @@
+output/**
+*.class
+*~
+*.iml
+*/.idea/**
+.idea/**
+.idea
+*.log
+*.log.[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]
+*/.classpath
+.classpath
+*/.project
+.project
+.cache/**
+target/
+build/
+tmp_deb_control/
+tmp_rpm_control/
+tmp_sh/
+.gwt/
+.settings/
+/bin
+bin/
+**/dependency-reduced-pom.xml
+pom.xml.versionsBackup
+.DS_Store
+**/.gradle
+**/local.properties
+**/build
+**/target
+**/Californium.properties
+**/.env
application/.gitignore 1(+1 -0)
diff --git a/application/.gitignore b/application/.gitignore
new file mode 100644
index 0000000..08eb0a0
--- /dev/null
+++ b/application/.gitignore
@@ -0,0 +1 @@
+!bin/
\ No newline at end of file
application/build.gradle 134(+134 -0)
diff --git a/application/build.gradle b/application/build.gradle
new file mode 100644
index 0000000..833cf4c
--- /dev/null
+++ b/application/build.gradle
@@ -0,0 +1,134 @@
+/**
+ * Copyright © 2016 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.
+ */
+
+buildscript {
+ ext {
+ osPackageVersion = "3.8.0"
+ }
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath("com.netflix.nebula:gradle-ospackage-plugin:${osPackageVersion}")
+ }
+}
+
+apply plugin: "nebula.ospackage"
+
+buildDir = projectBuildDir
+version = projectVersion
+distsDirName = "./"
+
+// OS Package plugin configuration
+ospackage {
+ packageName = pkgName
+ version = "${project.version}"
+ release = 1
+ os = LINUX
+ type = BINARY
+
+ into pkgInstallFolder
+
+ user pkgName
+ permissionGroup pkgName
+
+ // Copy the actual .jar file
+ from(mainJar) {
+ // Strip the version from the jar filename
+ rename { String fileName ->
+ fileName.replace("-${project.version}", "")
+ }
+ fileMode 0500
+ into "bin"
+ }
+
+ // Copy the config files
+ from("target/conf") {
+ fileType CONFIG | NOREPLACE
+ fileMode 0754
+ into "conf"
+ }
+
+ // Copy the data files
+ from("target/data") {
+ fileType CONFIG | NOREPLACE
+ fileMode 0754
+ into "data"
+ }
+
+ // Copy the extensions files
+ from("target/extensions") {
+ into "extensions"
+ }
+}
+
+// Configure our RPM build task
+buildRpm {
+
+ arch = NOARCH
+
+ version = projectVersion.replace('-', '')
+ archiveName = "${pkgName}.rpm"
+
+ requires("java-1.8.0")
+
+ preInstall file("${buildDir}/control/rpm/preinst")
+ postInstall file("${buildDir}/control/rpm/postinst")
+ preUninstall file("${buildDir}/control/rpm/prerm")
+ postUninstall file("${buildDir}/control/rpm/postrm")
+
+ user pkgName
+ permissionGroup pkgName
+
+ // Copy the system unit files
+ from("${buildDir}/control/${pkgName}.service") {
+ addParentDirs = false
+ fileMode 0644
+ into "/usr/lib/systemd/system"
+ }
+
+ directory(pkgLogFolder, 0755)
+ link("${pkgInstallFolder}/bin/${pkgName}.yml", "${pkgInstallFolder}/conf/${pkgName}.yml")
+ link("/etc/${pkgName}/conf", "${pkgInstallFolder}/conf")
+}
+
+// Same as the buildRpm task
+buildDeb {
+
+ arch = "all"
+
+ archiveName = "${pkgName}.deb"
+
+ requires("openjdk-8-jre").or("java8-runtime").or("oracle-java8-installer")
+
+ configurationFile("${pkgInstallFolder}/conf/${pkgName}.conf")
+ configurationFile("${pkgInstallFolder}/conf/${pkgName}.yml")
+ configurationFile("${pkgInstallFolder}/conf/logback.xml")
+ configurationFile("${pkgInstallFolder}/conf/actor-system.conf")
+
+ preInstall file("${buildDir}/control/deb/preinst")
+ postInstall file("${buildDir}/control/deb/postinst")
+ preUninstall file("${buildDir}/control/deb/prerm")
+ postUninstall file("${buildDir}/control/deb/postrm")
+
+ user pkgName
+ permissionGroup pkgName
+
+ directory(pkgLogFolder, 0755)
+ link("/etc/init.d/${pkgName}", "${pkgInstallFolder}/bin/${pkgName}.jar")
+ link("${pkgInstallFolder}/bin/${pkgName}.yml", "${pkgInstallFolder}/conf/${pkgName}.yml")
+ link("/etc/${pkgName}/conf", "${pkgInstallFolder}/conf")
+}
application/pom.xml 417(+417 -0)
diff --git a/application/pom.xml b/application/pom.xml
new file mode 100644
index 0000000..d75ece1
--- /dev/null
+++ b/application/pom.xml
@@ -0,0 +1,417 @@
+<!--
+
+ Copyright © 2016 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>0.0.1-SNAPSHOT</version>
+ <artifactId>server</artifactId>
+ </parent>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>application</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Thingsboard Server Application</name>
+ <url>http://thingsboard.org</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <main.dir>${basedir}/..</main.dir>
+ <pkg.name>thingsboard</pkg.name>
+ <pkg.logFolder>/var/log/${pkg.name}</pkg.logFolder>
+ <pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-transport-native-epoll</artifactId>
+ <version>${netty.version}</version>
+ <!-- Explicitly bring in the linux classifier, test may fail on 32-bit linux -->
+ <classifier>linux-x86_64</classifier>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>extensions-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>extensions-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server.common</groupId>
+ <artifactId>transport</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server.transport</groupId>
+ <artifactId>http</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server.transport</groupId>
+ <artifactId>coap</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server.transport</groupId>
+ <artifactId>mqtt</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>dao</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>dao</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.takari.junit</groupId>
+ <artifactId>takari-cpsuite</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.cassandraunit</groupId>
+ <artifactId>cassandra-unit</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ </exclusion>
+ </exclusions>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>ui</artifactId>
+ <version>${project.version}</version>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-security</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-websocket</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.jsonwebtoken</groupId>
+ <artifactId>jjwt</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.velocity</groupId>
+ <artifactId>velocity</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.velocity</groupId>
+ <artifactId>velocity-tools</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-context-support</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.security</groupId>
+ <artifactId>spring-security-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.jayway.jsonpath</groupId>
+ <artifactId>json-path</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.jayway.jsonpath</groupId>
+ <artifactId>json-path-assert</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.typesafe.akka</groupId>
+ <artifactId>akka-actor_${scala.version}</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.typesafe.akka</groupId>
+ <artifactId>akka-slf4j_${scala.version}</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>log4j-over-slf4j</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>javax.mail</groupId>
+ <artifactId>mail</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.curator</groupId>
+ <artifactId>curator-recipes</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.protobuf</groupId>
+ <artifactId>protobuf-java</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-netty</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-protobuf</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-stub</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <finalName>${pkg.name}-${project.version}</finalName>
+ <resources>
+ <resource>
+ <directory>${project.basedir}/src/main/resources</directory>
+ </resource>
+ </resources>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>${surfire.version}</version>
+ <configuration>
+ <systemPropertyVariables>
+ <spring.config.name>thingsboard</spring.config.name>
+ </systemPropertyVariables>
+ <includes>
+ <include>**/*TestSuite.java</include>
+ </includes>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-resources-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>copy-conf</id>
+ <phase>process-resources</phase>
+ <goals>
+ <goal>copy-resources</goal>
+ </goals>
+ <configuration>
+ <outputDirectory>${project.build.directory}/conf</outputDirectory>
+ <resources>
+ <resource>
+ <directory>src/main/resources</directory>
+ <excludes>
+ <exclude>logback.xml</exclude>
+ </excludes>
+ <filtering>false</filtering>
+ </resource>
+ </resources>
+ </configuration>
+ </execution>
+ <execution>
+ <id>copy-service-conf</id>
+ <phase>process-resources</phase>
+ <goals>
+ <goal>copy-resources</goal>
+ </goals>
+ <configuration>
+ <outputDirectory>${project.build.directory}/conf</outputDirectory>
+ <resources>
+ <resource>
+ <directory>src/main/conf</directory>
+ <filtering>true</filtering>
+ </resource>
+ </resources>
+ </configuration>
+ </execution>
+ <execution>
+ <id>copy-control</id>
+ <phase>process-resources</phase>
+ <goals>
+ <goal>copy-resources</goal>
+ </goals>
+ <configuration>
+ <outputDirectory>${project.build.directory}/control</outputDirectory>
+ <resources>
+ <resource>
+ <directory>src/main/scripts/control</directory>
+ <filtering>true</filtering>
+ </resource>
+ </resources>
+ </configuration>
+ </execution>
+ <execution>
+ <id>copy-data-cql</id>
+ <phase>process-resources</phase>
+ <goals>
+ <goal>copy-resources</goal>
+ </goals>
+ <configuration>
+ <outputDirectory>${project.build.directory}/data</outputDirectory>
+ <resources>
+ <resource>
+ <directory>../dao/src/main/resources</directory>
+ <includes>
+ <include>**/*.cql</include>
+ </includes>
+ <filtering>false</filtering>
+ </resource>
+ </resources>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <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.server.extensions</groupId>
+ <artifactId>extension-rabbitmq</artifactId>
+ <classifier>extension</classifier>
+ </artifactItem>
+ <artifactItem>
+ <groupId>org.thingsboard.server.extensions</groupId>
+ <artifactId>extension-rest-api-call</artifactId>
+ <classifier>extension</classifier>
+ </artifactItem>
+ <artifactItem>
+ <groupId>org.thingsboard.server.extensions</groupId>
+ <artifactId>extension-kafka</artifactId>
+ <classifier>extension</classifier>
+ </artifactItem>
+ </artifactItems>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifestEntries>
+ <Implementation-Title>Thingsboard</Implementation-Title>
+ <Implementation-Version>${project.version}</Implementation-Version>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-maven-plugin</artifactId>
+ <configuration>
+ <layout>ZIP</layout>
+ <executable>true</executable>
+ <excludeDevtools>true</excludeDevtools>
+ <embeddedLaunchScriptProperties>
+ <confFolder>${pkg.installFolder}/conf</confFolder>
+ <logFolder>${pkg.logFolder}</logFolder>
+ <logFilename>${pkg.name}.out</logFilename>
+ </embeddedLaunchScriptProperties>
+ </configuration>
+ <executions>
+ <execution>
+ <goals>
+ <goal>repackage</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.fortasoft</groupId>
+ <artifactId>gradle-maven-plugin</artifactId>
+ <configuration>
+ <tasks>
+ <task>build</task>
+ <task>buildDeb</task>
+ <task>buildRpm</task>
+ </tasks>
+ <args>
+ <arg>-PprojectBuildDir=${project.build.directory}</arg>
+ <arg>-PprojectVersion=${project.version}</arg>
+ <arg>-PmainJar=${project.build.directory}/${project.build.finalName}.${project.packaging}</arg>
+ <arg>-PpkgName=${pkg.name}</arg>
+ <arg>-PpkgInstallFolder=${pkg.installFolder}</arg>
+ <arg>-PpkgLogFolder=${pkg.logFolder}</arg>
+ </args>
+ </configuration>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>invoke</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.xolstice.maven.plugins</groupId>
+ <artifactId>protobuf-maven-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>build-helper-maven-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+</project>
application/src/main/conf/logback.xml 44(+44 -0)
diff --git a/application/src/main/conf/logback.xml b/application/src/main/conf/logback.xml
new file mode 100644
index 0000000..4187356
--- /dev/null
+++ b/application/src/main/conf/logback.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+
+ Copyright © 2016 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.
+
+-->
+<!DOCTYPE configuration>
+<configuration>
+
+ <appender name="fileLogAppender"
+ class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <file>${pkg.logFolder}/${pkg.name}.log</file>
+ <rollingPolicy
+ class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+ <fileNamePattern>${pkg.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+ <maxFileSize>100MB</maxFileSize>
+ <maxHistory>30</maxHistory>
+ <totalSizeCap>3GB</totalSizeCap>
+ </rollingPolicy>
+ <encoder>
+ <pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <logger name="org.thingsboard.server" level="INFO" />
+ <logger name="akka" level="INFO" />
+
+ <root level="INFO">
+ <appender-ref ref="fileLogAppender"/>
+ </root>
+
+</configuration>
application/src/main/conf/thingsboard.conf 19(+19 -0)
diff --git a/application/src/main/conf/thingsboard.conf b/application/src/main/conf/thingsboard.conf
new file mode 100644
index 0000000..dc161c0
--- /dev/null
+++ b/application/src/main/conf/thingsboard.conf
@@ -0,0 +1,19 @@
+#
+# Copyright © 2016 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 JAVA_OPTS="$JAVA_OPTS"
+export LOG_FILENAME=${pkg.name}.out
+export LOADER_PATH=${pkg.installFolder}/conf,${pkg.installFolder}/extensions
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
new file mode 100644
index 0000000..6b5dd56
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -0,0 +1,191 @@
+/**
+ * Copyright © 2016 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;
+
+import akka.actor.ActorRef;
+import akka.actor.ActorSystem;
+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.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+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.cluster.ServerAddress;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
+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.event.EventService;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+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 java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Optional;
+
+@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;
+
+ @Autowired
+ @Getter private DiscoveryService discoveryService;
+
+ @Autowired
+ @Getter @Setter private ComponentDiscoveryService componentService;
+
+ @Autowired
+ @Getter private ClusterRoutingService routingService;
+
+ @Autowired
+ @Getter private ClusterRpcService rpcService;
+
+ @Autowired
+ @Getter private DeviceAuthService deviceAuthService;
+
+ @Autowired
+ @Getter private DeviceService deviceService;
+
+ @Autowired
+ @Getter private TenantService tenantService;
+
+ @Autowired
+ @Getter private CustomerService customerService;
+
+ @Autowired
+ @Getter private RuleService ruleService;
+
+ @Autowired
+ @Getter private PluginService pluginService;
+
+ @Autowired
+ @Getter private TimeseriesService tsService;
+
+ @Autowired
+ @Getter private AttributesService attributesService;
+
+ @Autowired
+ @Getter private EventService eventService;
+
+ @Autowired
+ @Getter @Setter private PluginWebSocketMsgEndpoint wsMsgEndpoint;
+
+ @Value("${actors.session.sync.timeout}")
+ @Getter private long syncSessionTimeout;
+
+ @Value("${actors.plugin.termination.delay}")
+ @Getter private long pluginActorTerminationDelay;
+
+ @Value("${actors.plugin.processing.timeout}")
+ @Getter private long pluginProcessingTimeout;
+
+ @Value("${actors.plugin.error_persist_frequency}")
+ @Getter private long pluginErrorPersistFrequency;
+
+ @Value("${actors.rule.termination.delay}")
+ @Getter private long ruleActorTerminationDelay;
+
+ @Value("${actors.rule.error_persist_frequency}")
+ @Getter private long ruleErrorPersistFrequency;
+
+ @Value("${actors.statistics.enabled}")
+ @Getter private boolean statisticsEnabled;
+
+ @Value("${actors.statistics.persist_frequency}")
+ @Getter private long statisticsPersistFrequency;
+
+ @Getter @Setter private ActorSystem actorSystem;
+
+ @Getter @Setter private ActorRef appActor;
+
+ @Getter @Setter private ActorRef sessionManagerActor;
+
+ @Getter @Setter private ActorRef statsActor;
+
+ @Getter private final Config config;
+
+ public ActorSystemContext() {
+ config = ConfigFactory.parseResources(AKKA_CONF_FILE_NAME).withFallback(ConfigFactory.load());
+ }
+
+ public Scheduler getScheduler() {
+ return actorSystem.scheduler();
+ }
+
+ public void persistError(TenantId tenantId, EntityId entityId, String method, Exception e) {
+ Event event = new Event();
+ event.setTenantId(tenantId);
+ event.setEntityId(entityId);
+ event.setType(DataConstants.ERROR);
+ event.setBody(toBodyJson(discoveryService.getCurrentServer().getServerAddress(), method, toString(e)));
+ persistEvent(event);
+ }
+
+ public void persistLifecycleEvent(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent lcEvent, Exception e) {
+ Event event = new Event();
+ event.setTenantId(tenantId);
+ event.setEntityId(entityId);
+ event.setType(DataConstants.LC_EVENT);
+ event.setBody(toBodyJson(discoveryService.getCurrentServer().getServerAddress(), lcEvent, Optional.ofNullable(e)));
+ persistEvent(event);
+ }
+
+ private void persistEvent(Event event) {
+ eventService.save(event);
+ }
+
+ private String toString(Exception e) {
+ StringWriter sw = new StringWriter();
+ e.printStackTrace(new PrintWriter(sw));
+ return sw.toString();
+ }
+
+ private JsonNode toBodyJson(ServerAddress server, ComponentLifecycleEvent event, Optional<Exception> e) {
+ ObjectNode node = mapper.createObjectNode().put("server", server.toString()).put("event", event.name());
+ if (e.isPresent()) {
+ node = node.put("success", false);
+ node = node.put("error", toString(e.get()));
+ } else {
+ node = node.put("success", true);
+ }
+ return node;
+ }
+
+ private JsonNode toBodyJson(ServerAddress server, String method, String body) {
+ return mapper.createObjectNode().put("server", server.toString()).put("method", method).put("error", body);
+ }
+}
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
new file mode 100644
index 0000000..c370616
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
@@ -0,0 +1,226 @@
+/**
+ * Copyright © 2016 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.app;
+
+import akka.actor.*;
+import akka.actor.SupervisorStrategy.Directive;
+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.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.tenant.TenantActor;
+import org.thingsboard.server.common.data.Tenant;
+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.plugin.ComponentLifecycleMsg;
+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 {
+
+ 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);
+ this.tenantService = systemContext.getTenantService();
+ this.tenantActors = new HashMap<>();
+ }
+
+ @Override
+ public SupervisorStrategy supervisorStrategy() {
+ return strategy;
+ }
+
+ @Override
+ public void preStart() {
+ logger.info("Starting main system actor.");
+ try {
+ ruleManager.init(this.context());
+ pluginManager.init(this.context());
+
+ PageDataIterable<Tenant> tenantIterator = new PageDataIterable<>(link -> tenantService.findTenants(link), ENTITY_PACK_LIMIT);
+ for (Tenant tenant : tenantIterator) {
+ logger.debug("[{}] Creating tenant actor", tenant.getId());
+ getOrCreateTenantActor(tenant.getId());
+ logger.debug("Tenant actor created.");
+ }
+
+ logger.info("Main system actor started.");
+ } catch (Exception e) {
+ logger.error(e, "Unknown failure");
+ }
+ }
+
+ @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);
+ }
+ }
+
+ private void onPluginTerminated(PluginTerminationMsg msg) {
+ pluginManager.remove(msg.getId());
+ }
+
+ private void broadcast(Object msg) {
+ pluginManager.broadcast(msg);
+ tenantActors.values().stream().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+ }
+
+ private void onToRuleMsg(ToRuleActorMsg msg) {
+ ActorRef target;
+ if (SYSTEM_TENANT.equals(msg.getTenantId())) {
+ target = ruleManager.getOrCreateRuleActor(this.context(), msg.getRuleId());
+ } else {
+ target = getOrCreateTenantActor(msg.getTenantId());
+ }
+ 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());
+ }
+
+ private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
+ ActorRef target = null;
+ if (SYSTEM_TENANT.equals(msg.getTenantId())) {
+ if (msg.getPluginId().isPresent()) {
+ target = pluginManager.getOrCreatePluginActor(this.context(), msg.getPluginId().get());
+ } else if (msg.getRuleId().isPresent()) {
+ Optional<ActorRef> ref = ruleManager.update(this.context(), msg.getRuleId().get(), msg.getEvent());
+ if (ref.isPresent()) {
+ target = ref.get();
+ } else {
+ logger.debug("Failed to find actor for rule: [{}]", msg.getRuleId());
+ return;
+ }
+ }
+ } else {
+ target = getOrCreateTenantActor(msg.getTenantId());
+ }
+ if (target != null) {
+ target.tell(msg, ActorRef.noSender());
+ }
+ }
+
+ private void onToDeviceActorMsg(ToDeviceActorNotificationMsg msg) {
+ getOrCreateTenantActor(msg.getTenantId()).tell(msg, ActorRef.noSender());
+ }
+
+ private void processDeviceMsg(ToDeviceActorMsg toDeviceActorMsg) {
+ TenantId tenantId = toDeviceActorMsg.getTenantId();
+ ActorRef tenantActor = getOrCreateTenantActor(tenantId);
+ if (toDeviceActorMsg.getPayload().getMsgType().requiresRulesProcessing()) {
+ tenantActor.tell(new RuleChainDeviceMsg(toDeviceActorMsg, ruleManager.getRuleChain()), context().self());
+ } else {
+ tenantActor.tell(toDeviceActorMsg, context().self());
+ }
+ }
+
+ private ActorRef getOrCreateTenantActor(TenantId tenantId) {
+ ActorRef tenantActor = tenantActors.get(tenantId);
+ if (tenantActor == null) {
+ tenantActor = context().actorOf(Props.create(new TenantActor.ActorCreator(systemContext, tenantId))
+ .withDispatcher(DefaultActorService.CORE_DISPATCHER_NAME), tenantId.toString());
+ tenantActors.put(tenantId, tenantActor);
+ }
+ return tenantActor;
+ }
+
+ private void processTermination(Terminated message) {
+ ActorRef terminated = message.actor();
+ if (terminated instanceof LocalActorRef) {
+ logger.debug("Removed actor: {}", terminated);
+ } else {
+ throw new IllegalStateException("Remote actors are not supported!");
+ }
+ }
+
+ public static class ActorCreator extends ContextBasedCreator<AppActor> {
+ private static final long serialVersionUID = 1L;
+
+ public ActorCreator(ActorSystemContext context) {
+ super(context);
+ }
+
+ @Override
+ public AppActor create() throws Exception {
+ return new AppActor(context);
+ }
+ }
+
+ private final SupervisorStrategy strategy = new OneForOneStrategy(3, Duration.create("1 minute"), new Function<Throwable, Directive>() {
+ @Override
+ public Directive apply(Throwable t) {
+ logger.error(t, "Unknown failure");
+ if (t instanceof RuntimeException) {
+ return SupervisorStrategy.restart();
+ } else {
+ return SupervisorStrategy.stop();
+ }
+ }
+ });
+}
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
new file mode 100644
index 0000000..8b669e9
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright © 2016 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 akka.event.Logging;
+import akka.event.LoggingAdapter;
+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.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.ToDeviceActorNotificationMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.*;
+
+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);
+ }
+
+ @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) {
+ processor.processAttributesUpdate(context(), (DeviceAttributesEventNotificationMsg) msg);
+ } else if (msg instanceof ToDeviceRpcRequestPluginMsg) {
+ processor.processRpcRequest(context(), (ToDeviceRpcRequestPluginMsg) 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());
+ }
+ }
+
+ public static class ActorCreator extends ContextBasedCreator<DeviceActor> {
+ private static final long serialVersionUID = 1L;
+
+ private final TenantId tenantId;
+ private final DeviceId deviceId;
+
+ public ActorCreator(ActorSystemContext context, TenantId tenantId, DeviceId deviceId) {
+ super(context);
+ this.tenantId = tenantId;
+ this.deviceId = deviceId;
+ }
+
+ @Override
+ public DeviceActor create() throws Exception {
+ return new DeviceActor(context, tenantId, deviceId);
+ }
+ }
+
+}
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
new file mode 100644
index 0000000..3949691
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
@@ -0,0 +1,366 @@
+/**
+ * Copyright © 2016 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 akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.event.LoggingAdapter;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.rule.ChainProcessingContext;
+import org.thingsboard.server.actors.rule.ChainProcessingMetaData;
+import org.thingsboard.server.actors.rule.RuleProcessingMsg;
+import org.thingsboard.server.actors.rule.RulesProcessedMsg;
+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.id.DeviceId;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.data.kv.AttributeKey;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.core.AttributesUpdateNotification;
+import org.thingsboard.server.common.msg.core.BasicCommandAckResponse;
+import org.thingsboard.server.common.msg.core.BasicToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.core.SessionCloseMsg;
+import org.thingsboard.server.common.msg.core.ToDeviceRpcRequestMsg;
+import org.thingsboard.server.common.msg.core.ToDeviceRpcResponseMsg;
+import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.kv.BasicAttributeKVMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.extensions.api.device.DeviceAttributes;
+import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
+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.HashMap;
+import java.util.HashSet;
+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;
+import java.util.stream.Collectors;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
+
+ private final DeviceId deviceId;
+ private final Map<SessionId, SessionInfo> attributeSubscriptions;
+ private final Map<SessionId, SessionInfo> rpcSubscriptions;
+
+ private final Map<Integer, ToDeviceRpcRequestMetadata> rpcPendingMap;
+
+ private int rpcSeq = 0;
+ private DeviceAttributes deviceAttributes;
+
+ public DeviceActorMessageProcessor(ActorSystemContext systemContext, LoggingAdapter logger, DeviceId deviceId) {
+ super(systemContext, logger);
+ this.deviceId = deviceId;
+ this.attributeSubscriptions = new HashMap<>();
+ this.rpcSubscriptions = new HashMap<>();
+ this.rpcPendingMap = new HashMap<>();
+ refreshAttributes();
+ }
+
+ private void refreshAttributes() {
+ this.deviceAttributes = new DeviceAttributes(fetchAttributes(DataConstants.CLIENT_SCOPE),
+ fetchAttributes(DataConstants.SERVER_SCOPE), fetchAttributes(DataConstants.SHARED_SCOPE));
+ }
+
+ void processRpcRequest(ActorContext context, ToDeviceRpcRequestPluginMsg msg) {
+ ToDeviceRpcRequest request = msg.getMsg();
+ ToDeviceRpcRequestBody body = request.getBody();
+ ToDeviceRpcRequestMsg rpcRequest = new ToDeviceRpcRequestMsg(
+ rpcSeq++,
+ body.getMethod(),
+ body.getParams()
+ );
+
+ long timeout = request.getExpirationTime() - System.currentTimeMillis();
+ if (timeout <= 0) {
+ logger.debug("[{}][{}] Ignoring message due to exp time reached", deviceId, request.getId(), request.getExpirationTime());
+ return;
+ }
+
+ boolean sent = rpcSubscriptions.size() > 0;
+ Set<SessionId> syncSessionSet = new HashSet<>();
+ rpcSubscriptions.entrySet().forEach(sub -> {
+ ToDeviceSessionActorMsg response = new BasicToDeviceSessionActorMsg(rpcRequest, sub.getKey());
+ sendMsgToSessionActor(response, sub.getValue().getServer());
+ if (SessionType.SYNC == sub.getValue().getType()) {
+ syncSessionSet.add(sub.getKey());
+ }
+ });
+ 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());
+ } else {
+ registerPendingRpcRequest(context, msg, sent, rpcRequest, timeout);
+ }
+ if (sent) {
+ logger.debug("[{}] RPC request {} is sent!", deviceId, request.getId());
+ } else {
+ logger.debug("[{}] RPC request {} is NOT sent!", deviceId, request.getId());
+ }
+
+ }
+
+ 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);
+ scheduleMsgWithDelay(context, timeoutMsg, timeoutMsg.getTimeout());
+ }
+
+ public void processTimeout(ActorContext context, TimeoutMsg msg) {
+ ToDeviceRpcRequestMetadata requestMd = rpcPendingMap.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());
+ }
+ }
+
+ 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 (type == SessionType.SYNC) {
+ logger.debug("[{}] Cleanup sync rpc session [{}]", deviceId, sessionId);
+ rpcSubscriptions.remove(sessionId);
+ }
+ } else {
+ logger.debug("[{}] No pending RPC messages for new async session [{}]", deviceId, sessionId);
+ }
+ Set<UUID> sentOneWayIds = new HashSet<>();
+ if (type == SessionType.ASYNC) {
+ rpcPendingMap.entrySet().forEach(processPendingRpc(context, sessionId, server, sentOneWayIds));
+ } else {
+ rpcPendingMap.entrySet().stream().findFirst().ifPresent(processPendingRpc(context, sessionId, server, sentOneWayIds));
+ }
+
+ sentOneWayIds.forEach(rpcPendingMap::remove);
+ }
+
+ private Consumer<Map.Entry<Integer, ToDeviceRpcRequestMetadata>> processPendingRpc(ActorContext context, SessionId sessionId, Optional<ServerAddress> server, Set<UUID> sentOneWayIds) {
+ return entry -> {
+ ToDeviceRpcRequest request = entry.getValue().getMsg().getMsg();
+ ToDeviceRpcRequestBody body = request.getBody();
+ if (request.isOneway()) {
+ sentOneWayIds.add(request.getId());
+ ToPluginRpcResponseDeviceMsg responsePluginMsg = toPluginRpcResponseMsg(entry.getValue().getMsg(), (String) null);
+ context.parent().tell(responsePluginMsg, ActorRef.noSender());
+ }
+ ToDeviceRpcRequestMsg rpcRequest = new ToDeviceRpcRequestMsg(
+ entry.getKey(),
+ body.getMethod(),
+ body.getParams()
+ );
+ ToDeviceSessionActorMsg response = new BasicToDeviceSessionActorMsg(rpcRequest, sessionId);
+ sendMsgToSessionActor(response, server);
+ };
+ }
+
+ void process(ActorContext context, ToDeviceActorMsg msg) {
+ processSubscriptionCommands(context, msg);
+ processRpcResponses(context, msg);
+ processSessionStateMsgs(msg);
+ }
+
+ void processAttributesUpdate(ActorContext context, DeviceAttributesEventNotificationMsg msg) {
+ //TODO: improve this procedure to fetch only changed attributes.
+ refreshAttributes();
+ //TODO: support attributes deletion
+ Set<AttributeKey> keys = msg.getKeys();
+ if (attributeSubscriptions.size() > 0) {
+ ToDeviceMsg notification = null;
+ if (msg.isDeleted()) {
+ List<AttributeKey> sharedKeys = keys.stream()
+ .filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope()))
+ .collect(Collectors.toList());
+ notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromDeleted(sharedKeys));
+ } else {
+ List<AttributeKvEntry> attributes = keys.stream()
+ .filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope()))
+ .map(key -> deviceAttributes.getServerPublicAttribute(key.getAttributeKey()))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toList());
+ if (attributes.size() > 0) {
+ notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromShared(attributes));
+ } else {
+ logger.debug("[{}] No public server side attributes changed!", deviceId);
+ }
+ }
+ if (notification != null) {
+ ToDeviceMsg finalNotification = notification;
+ attributeSubscriptions.entrySet().forEach(sub -> {
+ ToDeviceSessionActorMsg response = new BasicToDeviceSessionActorMsg(finalNotification, sub.getKey());
+ sendMsgToSessionActor(response, sub.getValue().getServer());
+ });
+ }
+ } else {
+ logger.debug("[{}] No registered attributes subscriptions to process!", deviceId);
+ }
+ }
+
+ void process(ActorContext context, RuleChainDeviceMsg srcMsg) {
+ ChainProcessingMetaData md = new ChainProcessingMetaData(srcMsg.getRuleChain(),
+ srcMsg.getToDeviceActorMsg(), 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) {
+ SessionId sessionId = msg.getSessionId();
+ FromDeviceMsg inMsg = msg.getPayload();
+ if (inMsg.getMsgType() == MsgType.TO_DEVICE_RPC_RESPONSE) {
+ logger.debug("[{}] Processing rpc command response [{}]", deviceId, sessionId);
+ ToDeviceRpcResponseMsg responseMsg = (ToDeviceRpcResponseMsg) inMsg;
+ ToDeviceRpcRequestMetadata requestMd = rpcPendingMap.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());
+ }
+ } 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());
+ }
+ }
+ }
+
+ public 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()
+ .map(serverAddress -> serverAddress.equals(msg.getServerAddress())).orElse(false);
+ attributeSubscriptions.entrySet().removeIf(filter);
+ rpcSubscriptions.entrySet().removeIf(filter);
+ }
+ }
+
+ 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) {
+ SessionId sessionId = msg.getSessionId();
+ SessionType sessionType = msg.getSessionType();
+ FromDeviceMsg inMsg = msg.getPayload();
+ if (inMsg.getMsgType() == MsgType.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) {
+ logger.debug("[{}] Canceling attributes subscription for session [{}]", deviceId, sessionId);
+ attributeSubscriptions.remove(sessionId);
+ } else if (inMsg.getMsgType() == MsgType.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) {
+ logger.debug("[{}] Canceling rpc subscription for session [{}][{}]", deviceId, sessionId, sessionType);
+ rpcSubscriptions.remove(sessionId);
+ }
+ }
+
+ private void processSessionStateMsgs(ToDeviceActorMsg msg) {
+ SessionId sessionId = msg.getSessionId();
+ FromDeviceMsg inMsg = msg.getPayload();
+ if (inMsg instanceof SessionCloseMsg) {
+ logger.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId);
+ attributeSubscriptions.remove(sessionId);
+ rpcSubscriptions.remove(sessionId);
+ }
+ }
+
+ private void sendMsgToSessionActor(ToDeviceSessionActorMsg response, Optional<ServerAddress> sessionAddress) {
+ if (sessionAddress.isPresent()) {
+ ServerAddress address = sessionAddress.get();
+ logger.debug("{} Forwarding msg: {}", address, response);
+ systemContext.getRpcService().tell(sessionAddress.get(), response);
+ } else {
+ systemContext.getSessionManagerActor().tell(response, ActorRef.noSender());
+ }
+ }
+
+ private List<AttributeKvEntry> fetchAttributes(String attributeType) {
+ return systemContext.getAttributesService().findAll(this.deviceId, attributeType);
+ }
+
+}
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
new file mode 100644
index 0000000..178c6e7
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 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.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;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class SessionInfo {
+ private final SessionType type;
+ private final Optional<ServerAddress> server;
+}
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
new file mode 100644
index 0000000..338c7eb
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 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.Data;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class ToDeviceRpcRequestMetadata {
+ private final ToDeviceRpcRequestPluginMsg msg;
+ private final boolean sent;
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActor.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActor.java
new file mode 100644
index 0000000..1deb031
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActor.java
@@ -0,0 +1,151 @@
+/**
+ * Copyright © 2016 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.plugin;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ComponentActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.actors.stats.StatsPersistTick;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg;
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+
+public class PluginActor extends ComponentActor<PluginId, PluginActorMessageProcessor> {
+
+ private PluginActor(ActorSystemContext systemContext, TenantId tenantId, PluginId pluginId) {
+ super(systemContext, tenantId, pluginId);
+ setProcessor(new PluginActorMessageProcessor(tenantId, pluginId, systemContext,
+ logger, context().parent(), context().self()));
+ }
+
+ @Override
+ public void onReceive(Object msg) throws Exception {
+ if (msg instanceof PluginWebsocketMsg) {
+ onWebsocketMsg((PluginWebsocketMsg<?>) msg);
+ } else if (msg instanceof PluginRestMsg) {
+ onRestMsg((PluginRestMsg) msg);
+ } else if (msg instanceof PluginCallbackMessage) {
+ onPluginCallback((PluginCallbackMessage) msg);
+ } else if (msg instanceof RuleToPluginMsgWrapper) {
+ onRuleToPluginMsg((RuleToPluginMsgWrapper) msg);
+ } else if (msg instanceof PluginRpcMsg) {
+ onRpcMsg((PluginRpcMsg) msg);
+ } else if (msg instanceof ClusterEventMsg) {
+ onClusterEventMsg((ClusterEventMsg) msg);
+ } else if (msg instanceof ComponentLifecycleMsg) {
+ onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+ } else if (msg instanceof ToPluginRpcResponseDeviceMsg) {
+ onRpcResponse((ToPluginRpcResponseDeviceMsg) msg);
+ } else if (msg instanceof PluginTerminationMsg) {
+ logger.info("[{}][{}] Going to terminate plugin actor.", tenantId, id);
+ context().parent().tell(msg, ActorRef.noSender());
+ context().stop(self());
+ } else if (msg instanceof TimeoutMsg) {
+ onTimeoutMsg(context(), (TimeoutMsg) msg);
+ } else if (msg instanceof StatsPersistTick) {
+ onStatsPersistTick(id);
+ } else {
+ logger.debug("[{}][{}] Unknown msg type.", tenantId, id, msg.getClass().getName());
+ }
+ }
+
+ private void onPluginCallback(PluginCallbackMessage msg) {
+ try {
+ processor.onPluginCallbackMsg(msg);
+ } catch (Exception e) {
+ logAndPersist("onPluginCallbackMsg", e);
+ }
+ }
+
+ private void onTimeoutMsg(ActorContext context, TimeoutMsg msg) {
+ processor.onTimeoutMsg(context, msg);
+ }
+
+ private void onRpcResponse(ToPluginRpcResponseDeviceMsg msg) {
+ processor.onDeviceRpcMsg(msg.getResponse());
+ }
+
+ private void onRuleToPluginMsg(RuleToPluginMsgWrapper msg) throws RuleException {
+ logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg());
+ try {
+ processor.onRuleToPluginMsg(msg);
+ increaseMessagesProcessedCount();
+ } catch (Exception e) {
+ logAndPersist("onRuleMsg", e);
+ }
+ }
+
+ private void onWebsocketMsg(PluginWebsocketMsg<?> msg) {
+ logger.debug("[{}] Going to process web socket msg: {}", id, msg);
+ try {
+ processor.onWebsocketMsg(msg);
+ increaseMessagesProcessedCount();
+ } catch (Exception e) {
+ logAndPersist("onWebsocketMsg", e);
+ }
+ }
+
+ private void onRestMsg(PluginRestMsg msg) {
+ logger.debug("[{}] Going to process rest msg: {}", id, msg);
+ try {
+ processor.onRestMsg(msg);
+ increaseMessagesProcessedCount();
+ } catch (Exception e) {
+ logAndPersist("onRestMsg", e);
+ }
+ }
+
+ private void onRpcMsg(PluginRpcMsg msg) {
+ try {
+ logger.debug("[{}] Going to process rpc msg: {}", id, msg);
+ processor.onRpcMsg(msg);
+ } catch (Exception e) {
+ logAndPersist("onRpcMsg", e);
+ }
+ }
+
+ public static class ActorCreator extends ContextBasedCreator<PluginActor> {
+ private static final long serialVersionUID = 1L;
+
+ private final TenantId tenantId;
+ private final PluginId pluginId;
+
+ public ActorCreator(ActorSystemContext context, TenantId tenantId, PluginId pluginId) {
+ super(context);
+ this.tenantId = tenantId;
+ this.pluginId = pluginId;
+ }
+
+ @Override
+ public PluginActor create() throws Exception {
+ return new PluginActor(context, tenantId, pluginId);
+ }
+ }
+
+ @Override
+ protected long getErrorPersistFrequency() {
+ return systemContext.getPluginErrorPersistFrequency();
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
new file mode 100644
index 0000000..72ae4bb
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
@@ -0,0 +1,233 @@
+/**
+ * Copyright © 2016 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.plugin;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.event.LoggingAdapter;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.api.plugins.Plugin;
+import org.thingsboard.server.extensions.api.plugins.PluginInitializationException;
+import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
+import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId> {
+
+ private final SharedPluginProcessingContext pluginCtx;
+ private final PluginProcessingContext trustedCtx;
+ private PluginMetaData pluginMd;
+ private Plugin pluginImpl;
+ private ComponentLifecycleState state;
+
+
+ protected PluginActorMessageProcessor(TenantId tenantId, PluginId pluginId, ActorSystemContext systemContext
+ , LoggingAdapter logger, ActorRef parent, ActorRef self) {
+ super(systemContext, logger, tenantId, pluginId);
+ this.pluginCtx = new SharedPluginProcessingContext(systemContext, tenantId, pluginId, parent, self);
+ this.trustedCtx = new PluginProcessingContext(pluginCtx, null);
+ }
+
+ @Override
+ public void start() throws Exception {
+ logger.info("[{}] Going to start plugin actor.", entityId);
+ pluginMd = systemContext.getPluginService().findPluginById(entityId);
+ if (pluginMd == null) {
+ throw new PluginInitializationException("Plugin not found!");
+ }
+ if (pluginMd.getConfiguration() == null) {
+ throw new PluginInitializationException("Plugin metadata is empty!");
+ }
+ state = pluginMd.getState();
+ if (state == ComponentLifecycleState.ACTIVE) {
+ logger.info("[{}] Plugin is active. Going to initialize plugin.", entityId);
+ initComponent();
+ } else {
+ logger.info("[{}] Plugin is suspended. Skipping plugin initialization.", entityId);
+ }
+ }
+
+ @Override
+ public void stop() throws Exception {
+ onStop();
+ }
+
+ private void initComponent() {
+ try {
+ pluginImpl = initComponent(pluginMd.getClazz(), ComponentType.PLUGIN, mapper.writeValueAsString(pluginMd.getConfiguration()));
+ } catch (InstantiationException e) {
+ throw new PluginInitializationException("No default constructor for plugin implementation!", e);
+ } catch (IllegalAccessException e) {
+ throw new PluginInitializationException("Illegal Access Exception during plugin initialization!", e);
+ } catch (ClassNotFoundException e) {
+ throw new PluginInitializationException("Plugin Class not found!", e);
+ } catch (JsonProcessingException e) {
+ throw new PluginInitializationException("Plugin Configuration is invalid!", e);
+ } catch (Exception e) {
+ throw new PluginInitializationException(e.getMessage(), e);
+ }
+ }
+
+ public void onRuleToPluginMsg(RuleToPluginMsgWrapper msg) throws RuleException {
+ if (state == ComponentLifecycleState.ACTIVE) {
+ pluginImpl.process(trustedCtx, msg.getRuleTenantId(), msg.getRuleId(), msg.getMsg());
+ } else {
+ //TODO: reply with plugin suspended message
+ }
+ }
+
+ public void onWebsocketMsg(PluginWebsocketMsg<?> msg) {
+ if (state == ComponentLifecycleState.ACTIVE) {
+ pluginImpl.process(new PluginProcessingContext(pluginCtx, msg.getSecurityCtx()), msg);
+ } else {
+ //TODO: reply with plugin suspended message
+ }
+ }
+
+ public void onRestMsg(PluginRestMsg msg) {
+ if (state == ComponentLifecycleState.ACTIVE) {
+ pluginImpl.process(new PluginProcessingContext(pluginCtx, msg.getSecurityCtx()), msg);
+ }
+ }
+
+ public void onRpcMsg(PluginRpcMsg msg) {
+ if (state == ComponentLifecycleState.ACTIVE) {
+ pluginImpl.process(trustedCtx, msg.getRpcMsg());
+ } else {
+ //TODO: reply with plugin suspended message
+ }
+ }
+
+ public void onPluginCallbackMsg(PluginCallbackMessage msg) {
+ if (state == ComponentLifecycleState.ACTIVE) {
+ if (msg.isSuccess()) {
+ msg.getCallback().onSuccess(trustedCtx, msg.getV());
+ } else {
+ msg.getCallback().onFailure(trustedCtx, msg.getE());
+ }
+ } else {
+ //TODO: reply with plugin suspended message
+ }
+ }
+
+
+ public void onTimeoutMsg(ActorContext context, TimeoutMsg<?> msg) {
+ if (state == ComponentLifecycleState.ACTIVE) {
+ pluginImpl.process(trustedCtx, msg);
+ }
+ }
+
+
+ public void onDeviceRpcMsg(FromDeviceRpcResponse response) {
+ if (state == ComponentLifecycleState.ACTIVE) {
+ pluginImpl.process(trustedCtx, response);
+ }
+ }
+
+ @Override
+ public void onClusterEventMsg(ClusterEventMsg msg) {
+ if (state == ComponentLifecycleState.ACTIVE) {
+ ServerAddress address = msg.getServerAddress();
+ if (msg.isAdded()) {
+ logger.debug("[{}] Going to process server add msg: {}", entityId, address);
+ pluginImpl.onServerAdded(trustedCtx, address);
+ } else {
+ logger.debug("[{}] Going to process server remove msg: {}", entityId, address);
+ pluginImpl.onServerRemoved(trustedCtx, address);
+ }
+ }
+ }
+
+ @Override
+ public void onCreated(ActorContext context) {
+ logger.info("[{}] Going to process onCreated plugin.", entityId);
+ }
+
+ @Override
+ public void onUpdate(ActorContext context) throws Exception {
+ PluginMetaData oldPluginMd = systemContext.getPluginService().findPluginById(entityId);
+ pluginMd = systemContext.getPluginService().findPluginById(entityId);
+ boolean requiresRestart = false;
+ logger.info("[{}] Plugin configuration was updated from {} to {}.", entityId, oldPluginMd, pluginMd);
+ if (!oldPluginMd.getClazz().equals(pluginMd.getClazz())) {
+ logger.info("[{}] Plugin requires restart due to clazz change from {} to {}.",
+ entityId, oldPluginMd.getClazz(), pluginMd.getClazz());
+ requiresRestart = true;
+ } else if (oldPluginMd.getConfiguration().equals(pluginMd.getConfiguration())) {
+ logger.info("[{}] Plugin requires restart due to configuration change from {} to {}.",
+ entityId, oldPluginMd.getConfiguration(), pluginMd.getConfiguration());
+ requiresRestart = true;
+ }
+ if (requiresRestart) {
+ this.state = ComponentLifecycleState.SUSPENDED;
+ if (pluginImpl != null) {
+ pluginImpl.stop(trustedCtx);
+ }
+ start();
+ }
+ }
+
+ @Override
+ public void onStop(ActorContext context) {
+ onStop();
+ scheduleMsgWithDelay(context, new PluginTerminationMsg(entityId), systemContext.getPluginActorTerminationDelay());
+ }
+
+ private void onStop() {
+ logger.info("[{}] Going to process onStop plugin.", entityId);
+ this.state = ComponentLifecycleState.SUSPENDED;
+ if (pluginImpl != null) {
+ pluginImpl.stop(trustedCtx);
+ }
+ }
+
+ @Override
+ public void onActivate(ActorContext context) throws Exception {
+ logger.info("[{}] Going to process onActivate plugin.", entityId);
+ this.state = ComponentLifecycleState.ACTIVE;
+ if (pluginImpl != null) {
+ pluginImpl.resume(trustedCtx);
+ logger.info("[{}] Plugin resumed.", entityId);
+ } else {
+ start();
+ }
+ }
+
+ @Override
+ public void onSuspend(ActorContext context) {
+ logger.info("[{}] Going to process onSuspend plugin.", entityId);
+ this.state = ComponentLifecycleState.SUSPENDED;
+ if (pluginImpl != null) {
+ pluginImpl.suspend(trustedCtx);
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginCallbackMessage.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginCallbackMessage.java
new file mode 100644
index 0000000..193ff31
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginCallbackMessage.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright © 2016 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.plugin;
+
+import lombok.Data;
+import lombok.Getter;
+import lombok.ToString;
+import org.thingsboard.server.extensions.api.plugins.PluginCallback;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+@ToString
+public final class PluginCallbackMessage<V> {
+ @Getter
+ private final PluginCallback<V> callback;
+ @Getter
+ private final boolean success;
+ @Getter
+ private final V v;
+ @Getter
+ private final Exception e;
+
+ public static <V> PluginCallbackMessage<V> onSuccess(PluginCallback<V> callback, V data) {
+ return new PluginCallbackMessage<V>(true, callback, data, null);
+ }
+
+ public static <V> PluginCallbackMessage<V> onError(PluginCallback<V> callback, Exception e) {
+ return new PluginCallbackMessage<V>(false, callback, null, e);
+ }
+
+ private PluginCallbackMessage(boolean success, PluginCallback<V> callback, V v, Exception e) {
+ this.success = success;
+ this.callback = callback;
+ this.v = v;
+ this.e = e;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
new file mode 100644
index 0000000..5541a24
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
@@ -0,0 +1,306 @@
+/**
+ * Copyright © 2016 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.plugin;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
+import com.datastax.driver.core.Row;
+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.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.CustomerId;
+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.data.kv.AttributeKey;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.PluginCallback;
+import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
+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.rpc.PluginRpcMsg;
+import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+
+import akka.actor.ActorRef;
+
+import javax.annotation.Nullable;
+
+@Slf4j
+public final class PluginProcessingContext implements PluginContext {
+
+ private static final Executor executor = Executors.newSingleThreadExecutor();
+
+ private final SharedPluginProcessingContext pluginCtx;
+ private final Optional<PluginApiCallSecurityContext> securityCtx;
+
+ public PluginProcessingContext(SharedPluginProcessingContext pluginCtx, PluginApiCallSecurityContext securityCtx) {
+ super();
+ this.pluginCtx = pluginCtx;
+ this.securityCtx = Optional.ofNullable(securityCtx);
+ }
+
+ @Override
+ public void sendPluginRpcMsg(RpcMsg msg) {
+ this.pluginCtx.rpcService.tell(new PluginRpcMsg(pluginCtx.tenantId, pluginCtx.pluginId, msg));
+ }
+
+ @Override
+ public void send(PluginWebsocketMsg<?> wsMsg) throws IOException {
+ pluginCtx.msgEndpoint.send(wsMsg);
+ }
+
+ @Override
+ public void close(PluginWebsocketSessionRef sessionRef) throws IOException {
+ pluginCtx.msgEndpoint.close(sessionRef);
+ }
+
+ @Override
+ public void saveAttributes(DeviceId deviceId, String scope, List<AttributeKvEntry> attributes, PluginCallback<Void> callback) {
+ validate(deviceId);
+ Set<AttributeKey> keys = new HashSet<>();
+ for (AttributeKvEntry attribute : attributes) {
+ keys.add(new AttributeKey(scope, attribute.getKey()));
+ }
+
+ ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.attributesService.save(deviceId, scope, attributes);
+ Futures.addCallback(rsListFuture, getListCallback(callback, v -> {
+ onDeviceAttributesChanged(deviceId, keys);
+ return null;
+ }), executor);
+ }
+
+ @Override
+ public Optional<AttributeKvEntry> loadAttribute(DeviceId deviceId, String attributeType, String attributeKey) {
+ validate(deviceId);
+ AttributeKvEntry attribute = pluginCtx.attributesService.find(deviceId, attributeType, attributeKey);
+ return Optional.ofNullable(attribute);
+ }
+
+ @Override
+ public List<AttributeKvEntry> loadAttributes(DeviceId deviceId, String attributeType, List<String> attributeKeys) {
+ validate(deviceId);
+ List<AttributeKvEntry> result = new ArrayList<>(attributeKeys.size());
+ for (String attributeKey : attributeKeys) {
+ AttributeKvEntry attribute = pluginCtx.attributesService.find(deviceId, attributeType, attributeKey);
+ if (attribute != null) {
+ result.add(attribute);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public List<AttributeKvEntry> loadAttributes(DeviceId deviceId, String attributeType) {
+ validate(deviceId);
+ return pluginCtx.attributesService.findAll(deviceId, attributeType);
+ }
+
+ @Override
+ public void removeAttributes(DeviceId deviceId, String scope, List<String> keys) {
+ validate(deviceId);
+ pluginCtx.attributesService.removeAll(deviceId, scope, keys);
+ onDeviceAttributesDeleted(deviceId, keys.stream().map(key -> new AttributeKey(scope, key)).collect(Collectors.toSet()));
+ }
+
+ @Override
+ public void saveTsData(DeviceId deviceId, TsKvEntry entry, PluginCallback<Void> callback) {
+ validate(deviceId);
+ ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entry);
+ Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor);
+ }
+
+ @Override
+ public void saveTsData(DeviceId deviceId, List<TsKvEntry> entries, PluginCallback<Void> callback) {
+ validate(deviceId);
+ ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entries);
+ Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor);
+ }
+
+ @Override
+ public List<TsKvEntry> loadTimeseries(DeviceId deviceId, TsKvQuery query) {
+ validate(deviceId);
+ return pluginCtx.tsService.find(DataConstants.DEVICE, deviceId, query);
+ }
+
+ @Override
+ public void loadLatestTimeseries(DeviceId deviceId, PluginCallback<List<TsKvEntry>> callback) {
+ validate(deviceId);
+ ResultSetFuture future = pluginCtx.tsService.findAllLatest(DataConstants.DEVICE, deviceId);
+ Futures.addCallback(future, getCallback(callback, pluginCtx.tsService::convertResultSetToTsKvEntryList), executor);
+ }
+
+ @Override
+ public void loadLatestTimeseries(DeviceId deviceId, Collection<String> keys, PluginCallback<List<TsKvEntry>> callback) {
+ validate(deviceId);
+ ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.findLatest(DataConstants.DEVICE, deviceId, keys);
+ Futures.addCallback(rsListFuture, getListCallback(callback, rsList ->
+ {
+ List<TsKvEntry> result = new ArrayList<>();
+ for (ResultSet rs : rsList) {
+ Row row = rs.one();
+ if (row != null) {
+ result.add(pluginCtx.tsService.convertResultToTsKvEntry(row));
+ }
+ }
+ return result;
+ }), executor);
+ }
+
+ @Override
+ public void reply(PluginToRuleMsg<?> msg) {
+ pluginCtx.parentActor.tell(msg, ActorRef.noSender());
+ }
+
+ @Override
+ public boolean checkAccess(DeviceId deviceId) {
+ try {
+ return validate(deviceId);
+ } catch (IllegalStateException | IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public PluginId getPluginId() {
+ return pluginCtx.pluginId;
+ }
+
+ @Override
+ public Optional<PluginApiCallSecurityContext> getSecurityCtx() {
+ return securityCtx;
+ }
+
+ private void onDeviceAttributesChanged(DeviceId deviceId, AttributeKey key) {
+ onDeviceAttributesChanged(deviceId, Collections.singleton(key));
+ }
+
+ private void onDeviceAttributesDeleted(DeviceId deviceId, Set<AttributeKey> keys) {
+ Device device = pluginCtx.deviceService.findDeviceById(deviceId);
+ pluginCtx.toDeviceActor(DeviceAttributesEventNotificationMsg.onDelete(device.getTenantId(), deviceId, keys));
+ }
+
+ private void onDeviceAttributesChanged(DeviceId deviceId, Set<AttributeKey> keys) {
+ Device device = pluginCtx.deviceService.findDeviceById(deviceId);
+ pluginCtx.toDeviceActor(DeviceAttributesEventNotificationMsg.onUpdate(device.getTenantId(), deviceId, keys));
+ }
+
+ private <T> FutureCallback<List<ResultSet>> getListCallback(final PluginCallback<T> callback, Function<List<ResultSet>, T> transformer) {
+ return new FutureCallback<List<ResultSet>>() {
+ @Override
+ public void onSuccess(@Nullable List<ResultSet> result) {
+ pluginCtx.self().tell(PluginCallbackMessage.onSuccess(callback, transformer.apply(result)), ActorRef.noSender());
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ if (t instanceof Exception) {
+ pluginCtx.self().tell(PluginCallbackMessage.onError(callback, (Exception) t), ActorRef.noSender());
+ } else {
+ log.error("Critical error: {}", t.getMessage(), t);
+ }
+ }
+ };
+ }
+
+ private <T> FutureCallback<ResultSet> getCallback(final PluginCallback<T> callback, Function<ResultSet, T> transformer) {
+ return new FutureCallback<ResultSet>() {
+ @Override
+ public void onSuccess(@Nullable ResultSet result) {
+ pluginCtx.self().tell(PluginCallbackMessage.onSuccess(callback, transformer.apply(result)), ActorRef.noSender());
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ if (t instanceof Exception) {
+ pluginCtx.self().tell(PluginCallbackMessage.onError(callback, (Exception) t), ActorRef.noSender());
+ } else {
+ log.error("Critical error: {}", t.getMessage(), t);
+ }
+ }
+ };
+ }
+
+ // TODO: replace with our own exceptions
+ private boolean validate(DeviceId deviceId) {
+ if (securityCtx.isPresent()) {
+ PluginApiCallSecurityContext ctx = securityCtx.get();
+ if (ctx.isTenantAdmin() || ctx.isCustomerUser()) {
+ Device device = pluginCtx.deviceService.findDeviceById(deviceId);
+ if (device == null) {
+ throw new IllegalStateException("Device not found!");
+ } else {
+ if (!device.getTenantId().equals(ctx.getTenantId())) {
+ throw new IllegalArgumentException("Device belongs to different tenant!");
+ } else if (ctx.isCustomerUser() && !device.getCustomerId().equals(ctx.getCustomerId())) {
+ throw new IllegalArgumentException("Device belongs to different customer!");
+ }
+ }
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public Optional<ServerAddress> resolve(DeviceId deviceId) {
+ return pluginCtx.routingService.resolve(deviceId);
+ }
+
+ @Override
+ public void getDevice(DeviceId deviceId, PluginCallback<Device> callback) {
+ //TODO: add caching here with async api.
+ Device device = pluginCtx.deviceService.findDeviceById(deviceId);
+ pluginCtx.self().tell(PluginCallbackMessage.onSuccess(callback, device), ActorRef.noSender());
+ }
+
+ @Override
+ public void getCustomerDevices(TenantId tenantId, CustomerId customerId, int limit, PluginCallback<List<Device>> callback) {
+ //TODO: add caching here with async api.
+ List<Device> devices = pluginCtx.deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, new TextPageLink(limit)).getData();
+ pluginCtx.self().tell(PluginCallbackMessage.onSuccess(callback, devices), ActorRef.noSender());
+ }
+
+ @Override
+ public void sendRpcRequest(ToDeviceRpcRequest msg) {
+ pluginCtx.sendRpcRequest(msg);
+ }
+
+ @Override
+ public void scheduleTimeoutMsg(TimeoutMsg msg) {
+ pluginCtx.scheduleTimeoutMsg(msg);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginTerminationMsg.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginTerminationMsg.java
new file mode 100644
index 0000000..4363109
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginTerminationMsg.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 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.plugin;
+
+import org.thingsboard.server.actors.shared.ActorTerminationMsg;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.SessionId;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class PluginTerminationMsg extends ActorTerminationMsg<PluginId> {
+
+ public PluginTerminationMsg(PluginId id) {
+ super(id);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/RuleToPluginMsgWrapper.java b/application/src/main/java/org/thingsboard/server/actors/plugin/RuleToPluginMsgWrapper.java
new file mode 100644
index 0000000..599ad68
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/RuleToPluginMsgWrapper.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright © 2016 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.plugin;
+
+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.msg.aware.RuleAwareMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
+
+public class RuleToPluginMsgWrapper implements ToPluginActorMsg, RuleAwareMsg {
+
+ private final TenantId pluginTenantId;
+ private final PluginId pluginId;
+ private final TenantId ruleTenantId;
+ private final RuleId ruleId;
+ private final RuleToPluginMsg<?> msg;
+
+ public RuleToPluginMsgWrapper(TenantId pluginTenantId, PluginId pluginId, TenantId ruleTenantId, RuleId ruleId, RuleToPluginMsg<?> msg) {
+ super();
+ this.pluginTenantId = pluginTenantId;
+ this.pluginId = pluginId;
+ this.ruleTenantId = ruleTenantId;
+ this.ruleId = ruleId;
+ this.msg = msg;
+ }
+
+ @Override
+ public TenantId getPluginTenantId() {
+ return pluginTenantId;
+ }
+
+ @Override
+ public PluginId getPluginId() {
+ return pluginId;
+ }
+
+ public TenantId getRuleTenantId() {
+ return ruleTenantId;
+ }
+
+ @Override
+ public RuleId getRuleId() {
+ return ruleId;
+ }
+
+
+ public RuleToPluginMsg<?> getMsg() {
+ return msg;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
new file mode 100644
index 0000000..71a6ac9
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
@@ -0,0 +1,111 @@
+/**
+ * Copyright © 2016 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.plugin;
+
+import akka.actor.ActorRef;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
+import org.thingsboard.server.common.data.id.PluginId;
+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.extensions.api.device.DeviceAttributesEventNotificationMsg;
+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.ToDeviceRpcRequestPluginMsg;
+import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
+import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
+import scala.concurrent.duration.Duration;
+
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+
+@Slf4j
+public final class SharedPluginProcessingContext {
+ final ActorRef parentActor;
+ final ActorRef currentActor;
+ final ActorSystemContext systemContext;
+ final PluginWebSocketMsgEndpoint msgEndpoint;
+ final DeviceService deviceService;
+ final TimeseriesService tsService;
+ final AttributesService attributesService;
+ final ClusterRpcService rpcService;
+ final ClusterRoutingService routingService;
+ final PluginId pluginId;
+ final TenantId tenantId;
+
+ public SharedPluginProcessingContext(ActorSystemContext sysContext, TenantId tenantId, PluginId pluginId,
+ ActorRef parentActor, ActorRef self) {
+ super();
+ this.tenantId = tenantId;
+ this.pluginId = pluginId;
+ this.parentActor = parentActor;
+ this.currentActor = self;
+ this.systemContext = sysContext;
+ this.msgEndpoint = sysContext.getWsMsgEndpoint();
+ this.tsService = sysContext.getTsService();
+ this.attributesService = sysContext.getAttributesService();
+ this.deviceService = sysContext.getDeviceService();
+ this.rpcService = sysContext.getRpcService();
+ this.routingService = sysContext.getRoutingService();
+ }
+
+ public PluginId getPluginId() {
+ return pluginId;
+ }
+
+ public void toDeviceActor(DeviceAttributesEventNotificationMsg msg) {
+ forward(msg.getDeviceId(), msg, rpcService::tell);
+ }
+
+ public void sendRpcRequest(ToDeviceRpcRequest msg) {
+ log.trace("[{}] Forwarding msg {} to device actor!", pluginId, msg);
+ ToDeviceRpcRequestPluginMsg rpcMsg = new ToDeviceRpcRequestPluginMsg(pluginId, tenantId, msg);
+ forward(msg.getDeviceId(), rpcMsg, rpcService::tell);
+ }
+
+ private <T> void forward(DeviceId deviceId, T msg, BiConsumer<ServerAddress, T> rpcFunction) {
+ Optional<ServerAddress> instance = routingService.resolve(deviceId);
+ if (instance.isPresent()) {
+ log.trace("[{}] Forwarding msg {} to remote device actor!", pluginId, msg);
+ rpcFunction.accept(instance.get(), msg);
+ } else {
+ log.trace("[{}] Forwarding msg {} to local device actor!", pluginId, msg);
+ parentActor.tell(msg, ActorRef.noSender());
+ }
+ }
+
+ public void scheduleTimeoutMsg(TimeoutMsg msg) {
+ log.debug("Scheduling msg {} with delay {} ms", msg, msg.getTimeout());
+ systemContext.getScheduler().scheduleOnce(
+ Duration.create(msg.getTimeout(), TimeUnit.MILLISECONDS),
+ currentActor,
+ msg,
+ systemContext.getActorSystem().dispatcher(),
+ ActorRef.noSender());
+
+ }
+
+ public ActorRef self() {
+ return currentActor;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/TimeoutScheduler.java b/application/src/main/java/org/thingsboard/server/actors/plugin/TimeoutScheduler.java
new file mode 100644
index 0000000..44a4676
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/TimeoutScheduler.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 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.plugin;
+
+import akka.actor.ActorRef;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface TimeoutScheduler {
+
+ void scheduleMsgWithDelay(Object msg, long delayInMs);
+
+}
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
new file mode 100644
index 0000000..64a9a73
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
@@ -0,0 +1,170 @@
+/**
+ * Copyright © 2016 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.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 {
+
+ 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();
+ this.manager = manager;
+ this.self = self;
+ }
+
+ @Override
+ public void onConnected(GrpcSession session) {
+ log.info("{} session started -> {}", getType(session), session.getRemoteServer());
+ if (!session.isClient()) {
+ manager.tell(new RpcSessionConnectedMsg(session.getRemoteServer(), session.getSessionId()), self);
+ }
+ }
+
+ @Override
+ public void onDisconnected(GrpcSession session) {
+ log.info("{} session closed -> {}", getType(session), session.getRemoteServer());
+ manager.tell(new RpcSessionDisconnectedMsg(session.isClient(), session.getRemoteServer()), self);
+ }
+
+ @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()));
+ }
+
+ @Override
+ public void onError(GrpcSession session, Throwable t) {
+ log.warn("{} session got error -> {}", getType(session), session.getRemoteServer(), t);
+ manager.tell(new RpcSessionClosedMsg(session.isClient(), session.getRemoteServer()), self);
+ session.close();
+ }
+
+ private static String getType(GrpcSession session) {
+ 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()), 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
new file mode 100644
index 0000000..a528dd6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcBroadcastMsg.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 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.rpc;
+
+import lombok.Data;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public final class RpcBroadcastMsg {
+ private final ClusterAPIProtos.ToRpcServerMessage 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
new file mode 100644
index 0000000..27ffdba
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
@@ -0,0 +1,192 @@
+/**
+ * Copyright © 2016 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.rpc;
+
+import akka.actor.ActorRef;
+import akka.actor.Props;
+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.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.*;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RpcManagerActor extends ContextAwareActor {
+
+ private final LoggingAdapter log = Logging.getLogger(getContext().system(), this);
+
+ private final Map<ServerAddress, SessionActorInfo> sessionActors;
+
+ private final Map<ServerAddress, Queue<ClusterAPIProtos.ToRpcServerMessage>> pendingMsgs;
+
+ private final ServerAddress instance;
+
+ public RpcManagerActor(ActorSystemContext systemContext) {
+ super(systemContext);
+ this.sessionActors = new HashMap<>();
+ this.pendingMsgs = new HashMap<>();
+ this.instance = systemContext.getDiscoveryService().getCurrentServer().getServerAddress();
+
+ systemContext.getDiscoveryService().getOtherServers().stream()
+ .filter(otherServer -> otherServer.getServerAddress().compareTo(instance) > 0)
+ .forEach(otherServer -> onCreateSessionRequest(
+ new RpcSessionCreateRequestMsg(UUID.randomUUID(), otherServer.getServerAddress(), null)));
+
+ }
+
+ @Override
+ public void onReceive(Object msg) throws Exception {
+ if (msg instanceof RpcSessionTellMsg) {
+ onMsg((RpcSessionTellMsg) msg);
+ } else if (msg instanceof RpcBroadcastMsg) {
+ onMsg((RpcBroadcastMsg) msg);
+ } else if (msg instanceof RpcSessionCreateRequestMsg) {
+ onCreateSessionRequest((RpcSessionCreateRequestMsg) msg);
+ } else if (msg instanceof RpcSessionConnectedMsg) {
+ onSessionConnected((RpcSessionConnectedMsg) msg);
+ } else if (msg instanceof RpcSessionDisconnectedMsg) {
+ onSessionDisconnected((RpcSessionDisconnectedMsg) msg);
+ } else if (msg instanceof RpcSessionClosedMsg) {
+ onSessionClosed((RpcSessionClosedMsg) msg);
+ } else if (msg instanceof ClusterEventMsg) {
+ onClusterEvent((ClusterEventMsg) msg);
+ }
+ }
+
+ private void onMsg(RpcBroadcastMsg msg) {
+ log.debug("Forwarding msg to session actors {}", msg);
+ sessionActors.keySet().forEach(address -> onMsg(new RpcSessionTellMsg(address, 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);
+ }
+ queue.add(msg.getMsg());
+ }
+ }
+
+ @Override
+ public void postStop() {
+ sessionActors.clear();
+ pendingMsgs.clear();
+ }
+
+ private void onClusterEvent(ClusterEventMsg msg) {
+ ServerAddress server = msg.getServerAddress();
+ if (server.compareTo(instance) > 0) {
+ if (msg.isAdded()) {
+ onCreateSessionRequest(new RpcSessionCreateRequestMsg(UUID.randomUUID(), server, null));
+ } else {
+ onSessionClose(false, server);
+ }
+ }
+ }
+
+ private void onSessionConnected(RpcSessionConnectedMsg msg) {
+ register(msg.getRemoteAddress(), msg.getId(), context().sender());
+ }
+
+ private void onSessionDisconnected(RpcSessionDisconnectedMsg msg) {
+ boolean reconnect = msg.isClient() && isRegistered(msg.getRemoteAddress());
+ onSessionClose(reconnect, msg.getRemoteAddress());
+ }
+
+ private void onSessionClosed(RpcSessionClosedMsg msg) {
+ boolean reconnect = msg.isClient() && isRegistered(msg.getRemoteAddress());
+ onSessionClose(reconnect, msg.getRemoteAddress());
+ }
+
+ private boolean isRegistered(ServerAddress address) {
+ for (ServerInstance server : systemContext.getDiscoveryService().getOtherServers()) {
+ if (server.getServerAddress().equals(address)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ 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)) {
+ sessionActors.remove(remoteAddress);
+ pendingMsgs.remove(remoteAddress);
+ if (reconnect) {
+ onCreateSessionRequest(new RpcSessionCreateRequestMsg(sessionRef.sessionId, remoteAddress, null));
+ }
+ }
+ }
+
+ private void onCreateSessionRequest(RpcSessionCreateRequestMsg msg) {
+ ActorRef actorRef = createSessionActor(msg);
+ if (msg.getRemoteAddress() != null) {
+ register(msg.getRemoteAddress(), msg.getMsgUid(), actorRef);
+ }
+ }
+
+ 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);
+ if (data != null) {
+ log.debug("[{}][{}] Forwarding {} pending messages.", remoteAddress, uuid, data.size());
+ data.forEach(msg -> sender.tell(new RpcSessionTellMsg(remoteAddress, msg), ActorRef.noSender()));
+ } else {
+ log.debug("[{}][{}] No pending messages to forward.", remoteAddress, uuid);
+ }
+ }
+
+ private ActorRef createSessionActor(RpcSessionCreateRequestMsg msg) {
+ log.debug("[{}] Creating session actor.", msg.getMsgUid());
+ ActorRef actor = context().actorOf(
+ Props.create(new RpcSessionActor.ActorCreator(systemContext, msg.getMsgUid())).withDispatcher(DefaultActorService.RPC_DISPATCHER_NAME));
+ actor.tell(msg, context().self());
+ return actor;
+ }
+
+ public static class ActorCreator extends ContextBasedCreator<RpcManagerActor> {
+ private static final long serialVersionUID = 1L;
+
+ public ActorCreator(ActorSystemContext context) {
+ super(context);
+ }
+
+ @Override
+ public RpcManagerActor create() throws Exception {
+ return new RpcManagerActor(context);
+ }
+ }
+}
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
new file mode 100644
index 0000000..a66fbc5
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright © 2016 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.rpc;
+
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
+import io.grpc.Channel;
+import io.grpc.ManagedChannelBuilder;
+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.cluster.ServerAddress;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+import org.thingsboard.server.gen.cluster.ClusterRpcServiceGrpc;
+import org.thingsboard.server.service.cluster.rpc.GrpcSession;
+import org.thingsboard.server.service.cluster.rpc.GrpcSessionListener;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RpcSessionActor extends ContextAwareActor {
+
+ private final LoggingAdapter log = Logging.getLogger(getContext().system(), this);
+
+ private final UUID sessionId;
+ private GrpcSession session;
+ private GrpcSessionListener listener;
+
+ public RpcSessionActor(ActorSystemContext systemContext, UUID sessionId) {
+ super(systemContext);
+ this.sessionId = sessionId;
+ }
+
+ @Override
+ public void onReceive(Object msg) throws Exception {
+ if (msg instanceof RpcSessionTellMsg) {
+ tell((RpcSessionTellMsg) msg);
+ } else if (msg instanceof RpcSessionCreateRequestMsg) {
+ initSession((RpcSessionCreateRequestMsg) msg);
+ }
+ }
+
+ private void tell(RpcSessionTellMsg msg) {
+ session.sendMsg(msg.getMsg());
+ }
+
+ @Override
+ public void postStop() {
+ log.info("Closing session -> {}", session.getRemoteServer());
+ session.close();
+ }
+
+ private void initSession(RpcSessionCreateRequestMsg msg) {
+ log.info("[{}] Initializing session", context().self());
+ ServerAddress remoteServer = msg.getRemoteAddress();
+ listener = new BasicRpcSessionListener(systemContext, context().parent(), context().self());
+ if (msg.getRemoteAddress() == null) {
+ // Server session
+ session = new GrpcSession(listener);
+ session.setOutputStream(msg.getResponseObserver());
+ session.initInputStream();
+ session.initOutputStream();
+ systemContext.getRpcService().onSessionCreated(msg.getMsgUid(), session.getInputStream());
+ } else {
+ // Client session
+ Channel channel = ManagedChannelBuilder.forAddress(remoteServer.getHost(), remoteServer.getPort()).usePlaintext(true).build();
+ session = new GrpcSession(remoteServer, listener);
+ session.initInputStream();
+
+ ClusterRpcServiceGrpc.ClusterRpcServiceStub stub = ClusterRpcServiceGrpc.newStub(channel);
+ StreamObserver<ClusterAPIProtos.ToRpcServerMessage> outputStream = stub.handlePluginMsgs(session.getInputStream());
+
+ session.setOutputStream(outputStream);
+ session.initOutputStream();
+ outputStream.onNext(toConnectMsg());
+ }
+ }
+
+ public static class ActorCreator extends ContextBasedCreator<RpcSessionActor> {
+ private static final long serialVersionUID = 1L;
+
+ private final UUID sessionId;
+
+ public ActorCreator(ActorSystemContext context, UUID sessionId) {
+ super(context);
+ this.sessionId = sessionId;
+ }
+
+ @Override
+ public RpcSessionActor create() throws Exception {
+ return new RpcSessionActor(context, sessionId);
+ }
+ }
+
+ private ClusterAPIProtos.ToRpcServerMessage 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();
+
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionClosedMsg.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionClosedMsg.java
new file mode 100644
index 0000000..33bde07
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionClosedMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.rpc;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public final class RpcSessionClosedMsg {
+
+ private final boolean client;
+ private final ServerAddress remoteAddress;
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionConnectedMsg.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionConnectedMsg.java
new file mode 100644
index 0000000..0cacf6c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionConnectedMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 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.rpc;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public final class RpcSessionConnectedMsg {
+
+ private final ServerAddress remoteAddress;
+ private final UUID id;
+}
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
new file mode 100644
index 0000000..0fe2817
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionCreateRequestMsg.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 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.rpc;
+
+import io.grpc.stub.StreamObserver;
+import lombok.Data;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public final class RpcSessionCreateRequestMsg {
+
+ private final UUID msgUid;
+ private final ServerAddress remoteAddress;
+ private final StreamObserver<ClusterAPIProtos.ToRpcServerMessage> responseObserver;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionDisconnectedMsg.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionDisconnectedMsg.java
new file mode 100644
index 0000000..fe6087c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionDisconnectedMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.rpc;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public final class RpcSessionDisconnectedMsg {
+
+ private final boolean client;
+ private final ServerAddress remoteAddress;
+}
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
new file mode 100644
index 0000000..7a1853a
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionTellMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.rpc;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public final class RpcSessionTellMsg {
+ private final ServerAddress serverAddress;
+ private final ClusterAPIProtos.ToRpcServerMessage msg;
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/SessionActorInfo.java b/application/src/main/java/org/thingsboard/server/actors/rpc/SessionActorInfo.java
new file mode 100644
index 0000000..70bad39
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/SessionActorInfo.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 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.rpc;
+
+import akka.actor.ActorRef;
+import lombok.Data;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public final class SessionActorInfo {
+ protected final UUID sessionId;
+ protected final ActorRef actor;
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingContext.java
new file mode 100644
index 0000000..2c7adef
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingContext.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright © 2016 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.rule;
+
+import akka.actor.ActorRef;
+import org.thingsboard.server.common.msg.core.RuleEngineError;
+import org.thingsboard.server.common.msg.core.RuleEngineErrorMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.extensions.api.device.DeviceAttributes;
+
+public class ChainProcessingContext {
+
+ private final ChainProcessingMetaData md;
+ private final int index;
+ private final RuleEngineError error;
+ private ToDeviceMsg response;
+
+
+ public ChainProcessingContext(ChainProcessingMetaData md) {
+ super();
+ this.md = md;
+ this.index = 0;
+ this.error = RuleEngineError.NO_RULES;
+ }
+
+ private ChainProcessingContext(ChainProcessingContext other, int indexOffset, RuleEngineError error) {
+ super();
+ this.md = other.md;
+ this.index = other.index + indexOffset;
+ this.error = error;
+ this.response = other.response;
+
+ if (this.index < 0 || this.index >= this.md.chain.size()) {
+ throw new IllegalArgumentException("Can't apply offset " + indexOffset + " to the chain!");
+ }
+ }
+
+ public ActorRef getDeviceActor() {
+ return md.originator;
+ }
+
+ public ActorRef getCurrentActor() {
+ return md.chain.getRuleActorMd(index).getActorRef();
+ }
+
+ public boolean hasNext() {
+ return (getChainLength() - 1) > index;
+ }
+
+ public boolean isFailure() {
+ return (error != null && error.isCritical()) || (response != null && !response.isSuccess());
+ }
+
+ public ChainProcessingContext getNext() {
+ return new ChainProcessingContext(this, 1, this.error);
+ }
+
+ public ChainProcessingContext withError(RuleEngineError error) {
+ if (error != null && (this.error == null || this.error.getPriority() < error.getPriority())) {
+ return new ChainProcessingContext(this, 0, error);
+ } else {
+ return this;
+ }
+ }
+
+ public int getChainLength() {
+ return md.chain.size();
+ }
+
+ public ToDeviceActorMsg getInMsg() {
+ return md.inMsg;
+ }
+
+ public DeviceAttributes getAttributes() {
+ return md.deviceAttributes;
+ }
+
+ public ToDeviceMsg getResponse() {
+ return response;
+ }
+
+ public void mergeResponse(ToDeviceMsg response) {
+ // TODO add merge logic
+ this.response = response;
+ }
+
+ public RuleEngineErrorMsg getError() {
+ return new RuleEngineErrorMsg(md.inMsg.getPayload().getMsgType(), error);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingMetaData.java b/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingMetaData.java
new file mode 100644
index 0000000..652cd24
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingMetaData.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016 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.rule;
+
+import org.thingsboard.server.extensions.api.device.DeviceAttributes;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+
+import akka.actor.ActorRef;
+
+/**
+ * Immutable part of chain processing data;
+ *
+ * @author ashvayka
+ */
+public final class ChainProcessingMetaData {
+
+ final RuleActorChain chain;
+ final ToDeviceActorMsg inMsg;
+ final ActorRef originator;
+ final DeviceAttributes deviceAttributes;
+
+ public ChainProcessingMetaData(RuleActorChain chain, ToDeviceActorMsg inMsg, DeviceAttributes deviceAttributes, ActorRef originator) {
+ super();
+ this.chain = chain;
+ this.inMsg = inMsg;
+ this.originator = originator;
+ this.deviceAttributes = deviceAttributes;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/ComplexRuleActorChain.java b/application/src/main/java/org/thingsboard/server/actors/rule/ComplexRuleActorChain.java
new file mode 100644
index 0000000..d7c6ce4
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/ComplexRuleActorChain.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016 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.rule;
+
+public class ComplexRuleActorChain implements RuleActorChain {
+
+ private final RuleActorChain systemChain;
+ private final RuleActorChain tenantChain;
+
+ public ComplexRuleActorChain(RuleActorChain systemChain, RuleActorChain tenantChain) {
+ super();
+ this.systemChain = systemChain;
+ this.tenantChain = tenantChain;
+ }
+
+ @Override
+ public int size() {
+ return systemChain.size() + tenantChain.size();
+ }
+
+ @Override
+ public RuleActorMetaData getRuleActorMd(int index) {
+ if (index < systemChain.size()) {
+ return systemChain.getRuleActorMd(index);
+ } else {
+ return tenantChain.getRuleActorMd(index - systemChain.size());
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/CompoundRuleActorChain.java b/application/src/main/java/org/thingsboard/server/actors/rule/CompoundRuleActorChain.java
new file mode 100644
index 0000000..edbd35f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/CompoundRuleActorChain.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright © 2016 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.rule;
+
+public class CompoundRuleActorChain {
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActor.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActor.java
new file mode 100644
index 0000000..f16a444
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActor.java
@@ -0,0 +1,90 @@
+/**
+ * Copyright © 2016 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.rule;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ComponentActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.actors.stats.StatsPersistTick;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
+
+public class RuleActor extends ComponentActor<RuleId, RuleActorMessageProcessor> {
+
+ private RuleActor(ActorSystemContext systemContext, TenantId tenantId, RuleId ruleId) {
+ super(systemContext, tenantId, ruleId);
+ setProcessor(new RuleActorMessageProcessor(tenantId, ruleId, systemContext, logger));
+ }
+
+ @Override
+ public void onReceive(Object msg) throws Exception {
+ logger.debug("[{}] Received message: {}", id, msg);
+ if (msg instanceof RuleProcessingMsg) {
+ try {
+ processor.onRuleProcessingMsg(context(), (RuleProcessingMsg) msg);
+ increaseMessagesProcessedCount();
+ } catch (Exception e) {
+ logAndPersist("onDeviceMsg", e);
+ }
+ } else if (msg instanceof PluginToRuleMsg<?>) {
+ try {
+ processor.onPluginMsg(context(), (PluginToRuleMsg<?>) msg);
+ } catch (Exception e) {
+ logAndPersist("onPluginMsg", e);
+ }
+ } else if (msg instanceof ComponentLifecycleMsg) {
+ onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+ } else if (msg instanceof ClusterEventMsg) {
+ onClusterEventMsg((ClusterEventMsg) msg);
+ } else if (msg instanceof RuleToPluginTimeoutMsg) {
+ try {
+ processor.onTimeoutMsg(context(), (RuleToPluginTimeoutMsg) msg);
+ } catch (Exception e) {
+ logAndPersist("onTimeoutMsg", e);
+ }
+ } else if (msg instanceof StatsPersistTick) {
+ onStatsPersistTick(id);
+ } else {
+ logger.debug("[{}][{}] Unknown msg type.", tenantId, id, msg.getClass().getName());
+ }
+ }
+
+ public static class ActorCreator extends ContextBasedCreator<RuleActor> {
+ private static final long serialVersionUID = 1L;
+
+ private final TenantId tenantId;
+ private final RuleId ruleId;
+
+ public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleId ruleId) {
+ super(context);
+ this.tenantId = tenantId;
+ this.ruleId = ruleId;
+ }
+
+ @Override
+ public RuleActor create() throws Exception {
+ return new RuleActor(context, tenantId, ruleId);
+ }
+ }
+
+ @Override
+ protected long getErrorPersistFrequency() {
+ return systemContext.getRuleErrorPersistFrequency();
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorChain.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorChain.java
new file mode 100644
index 0000000..ee73d81
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorChain.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 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.rule;
+
+public interface RuleActorChain {
+
+ int size();
+
+ RuleActorMetaData getRuleActorMd(int index);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
new file mode 100644
index 0000000..82011c0
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
@@ -0,0 +1,339 @@
+/**
+ * Copyright © 2016 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.rule;
+
+import java.util.*;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.plugin.RuleToPluginMsgWrapper;
+import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
+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.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.core.BasicRequest;
+import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
+import org.thingsboard.server.common.msg.core.RuleEngineError;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.common.msg.session.ex.ProcessingTimeoutException;
+import org.thingsboard.server.extensions.api.rules.*;
+import org.thingsboard.server.extensions.api.plugins.PluginAction;
+import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.event.LoggingAdapter;
+
+class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> {
+
+ private final RuleProcessingContext ruleCtx;
+ private final Map<UUID, RuleProcessingMsg> pendingMsgMap;
+
+ private RuleMetaData ruleMd;
+ private ComponentLifecycleState state;
+ private List<RuleFilter> filters;
+ private RuleProcessor processor;
+ private PluginAction action;
+
+ private TenantId pluginTenantId;
+ private PluginId pluginId;
+
+ protected RuleActorMessageProcessor(TenantId tenantId, RuleId ruleId, ActorSystemContext systemContext, LoggingAdapter logger) {
+ super(systemContext, logger, tenantId, ruleId);
+ this.pendingMsgMap = new HashMap<>();
+ this.ruleCtx = new RuleProcessingContext(systemContext, ruleId);
+ }
+
+ @Override
+ public void start() throws Exception {
+ logger.info("[{}][{}] Starting rule actor.", entityId, tenantId);
+ ruleMd = systemContext.getRuleService().findRuleById(entityId);
+ if (ruleMd == null) {
+ throw new RuleInitializationException("Rule not found!");
+ }
+ state = ruleMd.getState();
+ if (state == ComponentLifecycleState.ACTIVE) {
+ logger.info("[{}] Rule is active. Going to initialize rule components.", entityId);
+ initComponent();
+ } else {
+ logger.info("[{}] Rule is suspended. Skipping rule components initialization.", entityId);
+ }
+
+ logger.info("[{}][{}] Started rule actor.", entityId, tenantId);
+ }
+
+ @Override
+ public void stop() throws Exception {
+ onStop();
+ }
+
+
+ private void initComponent() throws RuleException {
+ try {
+ if (!ruleMd.getFilters().isArray()) {
+ throw new RuntimeException("Filters are not array!");
+ }
+ fetchPluginInfo();
+ initFilters();
+ initProcessor();
+ initAction();
+ } catch (RuntimeException e) {
+ throw new RuleInitializationException("Unknown runtime exception!", e);
+ } catch (InstantiationException e) {
+ throw new RuleInitializationException("No default constructor for rule implementation!", e);
+ } catch (IllegalAccessException e) {
+ throw new RuleInitializationException("Illegal Access Exception during rule initialization!", e);
+ } catch (ClassNotFoundException e) {
+ throw new RuleInitializationException("Rule Class not found!", e);
+ } catch (Exception e) {
+ throw new RuleException(e.getMessage(), e);
+ }
+ }
+
+ private void initAction() throws Exception {
+ JsonNode actionMd = ruleMd.getAction();
+ action = initComponent(actionMd);
+ }
+
+ private void initProcessor() throws Exception {
+ if (ruleMd.getProcessor() != null && !ruleMd.getProcessor().isNull()) {
+ processor = initComponent(ruleMd.getProcessor());
+ }
+ }
+
+ private void initFilters() throws Exception {
+ filters = new ArrayList<>(ruleMd.getFilters().size());
+ for (int i = 0; i < ruleMd.getFilters().size(); i++) {
+ filters.add(initComponent(ruleMd.getFilters().get(i)));
+ }
+ }
+
+ private void fetchPluginInfo() {
+ PluginMetaData pluginMd = systemContext.getPluginService().findPluginByApiToken(ruleMd.getPluginToken());
+ pluginTenantId = pluginMd.getTenantId();
+ pluginId = pluginMd.getId();
+ }
+
+ protected void onRuleProcessingMsg(ActorContext context, RuleProcessingMsg msg) throws RuleException {
+ if (state != ComponentLifecycleState.ACTIVE) {
+ pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_ACTIVE_RULES);
+ return;
+ }
+ ChainProcessingContext chainCtx = msg.getCtx();
+ ToDeviceActorMsg inMsg = chainCtx.getInMsg();
+
+ ruleCtx.update(inMsg, chainCtx.getAttributes());
+
+ logger.debug("[{}] Going to filter in msg: {}", entityId, inMsg);
+ for (RuleFilter filter : filters) {
+ if (!filter.filter(ruleCtx, inMsg)) {
+ logger.debug("[{}] In msg is NOT valid for processing by current rule: {}", entityId, inMsg);
+ pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_FILTERS_MATCHED);
+ return;
+ }
+ }
+ RuleProcessingMetaData inMsgMd;
+ if (processor != null) {
+ logger.debug("[{}] Going to process in msg: {}", entityId, inMsg);
+ inMsgMd = processor.process(ruleCtx, inMsg);
+ } else {
+ inMsgMd = new RuleProcessingMetaData();
+ }
+ logger.debug("[{}] Going to convert in msg: {}", entityId, inMsg);
+ Optional<RuleToPluginMsg<?>> ruleToPluginMsgOptional = action.convert(ruleCtx, inMsg, inMsgMd);
+ if (ruleToPluginMsgOptional.isPresent()) {
+ RuleToPluginMsg<?> ruleToPluginMsg = ruleToPluginMsgOptional.get();
+ logger.debug("[{}] Device msg is converter to: {}", entityId, ruleToPluginMsg);
+ context.parent().tell(new RuleToPluginMsgWrapper(pluginTenantId, pluginId, tenantId, entityId, ruleToPluginMsg), context.self());
+ if (action.isOneWayAction()) {
+ pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_TWO_WAY_ACTIONS);
+ } else {
+ pendingMsgMap.put(ruleToPluginMsg.getUid(), msg);
+ scheduleMsgWithDelay(context, new RuleToPluginTimeoutMsg(ruleToPluginMsg.getUid()), systemContext.getPluginProcessingTimeout());
+ }
+ } else {
+ logger.debug("[{}] Nothing to send to plugin: {}", entityId, pluginId);
+ pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_REQUEST_FROM_ACTIONS);
+ return;
+ }
+ }
+
+ public void onPluginMsg(ActorContext context, PluginToRuleMsg<?> msg) {
+ RuleProcessingMsg pendingMsg = pendingMsgMap.remove(msg.getUid());
+ if (pendingMsg != null) {
+ ChainProcessingContext ctx = pendingMsg.getCtx();
+ Optional<ToDeviceMsg> ruleResponseOptional = action.convert(msg);
+ if (ruleResponseOptional.isPresent()) {
+ ctx.mergeResponse(ruleResponseOptional.get());
+ pushToNextRule(context, ctx, null);
+ } else {
+ pushToNextRule(context, ctx, RuleEngineError.NO_RESPONSE_FROM_ACTIONS);
+ }
+ } else {
+ logger.warning("[{}] Processing timeout detected: [{}]", entityId, msg.getUid());
+ }
+ }
+
+ public void onTimeoutMsg(ActorContext context, RuleToPluginTimeoutMsg msg) {
+ RuleProcessingMsg pendingMsg = pendingMsgMap.remove(msg.getMsgId());
+ if (pendingMsg != null) {
+ logger.debug("[{}] Processing timeout detected [{}]: {}", entityId, msg.getMsgId(), pendingMsg);
+ ChainProcessingContext ctx = pendingMsg.getCtx();
+ pushToNextRule(context, ctx, RuleEngineError.PLUGIN_TIMEOUT);
+ }
+ }
+
+ private void pushToNextRule(ActorContext context, ChainProcessingContext ctx, RuleEngineError error) {
+ if (error != null) {
+ ctx = ctx.withError(error);
+ }
+ if (ctx.isFailure()) {
+ logger.debug("[{}] Forwarding processing chain to device actor due to failure.", ctx.getInMsg().getDeviceId());
+ ctx.getDeviceActor().tell(new RulesProcessedMsg(ctx), ActorRef.noSender());
+ } else if (!ctx.hasNext()) {
+ logger.debug("[{}] Forwarding processing chain to device actor due to end of chain.", ctx.getInMsg().getDeviceId());
+ ctx.getDeviceActor().tell(new RulesProcessedMsg(ctx), ActorRef.noSender());
+ } else {
+ logger.debug("[{}] Forwarding processing chain to next rule actor.", ctx.getInMsg().getDeviceId());
+ ChainProcessingContext nextTask = ctx.getNext();
+ nextTask.getCurrentActor().tell(new RuleProcessingMsg(nextTask), context.self());
+ }
+ }
+
+ @Override
+ public void onCreated(ActorContext context) {
+ logger.info("[{}] Going to process onCreated rule.", entityId);
+ }
+
+ @Override
+ public void onUpdate(ActorContext context) throws RuleException {
+ RuleMetaData oldRuleMd = ruleMd;
+ ruleMd = systemContext.getRuleService().findRuleById(entityId);
+ logger.info("[{}] Rule configuration was updated from {} to {}.", entityId, oldRuleMd, ruleMd);
+ try {
+ fetchPluginInfo();
+ if (!Objects.equals(oldRuleMd.getFilters(), ruleMd.getFilters())) {
+ logger.info("[{}] Rule filters require restart due to json change from {} to {}.",
+ entityId, mapper.writeValueAsString(oldRuleMd.getFilters()), mapper.writeValueAsString(ruleMd.getFilters()));
+ stopFilters();
+ initFilters();
+ }
+ if (!Objects.equals(oldRuleMd.getProcessor(), ruleMd.getProcessor())) {
+ logger.info("[{}] Rule processor require restart due to configuration change.", entityId);
+ stopProcessor();
+ initProcessor();
+ }
+ if (!Objects.equals(oldRuleMd.getAction(), ruleMd.getAction())) {
+ logger.info("[{}] Rule action require restart due to configuration change.", entityId);
+ stopAction();
+ initAction();
+ }
+ } catch (RuntimeException e) {
+ throw new RuleInitializationException("Unknown runtime exception!", e);
+ } catch (InstantiationException e) {
+ throw new RuleInitializationException("No default constructor for rule implementation!", e);
+ } catch (IllegalAccessException e) {
+ throw new RuleInitializationException("Illegal Access Exception during rule initialization!", e);
+ } catch (ClassNotFoundException e) {
+ throw new RuleInitializationException("Rule Class not found!", e);
+ } catch (JsonProcessingException e) {
+ throw new RuleInitializationException("Rule configuration is invalid!", e);
+ } catch (Exception e) {
+ throw new RuleInitializationException(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public void onActivate(ActorContext context) throws Exception {
+ logger.info("[{}] Going to process onActivate rule.", entityId);
+ this.state = ComponentLifecycleState.ACTIVE;
+ if (action != null) {
+ if (filters != null) {
+ filters.forEach(f -> f.resume());
+ }
+ if (processor != null) {
+ processor.resume();
+ }
+ if (action != null) {
+ action.resume();
+ }
+ logger.info("[{}] Rule resumed.", entityId);
+ } else {
+ start();
+ }
+ }
+
+ @Override
+ public void onSuspend(ActorContext context) {
+ logger.info("[{}] Going to process onSuspend rule.", entityId);
+ this.state = ComponentLifecycleState.SUSPENDED;
+ if (filters != null) {
+ filters.forEach(f -> f.suspend());
+ }
+ if (processor != null) {
+ processor.suspend();
+ }
+ if (action != null) {
+ action.suspend();
+ }
+ }
+
+ @Override
+ public void onStop(ActorContext context) {
+ logger.info("[{}] Going to process onStop rule.", entityId);
+ onStop();
+ scheduleMsgWithDelay(context, new RuleTerminationMsg(entityId), systemContext.getRuleActorTerminationDelay());
+ }
+
+ private void onStop() {
+ this.state = ComponentLifecycleState.SUSPENDED;
+ stopFilters();
+ stopProcessor();
+ stopAction();
+ }
+
+ @Override
+ public void onClusterEventMsg(ClusterEventMsg msg) throws Exception {
+
+ }
+
+ private void stopAction() {
+ if (action != null) {
+ action.stop();
+ }
+ }
+
+ private void stopProcessor() {
+ if (processor != null) {
+ processor.stop();
+ }
+ }
+
+ private void stopFilters() {
+ if (filters != null) {
+ filters.forEach(f -> f.stop());
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMetaData.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMetaData.java
new file mode 100644
index 0000000..e25c8aa
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMetaData.java
@@ -0,0 +1,107 @@
+/**
+ * Copyright © 2016 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.rule;
+
+import java.util.Comparator;
+
+import org.thingsboard.server.common.data.id.RuleId;
+
+import akka.actor.ActorRef;
+
+public class RuleActorMetaData {
+
+ private final RuleId ruleId;
+ private final boolean systemRule;
+ private final int weight;
+ private final ActorRef actorRef;
+
+ public static final Comparator<RuleActorMetaData> RULE_ACTOR_MD_COMPARATOR = new Comparator<RuleActorMetaData>() {
+
+ @Override
+ public int compare(RuleActorMetaData r1, RuleActorMetaData r2) {
+ if (r1.isSystemRule() && !r2.isSystemRule()) {
+ return 1;
+ } else if (!r1.isSystemRule() && r2.isSystemRule()) {
+ return -1;
+ } else {
+ return Integer.compare(r2.getWeight(), r1.getWeight());
+ }
+ }
+ };
+
+ public static RuleActorMetaData systemRule(RuleId ruleId, int weight, ActorRef actorRef) {
+ return new RuleActorMetaData(ruleId, true, weight, actorRef);
+ }
+
+ public static RuleActorMetaData tenantRule(RuleId ruleId, int weight, ActorRef actorRef) {
+ return new RuleActorMetaData(ruleId, false, weight, actorRef);
+ }
+
+ private RuleActorMetaData(RuleId ruleId, boolean systemRule, int weight, ActorRef actorRef) {
+ super();
+ this.ruleId = ruleId;
+ this.systemRule = systemRule;
+ this.weight = weight;
+ this.actorRef = actorRef;
+ }
+
+ public RuleId getRuleId() {
+ return ruleId;
+ }
+
+ public boolean isSystemRule() {
+ return systemRule;
+ }
+
+ public int getWeight() {
+ return weight;
+ }
+
+ public ActorRef getActorRef() {
+ return actorRef;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((ruleId == null) ? 0 : ruleId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ RuleActorMetaData other = (RuleActorMetaData) obj;
+ if (ruleId == null) {
+ if (other.ruleId != null)
+ return false;
+ } else if (!ruleId.equals(other.ruleId))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "RuleActorMetaData [ruleId=" + ruleId + ", systemRule=" + systemRule + ", weight=" + weight + ", actorRef=" + actorRef + "]";
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleContextAwareMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleContextAwareMsgProcessor.java
new file mode 100644
index 0000000..507b955
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleContextAwareMsgProcessor.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016 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.rule;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
+import org.thingsboard.server.common.data.id.RuleId;
+
+import akka.event.LoggingAdapter;
+
+public class RuleContextAwareMsgProcessor extends AbstractContextAwareMsgProcessor {
+
+ private final RuleId ruleId;
+
+ protected RuleContextAwareMsgProcessor(ActorSystemContext systemContext, LoggingAdapter logger, RuleId ruleId) {
+ super(systemContext, logger);
+ this.ruleId = ruleId;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingContext.java
new file mode 100644
index 0000000..bd07285
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingContext.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright © 2016 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.rule;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.dao.event.EventService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.extensions.api.device.DeviceAttributes;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+
+import java.util.Optional;
+
+public class RuleProcessingContext implements RuleContext {
+
+ private final TimeseriesService tsService;
+ private final EventService eventService;
+ private final RuleId ruleId;
+ private TenantId tenantId;
+ private CustomerId customerId;
+ private DeviceId deviceId;
+ private DeviceAttributes deviceAttributes;
+
+ RuleProcessingContext(ActorSystemContext systemContext, RuleId ruleId) {
+ this.tsService = systemContext.getTsService();
+ this.eventService = systemContext.getEventService();
+ this.ruleId = ruleId;
+ }
+
+ void update(ToDeviceActorMsg toDeviceActorMsg, DeviceAttributes attributes) {
+ this.tenantId = toDeviceActorMsg.getTenantId();
+ this.customerId = toDeviceActorMsg.getCustomerId();
+ this.deviceId = toDeviceActorMsg.getDeviceId();
+ this.deviceAttributes = attributes;
+ }
+
+ @Override
+ public RuleId getRuleId() {
+ return ruleId;
+ }
+
+ @Override
+ public DeviceAttributes getDeviceAttributes() {
+ return deviceAttributes;
+ }
+
+ @Override
+ public Event save(Event event) {
+ checkEvent(event);
+ return eventService.save(event);
+ }
+
+ @Override
+ public Optional<Event> saveIfNotExists(Event event) {
+ checkEvent(event);
+ return eventService.saveIfNotExists(event);
+ }
+
+ @Override
+ public Optional<Event> findEvent(String eventType, String eventUid) {
+ return eventService.findEvent(tenantId, deviceId, eventType, eventUid);
+ }
+
+ private void checkEvent(Event event) {
+ if (event.getTenantId() == null) {
+ event.setTenantId(tenantId);
+ } else if (!tenantId.equals(event.getTenantId())) {
+ throw new IllegalArgumentException("Invalid Tenant id!");
+ }
+ if (event.getEntityId() == null) {
+ event.setEntityId(deviceId);
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingMsg.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingMsg.java
new file mode 100644
index 0000000..548c180
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 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.rule;
+
+public class RuleProcessingMsg {
+
+ private final ChainProcessingContext ctx;
+
+ public RuleProcessingMsg(ChainProcessingContext ctx) {
+ super();
+ this.ctx = ctx;
+ }
+
+ public ChainProcessingContext getCtx() {
+ return ctx;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RulesProcessedMsg.java b/application/src/main/java/org/thingsboard/server/actors/rule/RulesProcessedMsg.java
new file mode 100644
index 0000000..dfebeac
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RulesProcessedMsg.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016 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.rule;
+
+public class RulesProcessedMsg {
+ private final ChainProcessingContext ctx;
+
+ public RulesProcessedMsg(ChainProcessingContext ctx) {
+ super();
+ this.ctx = ctx;
+ }
+
+ public ChainProcessingContext getCtx() {
+ return ctx;
+ }
+
+ @Override
+ public String toString() {
+ return "RulesProcessedMsg [ctx=" + ctx + "]";
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleTerminationMsg.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleTerminationMsg.java
new file mode 100644
index 0000000..2e92005
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleTerminationMsg.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 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.rule;
+
+import org.thingsboard.server.actors.shared.ActorTerminationMsg;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.RuleId;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RuleTerminationMsg extends ActorTerminationMsg<RuleId> {
+
+ public RuleTerminationMsg(RuleId id) {
+ super(id);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleToPluginTimeoutMsg.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleToPluginTimeoutMsg.java
new file mode 100644
index 0000000..38595d6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleToPluginTimeoutMsg.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 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.rule;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+public class RuleToPluginTimeoutMsg implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private final UUID msgId;
+
+ public RuleToPluginTimeoutMsg(UUID msgId) {
+ super();
+ this.msgId = msgId;
+ }
+
+ public UUID getMsgId() {
+ return msgId;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/SimpleRuleActorChain.java b/application/src/main/java/org/thingsboard/server/actors/rule/SimpleRuleActorChain.java
new file mode 100644
index 0000000..8112ac4
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/SimpleRuleActorChain.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016 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.rule;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public class SimpleRuleActorChain implements RuleActorChain {
+
+ private final List<RuleActorMetaData> rules;
+
+ public SimpleRuleActorChain(Set<RuleActorMetaData> ruleSet) {
+ rules = new ArrayList<>(ruleSet);
+ Collections.sort(rules, RuleActorMetaData.RULE_ACTOR_MD_COMPARATOR);
+ }
+
+ public int size() {
+ return rules.size();
+ }
+
+ public RuleActorMetaData getRuleActorMd(int index) {
+ return rules.get(index);
+ }
+
+}
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
new file mode 100644
index 0000000..1c64f54
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 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.service;
+
+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.plugin.ComponentLifecycleEvent;
+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 {
+
+ void onPluginStateChange(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent state);
+
+ void onRuleStateChange(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent state);
+}
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
new file mode 100644
index 0000000..f991552
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
@@ -0,0 +1,169 @@
+/**
+ * Copyright © 2016 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.service;
+
+import akka.actor.ActorRef;
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
+import org.thingsboard.server.actors.stats.StatsPersistMsg;
+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.ClusterEventMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgProcessor<T>> extends ContextAwareActor {
+
+ protected final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
+
+ private long lastPersistedErrorTs = 0L;
+ protected final TenantId tenantId;
+ protected final T id;
+ protected P processor;
+ private long messagesProcessed;
+ private long errorsOccurred;
+
+ public ComponentActor(ActorSystemContext systemContext, TenantId tenantId, T id) {
+ super(systemContext);
+ this.tenantId = tenantId;
+ this.id = id;
+ }
+
+ protected void setProcessor(P processor) {
+ this.processor = processor;
+ }
+
+ @Override
+ public void preStart() {
+ try {
+ processor.start();
+ logLifecycleEvent(ComponentLifecycleEvent.STARTED);
+ if (systemContext.isStatisticsEnabled()) {
+ scheduleStatsPersistTick();
+ }
+ } catch (Exception e) {
+ logger.warning("[{}][{}] Failed to start {} processor: {}", tenantId, id, id.getEntityType(), e);
+ logAndPersist("OnStart", e, true);
+ logLifecycleEvent(ComponentLifecycleEvent.STARTED, e);
+ }
+ }
+
+ private void scheduleStatsPersistTick() {
+ try {
+ processor.scheduleStatsPersistTick(context(), systemContext.getStatisticsPersistFrequency());
+ } catch (Exception e) {
+ logger.error("[{}][{}] Failed to schedule statistics store message. No statistics is going to be stored: {}", tenantId, id, e.getMessage());
+ logAndPersist("onScheduleStatsPersistMsg", e);
+ }
+ }
+
+ @Override
+ public void postStop() {
+ try {
+ processor.stop();
+ logLifecycleEvent(ComponentLifecycleEvent.STOPPED);
+ } catch (Exception e) {
+ logger.warning("[{}][{}] Failed to stop {} processor: {}", tenantId, id, id.getEntityType(), e.getMessage());
+ logAndPersist("OnStop", e, true);
+ logLifecycleEvent(ComponentLifecycleEvent.STOPPED, e);
+ }
+ }
+
+ protected void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
+ try {
+ switch (msg.getEvent()) {
+ case CREATED:
+ processor.onCreated(context());
+ break;
+ case UPDATED:
+ processor.onUpdate(context());
+ break;
+ case ACTIVATED:
+ processor.onActivate(context());
+ break;
+ case SUSPENDED:
+ processor.onSuspend(context());
+ break;
+ case DELETED:
+ processor.onStop(context());
+ }
+ logLifecycleEvent(msg.getEvent());
+ } catch (Exception e) {
+ logAndPersist("onLifecycleMsg", e, true);
+ logLifecycleEvent(msg.getEvent(), e);
+ }
+ }
+
+ protected void onClusterEventMsg(ClusterEventMsg msg) {
+ try {
+ processor.onClusterEventMsg(msg);
+ } catch (Exception e) {
+ logAndPersist("onClusterEventMsg", e);
+ }
+ }
+
+ protected void onStatsPersistTick(EntityId entityId) {
+ try {
+ systemContext.getStatsActor().tell(new StatsPersistMsg(messagesProcessed, errorsOccurred, tenantId, entityId), ActorRef.noSender());
+ resetStatsCounters();
+ } catch (Exception e) {
+ logAndPersist("onStatsPersistTick", e);
+ }
+ }
+
+ private void resetStatsCounters() {
+ messagesProcessed = 0;
+ errorsOccurred = 0;
+ }
+
+ protected void increaseMessagesProcessedCount() {
+ messagesProcessed++;
+ }
+
+
+ protected void logAndPersist(String method, Exception e) {
+ logAndPersist(method, e, false);
+ }
+
+ private void logAndPersist(String method, Exception e, boolean critical) {
+ errorsOccurred++;
+ if (critical) {
+ logger.warning("[{}][{}] Failed to process {} msg: {}", id, tenantId, method, e);
+ } else {
+ logger.debug("[{}][{}] Failed to process {} msg: {}", id, tenantId, method, e);
+ }
+ long ts = System.currentTimeMillis();
+ if (ts - lastPersistedErrorTs > getErrorPersistFrequency()) {
+ systemContext.persistError(tenantId, id, method, e);
+ lastPersistedErrorTs = ts;
+ }
+ }
+
+ protected void logLifecycleEvent(ComponentLifecycleEvent event) {
+ logLifecycleEvent(event, null);
+ }
+
+ protected void logLifecycleEvent(ComponentLifecycleEvent event, Exception e) {
+ systemContext.persistLifecycleEvent(tenantId, id, event, e);
+ }
+
+ protected abstract long getErrorPersistFrequency();
+}
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
new file mode 100644
index 0000000..b2a0de7
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 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.service;
+
+import akka.actor.UntypedActor;
+import org.thingsboard.server.actors.ActorSystemContext;
+
+public abstract class ContextAwareActor extends UntypedActor {
+
+ public static final int ENTITY_PACK_LIMIT = 1024;
+
+ protected final ActorSystemContext systemContext;
+
+ public ContextAwareActor(ActorSystemContext systemContext) {
+ super();
+ this.systemContext = systemContext;
+ }
+}
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
new file mode 100644
index 0000000..7222110
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ContextBasedCreator.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 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.service;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+
+import akka.japi.Creator;
+
+public abstract class ContextBasedCreator<T> implements Creator<T> {
+
+ private static final long serialVersionUID = 1L;
+
+ protected final ActorSystemContext context;
+
+ public ContextBasedCreator(ActorSystemContext context) {
+ super();
+ this.context = context;
+ }
+}
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
new file mode 100644
index 0000000..db6526d
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
@@ -0,0 +1,234 @@
+/**
+ * Copyright © 2016 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.service;
+
+import akka.actor.ActorRef;
+import akka.actor.ActorSystem;
+import akka.actor.Props;
+import akka.actor.Terminated;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+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.PluginId;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+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.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.plugins.rest.PluginRestMsg;
+import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
+import org.thingsboard.server.service.cluster.discovery.ServerInstance;
+import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
+import scala.concurrent.Await;
+import scala.concurrent.Future;
+import scala.concurrent.duration.Duration;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+
+@Service
+@Slf4j
+public class DefaultActorService implements ActorService {
+
+ private static final String ACTOR_SYSTEM_NAME = "Akka";
+
+ public static final String APP_DISPATCHER_NAME = "app-dispatcher";
+ public static final String CORE_DISPATCHER_NAME = "core-dispatcher";
+ public static final String RULE_DISPATCHER_NAME = "rule-dispatcher";
+ public static final String PLUGIN_DISPATCHER_NAME = "plugin-dispatcher";
+ public static final String SESSION_DISPATCHER_NAME = "session-dispatcher";
+ public static final String RPC_DISPATCHER_NAME = "rpc-dispatcher";
+
+ @Autowired
+ private ActorSystemContext actorContext;
+
+ @Autowired
+ private ClusterRpcService rpcService;
+
+ @Autowired
+ private DiscoveryService discoveryService;
+
+ private ActorSystem system;
+
+ private ActorRef appActor;
+
+ private ActorRef sessionManagerActor;
+
+ private ActorRef rpcManagerActor;
+
+ @PostConstruct
+ public void initActorSystem() {
+ log.info("Initializing Actor system. {}", actorContext.getRuleService());
+ actorContext.setActorService(this);
+ system = ActorSystem.create(ACTOR_SYSTEM_NAME, actorContext.getConfig());
+ actorContext.setActorSystem(system);
+
+ appActor = system.actorOf(Props.create(new AppActor.ActorCreator(actorContext)).withDispatcher(APP_DISPATCHER_NAME), "appActor");
+ actorContext.setAppActor(appActor);
+
+ sessionManagerActor = system.actorOf(Props.create(new SessionManagerActor.ActorCreator(actorContext)).withDispatcher(CORE_DISPATCHER_NAME),
+ "sessionManagerActor");
+ actorContext.setSessionManagerActor(sessionManagerActor);
+
+ rpcManagerActor = system.actorOf(Props.create(new RpcManagerActor.ActorCreator(actorContext)).withDispatcher(CORE_DISPATCHER_NAME),
+ "rpcManagerActor");
+
+ ActorRef statsActor = system.actorOf(Props.create(new StatsActor.ActorCreator(actorContext)).withDispatcher(CORE_DISPATCHER_NAME), "statsActor");
+ actorContext.setStatsActor(statsActor);
+
+ rpcService.init(this);
+
+ discoveryService.addListener(this);
+ log.info("Actor system initialized.");
+ }
+
+ @PreDestroy
+ public void stopActorSystem() {
+ Future<Terminated> status = system.terminate();
+ try {
+ Terminated terminated = Await.result(status, Duration.Inf());
+ log.info("Actor system terminated: {}", terminated);
+ } catch (Exception e) {
+ log.error("Failed to terminate actor system.", e);
+ }
+ }
+
+ @Override
+ public void process(SessionAwareMsg msg) {
+ if (msg instanceof SessionAwareMsg) {
+ 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);
+ 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);
+ 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));
+ }
+
+ @Override
+ public void onServerUpdated(ServerInstance server) {
+
+ }
+
+ @Override
+ public void onServerRemoved(ServerInstance server) {
+ log.trace("Processing onServerRemoved msg: {}", server);
+ broadcast(new ClusterEventMsg(server.getServerAddress(), false));
+ }
+
+ @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 broadcast(ToAllNodesMsg msg) {
+ rpcService.broadcast(msg);
+ appActor.tell(msg, ActorRef.noSender());
+ }
+
+ private void broadcast(ClusterEventMsg msg) {
+ this.appActor.tell(msg, ActorRef.noSender());
+ this.sessionManagerActor.tell(msg, ActorRef.noSender());
+ this.rpcManagerActor.tell(msg, ActorRef.noSender());
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/RestMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/service/RestMsgProcessor.java
new file mode 100644
index 0000000..44c60e5
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/service/RestMsgProcessor.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 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.service;
+
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+
+public interface RestMsgProcessor {
+
+ void process(PluginRestMsg msg);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/WebSocketMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/service/WebSocketMsgProcessor.java
new file mode 100644
index 0000000..f8ebda3
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/service/WebSocketMsgProcessor.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 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.service;
+
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+
+public interface WebSocketMsgProcessor {
+
+ void process(PluginWebsocketMsg<?> msg);
+
+}
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
new file mode 100644
index 0000000..57adf8c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java
@@ -0,0 +1,119 @@
+/**
+ * Copyright © 2016 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.session;
+
+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.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.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 AbstractSessionActorMsgProcessor(ActorSystemContext ctx, LoggingAdapter logger, SessionId sessionId) {
+ super(ctx, logger);
+ this.sessionId = sessionId;
+ }
+
+ protected abstract void processToDeviceActorMsg(ActorContext ctx, ToDeviceActorSessionMsg msg);
+
+ protected abstract void processTimeoutMsg(ActorContext context, SessionTimeoutMsg msg);
+
+ protected abstract void processToDeviceMsg(ActorContext context, ToDeviceMsg msg);
+
+ public abstract void processClusterEvent(ActorContext context, ClusterEventMsg msg);
+
+ protected void processSessionCtrlMsg(ActorContext ctx, SessionCtrlMsg msg) {
+ if (msg instanceof SessionCloseMsg) {
+ cleanupSession(ctx);
+ terminateSession(ctx, sessionId);
+ }
+ }
+
+ protected void cleanupSession(ActorContext ctx) {
+ }
+
+ protected void updateSessionCtx(ToDeviceActorSessionMsg msg, SessionType type) {
+ sessionCtx = msg.getSessionMsg().getSessionContext();
+ toDeviceActorMsgPrototype = new BasicToDeviceActorMsg(msg, type);
+ }
+
+ protected ToDeviceActorMsg toDeviceMsg(ToDeviceActorSessionMsg msg) {
+ AdaptorToSessionActorMsg adaptorMsg = msg.getSessionMsg();
+ return new BasicToDeviceActorMsg(toDeviceActorMsgPrototype, adaptorMsg.getMsg());
+ }
+
+ protected Optional<ToDeviceActorMsg> toDeviceMsg(FromDeviceMsg msg) {
+ if (toDeviceActorMsgPrototype != null) {
+ return Optional.of(new BasicToDeviceActorMsg(toDeviceActorMsgPrototype, msg));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ protected Optional<ServerAddress> forwardToAppActor(ActorContext ctx, ToDeviceActorMsg toForward) {
+ Optional<ServerAddress> address = systemContext.getRoutingService().resolve(toForward.getDeviceId());
+ forwardToAppActor(ctx, toForward, address);
+ return address;
+ }
+
+ protected Optional<ServerAddress> forwardToAppActorIfAdressChanged(ActorContext ctx, ToDeviceActorMsg toForward, Optional<ServerAddress> oldAddress) {
+ Optional<ServerAddress> newAddress = systemContext.getRoutingService().resolve(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());
+ }
+ }
+ return newAddress;
+ }
+
+ protected void forwardToAppActor(ActorContext ctx, ToDeviceActorMsg toForward, Optional<ServerAddress> address) {
+ if (address.isPresent()) {
+ systemContext.getRpcService().tell(address.get(),
+ toForward.toOtherAddress(systemContext.getRoutingService().getCurrentServer()));
+ } else {
+ getAppActor().tell(toForward, ctx.self());
+ }
+ }
+
+ public static void terminateSession(ActorContext ctx, SessionId sessionId) {
+ ctx.parent().tell(new SessionTerminationMsg(sessionId), ActorRef.noSender());
+ ctx.stop(ctx.self());
+ }
+
+ public DeviceId getDeviceId() {
+ return toDeviceActorMsgPrototype.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
new file mode 100644
index 0000000..eb812df
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
@@ -0,0 +1,133 @@
+/**
+ * Copyright © 2016 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.session;
+
+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.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.ex.SessionException;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
+
+ Map<Integer, ToDeviceActorMsg> pendingMap = new HashMap<>();
+ private Optional<ServerAddress> currentTargetServer;
+ private boolean subscribedToAttributeUpdates;
+ private boolean subscribedToRpcCommands;
+
+ public ASyncMsgProcessor(ActorSystemContext ctx, LoggingAdapter logger, SessionId sessionId) {
+ super(ctx, logger, sessionId);
+ }
+
+ @Override
+ protected void processToDeviceActorMsg(ActorContext ctx, ToDeviceActorSessionMsg msg) {
+ updateSessionCtx(msg, SessionType.ASYNC);
+ ToDeviceActorMsg pendingMsg = toDeviceMsg(msg);
+ FromDeviceMsg fromDeviceMsg = pendingMsg.getPayload();
+ switch (fromDeviceMsg.getMsgType()) {
+ case POST_TELEMETRY_REQUEST:
+ case POST_ATTRIBUTES_REQUEST:
+ FromDeviceRequestMsg requestMsg = (FromDeviceRequestMsg) fromDeviceMsg;
+ if (requestMsg.getRequestId() >= 0) {
+ logger.debug("[{}] Pending request {} registered", requestMsg.getRequestId(), requestMsg.getMsgType());
+ //TODO: handle duplicates.
+ pendingMap.put(requestMsg.getRequestId(), pendingMsg);
+ }
+ break;
+ case SUBSCRIBE_ATTRIBUTES_REQUEST:
+ subscribedToAttributeUpdates = true;
+ break;
+ case UNSUBSCRIBE_ATTRIBUTES_REQUEST:
+ subscribedToAttributeUpdates = false;
+ break;
+ case SUBSCRIBE_RPC_COMMANDS_REQUEST:
+ subscribedToRpcCommands = true;
+ break;
+ case UNSUBSCRIBE_RPC_COMMANDS_REQUEST:
+ subscribedToRpcCommands = false;
+ break;
+ }
+ currentTargetServer = forwardToAppActor(ctx, pendingMsg);
+ }
+
+ @Override
+ public void processToDeviceMsg(ActorContext context, ToDeviceMsg msg) {
+ try {
+ switch (msg.getMsgType()) {
+ case STATUS_CODE_RESPONSE:
+ case GET_ATTRIBUTES_RESPONSE:
+ ResponseMsg responseMsg = (ResponseMsg) msg;
+ if (responseMsg.getRequestId() >= 0) {
+ logger.debug("[{}] Pending request processed: {}", responseMsg.getRequestId(), responseMsg);
+ pendingMap.remove(responseMsg.getRequestId());
+ }
+ break;
+ }
+ sessionCtx.onMsg(new BasicSessionActorToAdaptorMsg(this.sessionCtx, msg));
+ } catch (SessionException e) {
+ logger.warning("Failed to push session response msg", e);
+ }
+ }
+
+ @Override
+ public void processTimeoutMsg(ActorContext context, SessionTimeoutMsg msg) {
+ // TODO Auto-generated method stub
+ }
+
+ protected void cleanupSession(ActorContext ctx) {
+ toDeviceMsg(new SessionCloseMsg()).ifPresent(msg -> forwardToAppActor(ctx, msg));
+ }
+
+ @Override
+ public void processClusterEvent(ActorContext context, ClusterEventMsg msg) {
+ if (pendingMap.size() > 0 || subscribedToAttributeUpdates || subscribedToRpcCommands) {
+ Optional<ServerAddress> newTargetServer = systemContext.getRoutingService().resolve(getDeviceId());
+ if (!newTargetServer.equals(currentTargetServer)) {
+ currentTargetServer = newTargetServer;
+ pendingMap.values().stream().forEach(v -> {
+ forwardToAppActor(context, v, currentTargetServer);
+ if (currentTargetServer.isPresent()) {
+ logger.debug("[{}] Forwarded msg to new server: {}", sessionId, currentTargetServer.get());
+ } else {
+ logger.debug("[{}] Forwarded msg to local server.", sessionId);
+ }
+ });
+ if (subscribedToAttributeUpdates) {
+ toDeviceMsg(new AttributesSubscribeMsg()).ifPresent(m -> forwardToAppActor(context, m, currentTargetServer));
+ logger.debug("[{}] Forwarded attributes subscription.", sessionId);
+ }
+ if (subscribedToRpcCommands) {
+ toDeviceMsg(new RpcSubscribeMsg()).ifPresent(m -> forwardToAppActor(context, m, currentTargetServer));
+ logger.debug("[{}] Forwarded rpc commands subscription.", sessionId);
+ }
+ }
+ }
+ }
+}
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
new file mode 100644
index 0000000..30f5d99
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java
@@ -0,0 +1,139 @@
+/**
+ * Copyright © 2016 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.session;
+
+import akka.actor.OneForOneStrategy;
+import akka.actor.SupervisorStrategy;
+import akka.japi.Function;
+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.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.session.ToDeviceActorSessionMsg;
+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.ctrl.SessionCloseMsg;
+
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
+import scala.concurrent.duration.Duration;
+
+public class SessionActor extends ContextAwareActor {
+
+ private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
+
+ private final SessionId sessionId;
+ private AbstractSessionActorMsgProcessor processor;
+
+ private SessionActor(ActorSystemContext systemContext, SessionId sessionId) {
+ super(systemContext);
+ this.sessionId = sessionId;
+ }
+
+ @Override
+ public SupervisorStrategy supervisorStrategy() {
+ return new OneForOneStrategy(-1, Duration.Inf(),
+ throwable -> {
+ logger.error(throwable, "Unknown session error");
+ if (throwable instanceof Error) {
+ return OneForOneStrategy.escalate();
+ } else {
+ return OneForOneStrategy.resume();
+ }
+ });
+ }
+
+ @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);
+ }
+ }
+
+ private void processClusterEvent(ClusterEventMsg msg) {
+ processor.processClusterEvent(context(), msg);
+ }
+
+ private void processDeviceMsg(ToDeviceActorSessionMsg msg) {
+ initProcessor(msg);
+ processor.processToDeviceActorMsg(context(), msg);
+ }
+
+ private void processToDeviceMsg(ToDeviceSessionActorMsg msg) {
+ processor.processToDeviceMsg(context(), msg.getMsg());
+ }
+
+ private void processTimeoutMsg(SessionTimeoutMsg msg) {
+ if (processor != null) {
+ processor.processTimeoutMsg(context(), msg);
+ } else {
+ logger.warning("[{}] Can't process timeout msg: {} without processor", sessionId, msg);
+ }
+ }
+
+ private void processSessionCtrlMsg(SessionCtrlMsg msg) {
+ if (processor != null) {
+ processor.processSessionCtrlMsg(context(), msg);
+ } else if (msg instanceof SessionCloseMsg) {
+ AbstractSessionActorMsgProcessor.terminateSession(context(), sessionId);
+ } else {
+ logger.warning("[{}] Can't process session ctrl msg: {} without processor", sessionId, msg);
+ }
+ }
+
+ private void initProcessor(ToDeviceActorSessionMsg msg) {
+ if (processor == null) {
+ SessionMsg sessionMsg = (SessionMsg) msg.getSessionMsg();
+ if (sessionMsg.getSessionContext().getSessionType() == SessionType.SYNC) {
+ processor = new SyncMsgProcessor(systemContext, logger, sessionId);
+ } else {
+ processor = new ASyncMsgProcessor(systemContext, logger, sessionId);
+ }
+ }
+ }
+
+ public static class ActorCreator extends ContextBasedCreator<SessionActor> {
+ private static final long serialVersionUID = 1L;
+
+ private final SessionId sessionId;
+
+ public ActorCreator(ActorSystemContext context, SessionId sessionId) {
+ super(context);
+ this.sessionId = sessionId;
+ }
+
+ @Override
+ public SessionActor create() throws Exception {
+ return new SessionActor(context, sessionId);
+ }
+ }
+
+}
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
new file mode 100644
index 0000000..44eff16
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
@@ -0,0 +1,154 @@
+/**
+ * Copyright © 2016 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.session;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import akka.actor.*;
+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.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.SessionCloseMsg;
+import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
+
+public class SessionManagerActor extends ContextAwareActor {
+
+ private static final int INITIAL_SESSION_MAP_SIZE = 1024;
+
+ private final LoggingAdapter log = Logging.getLogger(getContext().system(), this);
+
+ private final Map<String, ActorRef> sessionActors;
+
+ public SessionManagerActor(ActorSystemContext systemContext) {
+ super(systemContext);
+ this.sessionActors = new HashMap<>(INITIAL_SESSION_MAP_SIZE);
+ }
+
+ @Override
+ public void onReceive(Object msg) throws Exception {
+ if (msg instanceof SessionAwareMsg) {
+ forwardToSessionActor((SessionAwareMsg) msg);
+ } else if (msg instanceof SessionTerminationMsg) {
+ onSessionTermination((SessionTerminationMsg) msg);
+ } else if (msg instanceof Terminated) {
+ onTermination((Terminated) msg);
+ } else if (msg instanceof SessionTimeoutMsg) {
+ onSessionTimeout((SessionTimeoutMsg) msg);
+ } else if (msg instanceof SessionCtrlMsg) {
+ onSessionCtrlMsg((SessionCtrlMsg) msg);
+ } else if (msg instanceof ClusterEventMsg) {
+ broadcast(msg);
+ }
+ }
+
+ private void broadcast(Object msg) {
+ sessionActors.values().stream().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+ }
+
+ private void onSessionTimeout(SessionTimeoutMsg msg) {
+ String sessionIdStr = msg.getSessionId().toUidStr();
+ ActorRef sessionActor = sessionActors.get(sessionIdStr);
+ if (sessionActor != null) {
+ sessionActor.tell(msg, ActorRef.noSender());
+ }
+ }
+
+ private void onSessionCtrlMsg(SessionCtrlMsg msg) {
+ String sessionIdStr = msg.getSessionId().toUidStr();
+ ActorRef sessionActor = sessionActors.get(sessionIdStr);
+ if (sessionActor != null) {
+ sessionActor.tell(msg, ActorRef.noSender());
+ }
+ }
+
+ private void onSessionTermination(SessionTerminationMsg msg) {
+ String sessionIdStr = msg.getId().toUidStr();
+ ActorRef sessionActor = sessionActors.remove(sessionIdStr);
+ if (sessionActor != null) {
+ log.debug("[{}] Removed session actor.", sessionIdStr);
+ //TODO: onSubscriptionUpdate device actor about session close;
+ } else {
+ log.debug("[{}] Session actor was already removed.", sessionIdStr);
+ }
+ }
+
+ private void forwardToSessionActor(SessionAwareMsg msg) {
+ if (msg instanceof ToDeviceSessionActorMsg || msg instanceof SessionCloseMsg) {
+ String sessionIdStr = msg.getSessionId().toUidStr();
+ ActorRef sessionActor = sessionActors.get(sessionIdStr);
+ if (sessionActor != null) {
+ sessionActor.tell(msg, ActorRef.noSender());
+ } else {
+ log.debug("[{}] Session actor was already removed.", sessionIdStr);
+ }
+ } else {
+ try {
+ getOrCreateSessionActor(msg.getSessionId()).tell(msg, self());
+ } catch (InvalidActorNameException e) {
+ log.info("Invalid msg : {}", msg);
+ }
+ }
+ }
+
+ private ActorRef getOrCreateSessionActor(SessionId sessionId) {
+ String sessionIdStr = sessionId.toUidStr();
+ ActorRef sessionActor = sessionActors.get(sessionIdStr);
+ if (sessionActor == null) {
+ log.debug("[{}] Creating session actor.", sessionIdStr);
+ sessionActor = context().actorOf(
+ Props.create(new SessionActor.ActorCreator(systemContext, sessionId)).withDispatcher(DefaultActorService.SESSION_DISPATCHER_NAME),
+ sessionIdStr);
+ sessionActors.put(sessionIdStr, sessionActor);
+ log.debug("[{}] Created session actor.", sessionIdStr);
+ }
+ return sessionActor;
+ }
+
+ private void onTermination(Terminated message) {
+ ActorRef terminated = message.actor();
+ if (terminated instanceof LocalActorRef) {
+ log.info("Removed actor: {}.", terminated);
+ //TODO: cleanup session actors map
+ } else {
+ throw new IllegalStateException("Remote actors are not supported!");
+ }
+ }
+
+ public static class ActorCreator extends ContextBasedCreator<SessionManagerActor> {
+ private static final long serialVersionUID = 1L;
+
+ public ActorCreator(ActorSystemContext context) {
+ super(context);
+ }
+
+ @Override
+ public SessionManagerActor create() throws Exception {
+ return new SessionManagerActor(context);
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SessionTerminationMsg.java b/application/src/main/java/org/thingsboard/server/actors/session/SessionTerminationMsg.java
new file mode 100644
index 0000000..365726b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SessionTerminationMsg.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 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.session;
+
+import org.thingsboard.server.actors.shared.ActorTerminationMsg;
+import org.thingsboard.server.common.data.id.SessionId;
+
+public class SessionTerminationMsg extends ActorTerminationMsg<SessionId> {
+
+ public SessionTerminationMsg(SessionId id) {
+ super(id);
+ }
+}
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
new file mode 100644
index 0000000..afb35ac
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
@@ -0,0 +1,93 @@
+/**
+ * Copyright © 2016 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.session;
+
+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.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 Optional<ServerAddress> currentTargetServer;
+ private boolean pendingResponse;
+
+ public SyncMsgProcessor(ActorSystemContext ctx, LoggingAdapter logger, SessionId sessionId) {
+ super(ctx, logger, sessionId);
+ }
+
+ @Override
+ protected void processToDeviceActorMsg(ActorContext ctx, ToDeviceActorSessionMsg msg) {
+ updateSessionCtx(msg, SessionType.SYNC);
+ pendingMsg = toDeviceMsg(msg);
+ pendingResponse = true;
+ currentTargetServer = forwardToAppActor(ctx, pendingMsg);
+ scheduleMsgWithDelay(ctx, new SessionTimeoutMsg(sessionId), getTimeout(systemContext, msg.getSessionMsg().getSessionContext()), ctx.parent());
+ }
+
+ public void processTimeoutMsg(ActorContext context, SessionTimeoutMsg msg) {
+ if (pendingResponse) {
+ try {
+ sessionCtx.onMsg(new SessionCloseMsg(sessionId, true));
+ } catch (SessionException e) {
+ logger.warning("Failed to push session close msg", e);
+ }
+ terminateSession(context, this.sessionId);
+ }
+ }
+
+ public void processToDeviceMsg(ActorContext context, ToDeviceMsg msg) {
+ try {
+ sessionCtx.onMsg(new BasicSessionActorToAdaptorMsg(this.sessionCtx, msg));
+ pendingResponse = false;
+ } catch (SessionException e) {
+ logger.warning("Failed to push session response msg", e);
+ }
+ terminateSession(context, this.sessionId);
+ }
+
+ @Override
+ public void processClusterEvent(ActorContext context, ClusterEventMsg msg) {
+ if (pendingResponse) {
+ Optional<ServerAddress> newTargetServer = forwardToAppActorIfAdressChanged(context, pendingMsg, currentTargetServer);
+ if (logger.isDebugEnabled()) {
+ if (!newTargetServer.equals(currentTargetServer)) {
+ if (newTargetServer.isPresent()) {
+ logger.debug("[{}] Forwarded msg to new server: {}", sessionId, newTargetServer.get());
+ } else {
+ logger.debug("[{}] Forwarded msg to local server.", sessionId);
+ }
+ }
+ }
+ currentTargetServer = newTargetServer;
+ }
+ }
+
+ private long getTimeout(ActorSystemContext ctx, SessionContext sessionCtx) {
+ return sessionCtx.getTimeout() > 0 ? sessionCtx.getTimeout() : ctx.getSyncSessionTimeout();
+ }
+}
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
new file mode 100644
index 0000000..1c7f687
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
@@ -0,0 +1,135 @@
+/**
+ * Copyright © 2016 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;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.actor.Scheduler;
+import akka.event.LoggingAdapter;
+import com.fasterxml.jackson.core.JsonProcessingException;
+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 {
+
+ protected final ActorSystemContext systemContext;
+ protected final LoggingAdapter logger;
+ protected final ObjectMapper mapper = new ObjectMapper();
+
+ protected AbstractContextAwareMsgProcessor(ActorSystemContext systemContext, LoggingAdapter logger) {
+ super();
+ this.systemContext = systemContext;
+ this.logger = logger;
+ }
+
+ protected ActorRef getAppActor() {
+ return systemContext.getAppActor();
+ }
+
+ protected Scheduler getScheduler() {
+ return systemContext.getScheduler();
+ }
+
+ protected ExecutionContextExecutor getSystemDispatcher() {
+ return systemContext.getActorSystem().dispatcher();
+ }
+
+ protected void schedulePeriodicMsgWithDelay(ActorContext ctx, Object msg, long delayInMs, long periodInMs) {
+ schedulePeriodicMsgWithDelay(ctx, msg, delayInMs, periodInMs, ctx.self());
+ }
+
+ protected void schedulePeriodicMsgWithDelay(ActorContext ctx, Object msg, long delayInMs, long periodInMs, ActorRef target) {
+ logger.debug("Scheduling periodic msg {} every {} ms with delay {} ms", msg, periodInMs, delayInMs);
+ getScheduler().schedule(Duration.create(delayInMs, TimeUnit.MILLISECONDS), Duration.create(periodInMs, TimeUnit.MILLISECONDS), target, msg, getSystemDispatcher(), null);
+ }
+
+
+ protected void scheduleMsgWithDelay(ActorContext ctx, Object msg, long delayInMs) {
+ scheduleMsgWithDelay(ctx, msg, delayInMs, ctx.self());
+ }
+
+ protected void scheduleMsgWithDelay(ActorContext ctx, Object msg, long delayInMs, ActorRef target) {
+ logger.debug("Scheduling msg {} with delay {} ms", msg, delayInMs);
+ 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 {
+ private final String clazz;
+ private final String name;
+ private final String configuration;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/ActorTerminationMsg.java b/application/src/main/java/org/thingsboard/server/actors/shared/ActorTerminationMsg.java
new file mode 100644
index 0000000..ed94e2c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/ActorTerminationMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 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;
+
+public abstract class ActorTerminationMsg<T> {
+
+ private final T id;
+
+ public ActorTerminationMsg(T id) {
+ super();
+ this.id = id;
+ }
+
+ public T getId() {
+ return id;
+ }
+
+}
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
new file mode 100644
index 0000000..2afd619
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright © 2016 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;
+
+import akka.actor.ActorContext;
+import akka.event.LoggingAdapter;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.stats.StatsPersistTick;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+
+public abstract class ComponentMsgProcessor<T> extends AbstractContextAwareMsgProcessor {
+
+ protected final TenantId tenantId;
+ protected final T entityId;
+
+ protected ComponentMsgProcessor(ActorSystemContext systemContext, LoggingAdapter logger, TenantId tenantId, T id) {
+ super(systemContext, logger);
+ this.tenantId = tenantId;
+ this.entityId = id;
+ }
+
+ public abstract void start() throws Exception;
+
+ public abstract void stop() throws Exception;
+
+ public abstract void onCreated(ActorContext context) throws Exception;
+
+ public abstract void onUpdate(ActorContext context) throws Exception;
+
+ public abstract void onActivate(ActorContext context) throws Exception;
+
+ public abstract void onSuspend(ActorContext context) throws Exception;
+
+ public abstract void onStop(ActorContext context) throws Exception;
+
+ public abstract void onClusterEventMsg(ClusterEventMsg msg) throws Exception;
+
+ public void scheduleStatsPersistTick(ActorContext context, long statsPersistFrequency) {
+ schedulePeriodicMsgWithDelay(context, new StatsPersistTick(), statsPersistFrequency, statsPersistFrequency);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java
new file mode 100644
index 0000000..c581c41
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright © 2016 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.plugin;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.plugin.PluginActor;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.service.DefaultActorService;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageDataIterable;
+import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.actor.Props;
+
+@Slf4j
+public abstract class PluginManager {
+
+ protected final ActorSystemContext systemContext;
+ protected final PluginService pluginService;
+ protected final Map<PluginId, ActorRef> pluginActors;
+
+ public PluginManager(ActorSystemContext systemContext) {
+ this.systemContext = systemContext;
+ this.pluginService = systemContext.getPluginService();
+ this.pluginActors = new HashMap<>();
+ }
+
+ public void init(ActorContext context) {
+ PageDataIterable<PluginMetaData> pluginIterator = new PageDataIterable<>(getFetchPluginsFunction(),
+ ContextAwareActor.ENTITY_PACK_LIMIT);
+ for (PluginMetaData plugin : pluginIterator) {
+ log.debug("[{}] Creating plugin actor", plugin.getId());
+ getOrCreatePluginActor(context, plugin.getId());
+ log.debug("Plugin actor created.");
+ }
+ }
+
+ abstract FetchFunction<PluginMetaData> getFetchPluginsFunction();
+
+ abstract TenantId getTenantId();
+
+ public ActorRef getOrCreatePluginActor(ActorContext context, PluginId pluginId) {
+ ActorRef pluginActor = pluginActors.get(pluginId);
+ if (pluginActor == null) {
+ pluginActor = context.actorOf(Props.create(new PluginActor.ActorCreator(systemContext, getTenantId(), pluginId))
+ .withDispatcher(DefaultActorService.PLUGIN_DISPATCHER_NAME), pluginId.toString());
+ pluginActors.put(pluginId, pluginActor);
+ }
+ return pluginActor;
+ }
+
+ public void broadcast(Object msg) {
+ pluginActors.values().stream().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+ }
+
+ public void remove(PluginId id) {
+ pluginActors.remove(id);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java
new file mode 100644
index 0000000..d8b58a0
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016 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.plugin;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.dao.plugin.BasePluginService;
+import org.thingsboard.server.dao.plugin.PluginService;
+
+public class SystemPluginManager extends PluginManager {
+
+ public SystemPluginManager(ActorSystemContext systemContext) {
+ super(systemContext);
+ }
+
+ @Override
+ FetchFunction<PluginMetaData> getFetchPluginsFunction() {
+ return link -> pluginService.findSystemPlugins(link);
+ }
+
+ @Override
+ TenantId getTenantId() {
+ return BasePluginService.SYSTEM_TENANT;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/TenantPluginManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/TenantPluginManager.java
new file mode 100644
index 0000000..dbff415
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/TenantPluginManager.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016 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.plugin;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+
+public class TenantPluginManager extends PluginManager {
+
+ private final TenantId tenantId;
+
+ public TenantPluginManager(ActorSystemContext systemContext, TenantId tenantId) {
+ super(systemContext);
+ this.tenantId = tenantId;
+ }
+
+ @Override
+ FetchFunction<PluginMetaData> getFetchPluginsFunction() {
+ return link -> pluginService.findTenantPlugins(tenantId, link);
+ }
+
+ @Override
+ TenantId getTenantId() {
+ return tenantId;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java
new file mode 100644
index 0000000..67d44e9
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java
@@ -0,0 +1,126 @@
+/**
+ * Copyright © 2016 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.rule;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.actor.Props;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.rule.RuleActor;
+import org.thingsboard.server.actors.rule.RuleActorChain;
+import org.thingsboard.server.actors.rule.RuleActorMetaData;
+import org.thingsboard.server.actors.rule.SimpleRuleActorChain;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.service.DefaultActorService;
+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.data.page.PageDataIterable.FetchFunction;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.dao.rule.RuleService;
+
+import java.util.*;
+
+public abstract class RuleManager {
+
+ protected static final Logger logger = LoggerFactory.getLogger(RuleManager.class);
+
+ protected final ActorSystemContext systemContext;
+ protected final RuleService ruleService;
+ protected final Map<RuleId, ActorRef> ruleActors;
+ protected final TenantId tenantId;
+
+ Map<RuleMetaData, RuleActorMetaData> ruleMap = new HashMap<>();
+ private RuleActorChain ruleChain;
+
+ public RuleManager(ActorSystemContext systemContext, TenantId tenantId) {
+ this.systemContext = systemContext;
+ this.ruleService = systemContext.getRuleService();
+ this.ruleActors = new HashMap<>();
+ this.tenantId = tenantId;
+ }
+
+ public void init(ActorContext context) {
+ PageDataIterable<RuleMetaData> ruleIterator = new PageDataIterable<>(getFetchRulesFunction(),
+ ContextAwareActor.ENTITY_PACK_LIMIT);
+ ruleMap = new HashMap<>();
+
+ for (RuleMetaData rule : ruleIterator) {
+ logger.debug("[{}] Creating rule actor {}", rule.getId(), rule);
+ ActorRef ref = getOrCreateRuleActor(context, rule.getId());
+ RuleActorMetaData actorMd = RuleActorMetaData.systemRule(rule.getId(), rule.getWeight(), ref);
+ ruleMap.put(rule, actorMd);
+ logger.debug("[{}] Rule actor created.", rule.getId());
+ }
+
+ refreshRuleChain();
+ }
+
+ public Optional<ActorRef> update(ActorContext context, RuleId ruleId, ComponentLifecycleEvent event) {
+ RuleMetaData rule = null;
+ if (event != ComponentLifecycleEvent.DELETED) {
+ rule = systemContext.getRuleService().findRuleById(ruleId);
+ }
+ if (rule == null) {
+ rule = ruleMap.keySet().stream().filter(r -> r.getId().equals(ruleId)).findFirst().orElse(null);
+ rule.setState(ComponentLifecycleState.SUSPENDED);
+ }
+ if (rule != null) {
+ RuleActorMetaData actorMd = ruleMap.get(rule);
+ if (actorMd == null) {
+ ActorRef ref = getOrCreateRuleActor(context, rule.getId());
+ actorMd = RuleActorMetaData.systemRule(rule.getId(), rule.getWeight(), ref);
+ ruleMap.put(rule, actorMd);
+ }
+ refreshRuleChain();
+ return Optional.of(actorMd.getActorRef());
+ } else {
+ logger.warn("[{}] Can't process unknown rule!", rule.getId());
+ return Optional.empty();
+ }
+ }
+
+ abstract FetchFunction<RuleMetaData> getFetchRulesFunction();
+
+ public ActorRef getOrCreateRuleActor(ActorContext context, RuleId ruleId) {
+ ActorRef ruleActor = ruleActors.get(ruleId);
+ if (ruleActor == null) {
+ ruleActor = context.actorOf(Props.create(new RuleActor.ActorCreator(systemContext, tenantId, ruleId))
+ .withDispatcher(DefaultActorService.RULE_DISPATCHER_NAME), ruleId.toString());
+ ruleActors.put(ruleId, ruleActor);
+ }
+ return ruleActor;
+ }
+
+ public RuleActorChain getRuleChain() {
+ return ruleChain;
+ }
+
+
+ private void refreshRuleChain() {
+ Set<RuleActorMetaData> activeRuleSet = new HashSet<>();
+ for (Map.Entry<RuleMetaData, RuleActorMetaData> rule : ruleMap.entrySet()) {
+ if (rule.getKey().getState() == ComponentLifecycleState.ACTIVE) {
+ activeRuleSet.add(rule.getValue());
+ }
+ }
+ ruleChain = new SimpleRuleActorChain(activeRuleSet);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/rule/SystemRuleManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rule/SystemRuleManager.java
new file mode 100644
index 0000000..6d56832
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/rule/SystemRuleManager.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 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.rule;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.dao.model.ModelConstants;
+
+public class SystemRuleManager extends RuleManager {
+
+ public SystemRuleManager(ActorSystemContext systemContext) {
+ super(systemContext, new TenantId(ModelConstants.NULL_UUID));
+ }
+
+ @Override
+ FetchFunction<RuleMetaData> getFetchRulesFunction() {
+ return link -> ruleService.findSystemRules(link);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/rule/TenantRuleManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rule/TenantRuleManager.java
new file mode 100644
index 0000000..700614d
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/rule/TenantRuleManager.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016 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.rule;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+
+public class TenantRuleManager extends RuleManager {
+
+ public TenantRuleManager(ActorSystemContext systemContext, TenantId tenantId) {
+ super(systemContext, tenantId);
+ }
+
+ @Override
+ FetchFunction<RuleMetaData> getFetchRulesFunction() {
+ return link -> ruleService.findTenantRules(tenantId, link);
+ }
+
+}
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
new file mode 100644
index 0000000..2305a35
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/SessionTimeoutMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.SessionId;
+
+import java.io.Serializable;
+
+@Data
+public class SessionTimeoutMsg implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private final SessionId sessionId;
+}
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
new file mode 100644
index 0000000..8b59f70
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright © 2016 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.stats;
+
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.data.DataConstants;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+
+public class StatsActor extends ContextAwareActor {
+
+ private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ public StatsActor(ActorSystemContext context) {
+ super(context);
+ }
+
+ @Override
+ public void onReceive(Object msg) throws Exception {
+ logger.debug("Received message: {}", msg);
+ if (msg instanceof StatsPersistMsg) {
+ try {
+ onStatsPersistMsg((StatsPersistMsg) msg);
+ } catch (Exception e) {
+ logger.warning("Failed to persist statistics: {}", msg, e);
+ }
+ }
+ }
+
+ public void onStatsPersistMsg(StatsPersistMsg msg) throws Exception {
+ Event event = new Event();
+ event.setEntityId(msg.getEntityId());
+ event.setTenantId(msg.getTenantId());
+ event.setType(DataConstants.STATS);
+ event.setBody(toBodyJson(systemContext.getDiscoveryService().getCurrentServer().getServerAddress(), msg.getMessagesProcessed(), msg.getErrorsOccurred()));
+ systemContext.getEventService().save(event);
+ }
+
+ private JsonNode toBodyJson(ServerAddress server, long messagesProcessed, long errorsOccurred) {
+ return mapper.createObjectNode().put("server", server.toString()).put("messagesProcessed", messagesProcessed).put("errorsOccurred", errorsOccurred);
+ }
+
+ public static class ActorCreator extends ContextBasedCreator<StatsActor> {
+ private static final long serialVersionUID = 1L;
+
+ public ActorCreator(ActorSystemContext context) {
+ super(context);
+ }
+
+ @Override
+ public StatsActor create() throws Exception {
+ return new StatsActor(context);
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistMsg.java b/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistMsg.java
new file mode 100644
index 0000000..437ef96
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistMsg.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 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.stats;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.ToString;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+@AllArgsConstructor
+@Getter
+@ToString
+public final class StatsPersistMsg {
+ private long messagesProcessed;
+ private long errorsOccurred;
+ private TenantId tenantId;
+ private EntityId entityId;
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistTick.java b/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistTick.java
new file mode 100644
index 0000000..61c9fc6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistTick.java
@@ -0,0 +1,18 @@
+/**
+ * Copyright © 2016 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.stats;
+
+public final class StatsPersistTick {}
diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/RuleChainDeviceMsg.java b/application/src/main/java/org/thingsboard/server/actors/tenant/RuleChainDeviceMsg.java
new file mode 100644
index 0000000..7d41c7b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/tenant/RuleChainDeviceMsg.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016 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.tenant;
+
+import org.thingsboard.server.actors.rule.RuleActorChain;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+
+public class RuleChainDeviceMsg {
+
+ private final ToDeviceActorMsg toDeviceActorMsg;
+ private final RuleActorChain ruleChain;
+
+ public RuleChainDeviceMsg(ToDeviceActorMsg toDeviceActorMsg, RuleActorChain ruleChain) {
+ super();
+ this.toDeviceActorMsg = toDeviceActorMsg;
+ this.ruleChain = ruleChain;
+ }
+
+ public ToDeviceActorMsg getToDeviceActorMsg() {
+ return toDeviceActorMsg;
+ }
+
+ public RuleActorChain getRuleChain() {
+ return ruleChain;
+ }
+
+}
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
new file mode 100644
index 0000000..965c652
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
@@ -0,0 +1,184 @@
+/**
+ * Copyright © 2016 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.tenant;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+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.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.common.data.id.DeviceId;
+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.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;
+
+public class TenantActor extends ContextAwareActor {
+
+ private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
+
+ 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);
+ this.tenantId = tenantId;
+ this.ruleManager = new TenantRuleManager(systemContext, tenantId);
+ this.pluginManager = new TenantPluginManager(systemContext, tenantId);
+ this.deviceActors = new HashMap<>();
+ }
+
+ @Override
+ public void preStart() {
+ logger.info("[{}] Starting tenant actor.", tenantId);
+ try {
+ ruleManager.init(this.context());
+ pluginManager.init(this.context());
+ logger.info("[{}] Tenant actor started.", tenantId);
+ } catch (Exception e) {
+ logger.error(e, "[{}] Unknown failure", tenantId);
+ }
+ }
+
+ @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);
+ }
+ }
+
+ private void broadcast(Object msg) {
+ pluginManager.broadcast(msg);
+ deviceActors.values().stream().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+ }
+
+ private void onToDeviceActorMsg(ToDeviceActorMsg msg) {
+ getOrCreateDeviceActor(msg.getDeviceId()).tell(msg, ActorRef.noSender());
+ }
+
+ private void onToDeviceActorMsg(ToDeviceActorNotificationMsg msg) {
+ getOrCreateDeviceActor(msg.getDeviceId()).tell(msg, ActorRef.noSender());
+ }
+
+ private void onToRuleMsg(ToRuleActorMsg msg) {
+ ActorRef target = ruleManager.getOrCreateRuleActor(this.context(), msg.getRuleId());
+ target.tell(msg, ActorRef.noSender());
+ }
+
+ 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 onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
+ if (msg.getPluginId().isPresent()) {
+ ActorRef pluginActor = pluginManager.getOrCreatePluginActor(this.context(), msg.getPluginId().get());
+ pluginActor.tell(msg, ActorRef.noSender());
+ } else if (msg.getRuleId().isPresent()) {
+ ActorRef target;
+ Optional<ActorRef> ref = ruleManager.update(this.context(), msg.getRuleId().get(), msg.getEvent());
+ if (ref.isPresent()) {
+ target = ref.get();
+ } else {
+ logger.debug("Failed to find actor for rule: [{}]", msg.getRuleId());
+ return;
+ }
+ target.tell(msg, ActorRef.noSender());
+ } else {
+ logger.debug("[{}] Invalid component lifecycle msg.", tenantId);
+ }
+ }
+
+ private void onPluginTerminated(PluginTerminationMsg msg) {
+ pluginManager.remove(msg.getId());
+ }
+
+ private void process(RuleChainDeviceMsg msg) {
+ ToDeviceActorMsg toDeviceActorMsg = msg.getToDeviceActorMsg();
+ ActorRef deviceActor = getOrCreateDeviceActor(toDeviceActorMsg.getDeviceId());
+ RuleActorChain chain = new ComplexRuleActorChain(msg.getRuleChain(), ruleManager.getRuleChain());
+ deviceActor.tell(new RuleChainDeviceMsg(toDeviceActorMsg, chain), context().self());
+ }
+
+ private ActorRef getOrCreateDeviceActor(DeviceId deviceId) {
+ ActorRef deviceActor = deviceActors.get(deviceId);
+ if (deviceActor == null) {
+ deviceActor = context().actorOf(Props.create(new DeviceActor.ActorCreator(systemContext, tenantId, deviceId))
+ .withDispatcher(DefaultActorService.CORE_DISPATCHER_NAME), deviceId.toString());
+ deviceActors.put(deviceId, deviceActor);
+ }
+ return deviceActor;
+ }
+
+ public static class ActorCreator extends ContextBasedCreator<TenantActor> {
+ private static final long serialVersionUID = 1L;
+
+ private final TenantId tenantId;
+
+ public ActorCreator(ActorSystemContext context, TenantId tenantId) {
+ super(context);
+ this.tenantId = tenantId;
+ }
+
+ @Override
+ public TenantActor create() throws Exception {
+ return new TenantActor(context, tenantId);
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/CurrentServerInstanceService.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/CurrentServerInstanceService.java
new file mode 100644
index 0000000..5100bf8
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/CurrentServerInstanceService.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright © 2016 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.cluster.discovery;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+import org.thingsboard.server.gen.discovery.ServerInstanceProtos;
+
+import javax.annotation.PostConstruct;
+
+import static org.thingsboard.server.utils.MiscUtils.missingProperty;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Service
+@Slf4j
+public class CurrentServerInstanceService implements ServerInstanceService {
+
+ @Value("${rpc.bind_host}")
+ private String rpcHost;
+ @Value("${rpc.bind_port}")
+ private Integer rpcPort;
+
+ private ServerInstance self;
+
+ @PostConstruct
+ public void init() {
+ Assert.hasLength(rpcHost, missingProperty("rpc.bind_host"));
+ Assert.notNull(rpcPort, missingProperty("rpc.bind_port"));
+
+ self = new ServerInstance(ServerInstanceProtos.ServerInfo.newBuilder().setHost(rpcHost).setPort(rpcPort).setTs(System.currentTimeMillis()).build());
+ log.info("Current server instance: [{};{}]", self.getHost(), self.getPort());
+ }
+
+ @Override
+ public ServerInstance getSelf() {
+ return self;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DiscoveryService.java
new file mode 100644
index 0000000..bd6fe7b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DiscoveryService.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016 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.cluster.discovery;
+
+import java.util.List;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface DiscoveryService {
+
+ void publishCurrentServer();
+
+ void unpublishCurrentServer();
+
+ ServerInstance getCurrentServer();
+
+ List<ServerInstance> getOtherServers();
+
+ boolean addListener(DiscoveryServiceListener listener);
+
+ boolean removeListener(DiscoveryServiceListener listener);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DiscoveryServiceListener.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DiscoveryServiceListener.java
new file mode 100644
index 0000000..a0b8eba
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DiscoveryServiceListener.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 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.cluster.discovery;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface DiscoveryServiceListener {
+
+ void onServerAdded(ServerInstance server);
+
+ void onServerUpdated(ServerInstance server);
+
+ void onServerRemoved(ServerInstance server);
+}
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
new file mode 100644
index 0000000..fdf9fdd
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DummyDiscoveryService.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright © 2016 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.cluster.discovery;
+
+import lombok.extern.slf4j.Slf4j;
+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;
+import java.util.List;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Service
+@ConditionalOnProperty(prefix = "zk", value = "enabled", havingValue = "false", matchIfMissing = true)
+@Slf4j
+@DependsOn("environmentLogService")
+public class DummyDiscoveryService implements DiscoveryService {
+
+ @Autowired
+ private ServerInstanceService serverInstance;
+
+ @PostConstruct
+ public void init() {
+ log.info("Initializing...");
+ }
+
+ @Override
+ public void publishCurrentServer() {
+
+ }
+
+ @Override
+ public void unpublishCurrentServer() {
+
+ }
+
+ @Override
+ public ServerInstance getCurrentServer() {
+ return serverInstance.getSelf();
+ }
+
+ @Override
+ public List<ServerInstance> getOtherServers() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean addListener(DiscoveryServiceListener listener) {
+ return false;
+ }
+
+ @Override
+ public boolean removeListener(DiscoveryServiceListener listener) {
+ return false;
+ }
+}
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
new file mode 100644
index 0000000..5f9d31b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstance.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright © 2016 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.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;
+
+/**
+ * @author Andrew Shvayka
+ */
+@ToString
+@EqualsAndHashCode(exclude = {"serverInfo", "serverAddress"})
+public final class ServerInstance implements Comparable<ServerInstance> {
+
+ @Getter(AccessLevel.PACKAGE)
+ private final ServerInfo serverInfo;
+ @Getter
+ private final String host;
+ @Getter
+ private final int port;
+ @Getter
+ private final ServerAddress serverAddress;
+
+ public ServerInstance(ServerInfo serverInfo) {
+ this.serverInfo = serverInfo;
+ this.host = serverInfo.getHost();
+ this.port = serverInfo.getPort();
+ this.serverAddress = new ServerAddress(host, port);
+ }
+
+ @Override
+ public int compareTo(ServerInstance o) {
+ return this.serverAddress.compareTo(o.serverAddress);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstanceService.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstanceService.java
new file mode 100644
index 0000000..af023ed
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstanceService.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 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.cluster.discovery;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ServerInstanceService {
+
+ ServerInstance getSelf();
+}
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
new file mode 100644
index 0000000..29e9b3c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
@@ -0,0 +1,207 @@
+/**
+ * Copyright © 2016 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.cluster.discovery;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.cache.ChildData;
+import org.apache.curator.framework.recipes.cache.PathChildrenCache;
+import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
+import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
+import org.apache.curator.retry.RetryForever;
+import org.apache.curator.utils.CloseableUtils;
+import org.apache.zookeeper.CreateMode;
+import org.springframework.beans.factory.annotation.Autowired;
+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.stereotype.Service;
+import org.springframework.util.Assert;
+import org.thingsboard.server.gen.discovery.ServerInstanceProtos.ServerInfo;
+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.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Collectors;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Service
+@ConditionalOnProperty(prefix = "zk", value = "enabled", havingValue = "true", matchIfMissing = false)
+@Slf4j
+public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheListener, ApplicationListener<ApplicationReadyEvent> {
+
+ @Value("${zk.url}")
+ private String zkUrl;
+ @Value("${zk.retry_interval_ms}")
+ private Integer zkRetryInterval;
+ @Value("${zk.connection_timeout_ms}")
+ private Integer zkConnectionTimeout;
+ @Value("${zk.session_timeout_ms}")
+ private Integer zkSessionTimeout;
+ @Value("${zk.zk_dir}")
+ private String zkDir;
+
+ private String zkNodesDir;
+
+ @Autowired
+ private ServerInstanceService serverInstance;
+
+ private final List<DiscoveryServiceListener> listeners = new CopyOnWriteArrayList<>();
+
+ private CuratorFramework client;
+ private PathChildrenCache cache;
+ private String nodePath;
+
+
+ @PostConstruct
+ public void init() {
+ log.info("Initializing...");
+ Assert.hasLength(zkUrl, MiscUtils.missingProperty("zk.url"));
+ Assert.notNull(zkRetryInterval, MiscUtils.missingProperty("zk.retry_interval_ms"));
+ Assert.notNull(zkConnectionTimeout, MiscUtils.missingProperty("zk.connection_timeout_ms"));
+ Assert.notNull(zkSessionTimeout, MiscUtils.missingProperty("zk.session_timeout_ms"));
+
+ log.info("Initializing discovery service using ZK connect string: {}", zkUrl);
+
+ zkNodesDir = zkDir + "/nodes";
+ try {
+ client = CuratorFrameworkFactory.newClient(zkUrl, zkSessionTimeout, zkConnectionTimeout, new RetryForever(zkRetryInterval));
+ client.start();
+ client.blockUntilConnected();
+ cache = new PathChildrenCache(client, zkNodesDir, true);
+ cache.getListenable().addListener(this);
+ cache.start();
+ } catch (Exception e) {
+ log.error("Failed to connect to ZK: {}", e.getMessage(), e);
+ CloseableUtils.closeQuietly(client);
+ throw new RuntimeException(e);
+ }
+ }
+
+ @PreDestroy
+ public void destroy() {
+ unpublishCurrentServer();
+ CloseableUtils.closeQuietly(client);
+ log.info("Stopped discovery service");
+ }
+
+ @Override
+ public void publishCurrentServer() {
+ try {
+ ServerInstance self = this.serverInstance.getSelf();
+ log.info("[{}:{}] Creating ZK node for current instance", self.getHost(), self.getPort());
+ nodePath = client.create()
+ .creatingParentsIfNeeded()
+ .withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(zkNodesDir + "/", self.getServerInfo().toByteArray());
+ log.info("[{}:{}] Created ZK node for current instance: {}", self.getHost(), self.getPort(), nodePath);
+ } catch (Exception e) {
+ log.error("Failed to create ZK node", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void unpublishCurrentServer() {
+ try {
+ if (nodePath != null) {
+ client.delete().forPath(nodePath);
+ }
+ } catch (Exception e) {
+ log.error("Failed to delete ZK node {}", nodePath, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public ServerInstance getCurrentServer() {
+ return serverInstance.getSelf();
+ }
+
+ @Override
+ public List<ServerInstance> getOtherServers() {
+ return cache.getCurrentData().stream()
+ .filter(cd -> !cd.getPath().equals(nodePath))
+ .map(cd -> {
+ try {
+ return new ServerInstance(ServerInfo.parseFrom(cd.getData()));
+ } catch (InvalidProtocolBufferException e) {
+ log.error("Failed to decode ZK node", e);
+ throw new RuntimeException(e);
+ }
+ })
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public boolean addListener(DiscoveryServiceListener listener) {
+ return listeners.add(listener);
+ }
+
+ @Override
+ public boolean removeListener(DiscoveryServiceListener listener) {
+ return listeners.remove(listener);
+ }
+
+ @Override
+ public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
+ publishCurrentServer();
+ getOtherServers().stream().forEach(
+ server -> log.info("Found active server: [{}:{}]", server.getHost(), server.getPort())
+ );
+ }
+
+ @Override
+ public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
+ ChildData data = pathChildrenCacheEvent.getData();
+ if (data == null) {
+ log.debug("Ignoring {} due to empty child data", pathChildrenCacheEvent);
+ return;
+ } else if (data.getData() == null) {
+ log.debug("Ignoring {} due to empty child's data", pathChildrenCacheEvent);
+ return;
+ } else if (nodePath != null && nodePath.equals(data.getPath())) {
+ log.debug("Ignoring event about current server {}", pathChildrenCacheEvent);
+ return;
+ }
+ ServerInstance instance;
+ try {
+ instance = new ServerInstance(ServerInfo.parseFrom(data.getData()));
+ } catch (IOException 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:
+ listeners.stream().forEach(listener -> listener.onServerAdded(instance));
+ break;
+ case CHILD_UPDATED:
+ listeners.stream().forEach(listener -> listener.onServerUpdated(instance));
+ break;
+ case CHILD_REMOVED:
+ listeners.stream().forEach(listener -> listener.onServerRemoved(instance));
+ break;
+ }
+ }
+}
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
new file mode 100644
index 0000000..352d89e
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016 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.cluster.routing;
+
+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;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ClusterRoutingService {
+
+ ServerAddress getCurrentServer();
+
+ Optional<ServerAddress> resolve(UUIDBased entityId);
+
+}
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
new file mode 100644
index 0000000..3c9ecf8
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright © 2016 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.cluster.routing;
+
+import com.google.common.hash.HashCode;
+import com.google.common.hash.HashFunction;
+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.springframework.util.Assert;
+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;
+import org.thingsboard.server.service.cluster.discovery.ServerInstance;
+import org.thingsboard.server.utils.MiscUtils;
+
+import javax.annotation.PostConstruct;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentNavigableMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+
+/**
+ * Cluster service implementation based on consistent hash ring
+ */
+
+@Service
+@Slf4j
+public class ConsistentClusterRoutingService implements ClusterRoutingService, DiscoveryServiceListener {
+
+ @Autowired
+ private DiscoveryService discoveryService;
+
+ @Value("${cluster.hash_function_name}")
+ private String hashFunctionName;
+ @Value("${cluster.vitrual_nodes_size}")
+ private Integer virtualNodesSize;
+
+ private ServerInstance currentServer;
+
+ private HashFunction hashFunction;
+
+ private final ConcurrentNavigableMap<Long, ServerInstance> circle =
+ new ConcurrentSkipListMap<>();
+
+ @PostConstruct
+ public void init() {
+ log.info("Initializing Cluster routing service!");
+ hashFunction = MiscUtils.forName(hashFunctionName);
+ discoveryService.addListener(this);
+ this.currentServer = discoveryService.getCurrentServer();
+ addNode(discoveryService.getCurrentServer());
+ for (ServerInstance instance : discoveryService.getOtherServers()) {
+ addNode(instance);
+ }
+ logCircle();
+ log.info("Cluster routing service initialized!");
+ }
+
+ @Override
+ public ServerAddress getCurrentServer() {
+ return discoveryService.getCurrentServer().getServerAddress();
+ }
+
+ @Override
+ public Optional<ServerAddress> resolve(UUIDBased entityId) {
+ Assert.notNull(entityId);
+ if (circle.isEmpty()) {
+ return Optional.empty();
+ }
+ Long hash = hashFunction.newHasher().putLong(entityId.getId().getMostSignificantBits())
+ .putLong(entityId.getId().getLeastSignificantBits()).hash().asLong();
+ if (!circle.containsKey(hash)) {
+ ConcurrentNavigableMap<Long, ServerInstance> tailMap =
+ circle.tailMap(hash);
+ hash = tailMap.isEmpty() ?
+ circle.firstKey() : tailMap.firstKey();
+ }
+ ServerInstance result = circle.get(hash);
+ if (!currentServer.equals(result)) {
+ return Optional.of(result.getServerAddress());
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public void onServerAdded(ServerInstance server) {
+ log.debug("On server added event: {}", server);
+ addNode(server);
+ logCircle();
+ }
+
+ @Override
+ public void onServerUpdated(ServerInstance server) {
+ log.debug("Ignoring server onUpdate event: {}", server);
+ }
+
+ @Override
+ public void onServerRemoved(ServerInstance server) {
+ log.debug("On server removed event: {}", server);
+ removeNode(server);
+ logCircle();
+ }
+
+ private void addNode(ServerInstance instance) {
+ for (int i = 0; i < virtualNodesSize; i++) {
+ circle.put(hash(instance, i).asLong(), instance);
+ }
+ }
+
+ private void removeNode(ServerInstance instance) {
+ for (int i = 0; i < virtualNodesSize; i++) {
+ circle.remove(hash(instance, i).asLong());
+ }
+ }
+
+ private HashCode hash(ServerInstance instance, int i) {
+ return hashFunction.newHasher().putString(instance.getHost(), MiscUtils.UTF8).putInt(instance.getPort()).putInt(i).hash();
+ }
+
+ private void logCircle() {
+ log.trace("Consistent Hash Circle Start");
+ circle.entrySet().stream().forEach((e) -> log.debug("{} -> {}", e.getKey(), e.getValue().getServerAddress()));
+ log.trace("Consistent Hash Circle End");
+ }
+
+}
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
new file mode 100644
index 0000000..3755c0b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterGrpcService.java
@@ -0,0 +1,273 @@
+/**
+ * Copyright © 2016 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.cluster.rpc;
+
+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.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 javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.io.IOException;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Service
+@Slf4j
+public class ClusterGrpcService extends ClusterRpcServiceGrpc.ClusterRpcServiceImplBase implements ClusterRpcService {
+
+ @Autowired
+ private ServerInstanceService instanceService;
+
+ private RpcMsgListener listener;
+
+ private Server server;
+
+ private ServerInstance instance;
+
+ private ConcurrentMap<UUID, RpcSessionCreationFuture> pendingSessionMap = new ConcurrentHashMap<>();
+
+ public void init(RpcMsgListener listener) {
+ this.listener = listener;
+ log.info("Initializing RPC service!");
+ instance = instanceService.getSelf();
+ server = ServerBuilder.forPort(instance.getPort()).addService(this).build();
+ log.info("Going to start RPC server using port: {}", instance.getPort());
+ try {
+ server.start();
+ } catch (IOException e) {
+ log.error("Failed to start RPC server!", e);
+ throw new RuntimeException("Failed to start RPC server!");
+ }
+ log.info("RPC service initialized!");
+ }
+
+ @Override
+ public void onSessionCreated(UUID msgUid, StreamObserver<ClusterAPIProtos.ToRpcServerMessage> msg) {
+ RpcSessionCreationFuture future = pendingSessionMap.remove(msgUid);
+ if (future != null) {
+ try {
+ future.onMsg(msg);
+ } catch (InterruptedException e) {
+ log.warn("Failed to report created session!");
+ }
+ } else {
+ log.warn("Failed to lookup pending session!");
+ }
+ }
+
+ @Override
+ public StreamObserver<ClusterAPIProtos.ToRpcServerMessage> handlePluginMsgs(StreamObserver<ClusterAPIProtos.ToRpcServerMessage> responseObserver) {
+ log.info("Processing new session.");
+ return createSession(new RpcSessionCreateRequestMsg(UUID.randomUUID(), null, responseObserver));
+ }
+
+ @PreDestroy
+ public void stop() {
+ if (server != null) {
+ log.info("Going to onStop RPC server");
+ server.shutdownNow();
+ try {
+ server.awaitTermination();
+ log.info("RPC server stopped!");
+ } catch (InterruptedException e) {
+ log.warn("Failed to onStop RPC server!");
+ }
+ }
+ }
+
+ @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);
+ }
+
+ @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);
+ try {
+ StreamObserver<ClusterAPIProtos.ToRpcServerMessage> observer = future.get();
+ log.info("Processed new session.");
+ return observer;
+ } catch (Exception e) {
+ log.info("Failed to process session.", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ 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());
+ }
+
+ private static ClusterAPIProtos.Uid toUid(UUID uuid) {
+ return ClusterAPIProtos.Uid.newBuilder().setPluginUuidMsb(uuid.getMostSignificantBits()).setPluginUuidLsb(
+ uuid.getLeastSignificantBits()).build();
+ }
+
+ private static ClusterAPIProtos.ToDeviceSessionActorRpcMessage toProtoMsg(ToDeviceSessionActorMsg msg) {
+ return ClusterAPIProtos.ToDeviceSessionActorRpcMessage.newBuilder().setData(
+ ByteString.copyFrom(SerializationUtils.serialize(msg))
+ ).build();
+ }
+
+}
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
new file mode 100644
index 0000000..0f13164
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterRpcService.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright © 2016 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.cluster.rpc;
+
+import io.grpc.stub.StreamObserver;
+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;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ClusterRpcService {
+
+ void init(RpcMsgListener listener);
+
+ void tell(ServerAddress serverAddress, ToDeviceActorMsg toForward);
+
+ void tell(ServerAddress serverAddress, ToDeviceSessionActorMsg toForward);
+
+ void tell(ServerAddress serverAddress, ToDeviceActorNotificationMsg toForward);
+
+ void tell(ServerAddress serverAddress, ToDeviceRpcRequestPluginMsg toForward);
+
+ void tell(ServerAddress serverAddress, ToPluginRpcResponseDeviceMsg toForward);
+
+ void tell(PluginRpcMsg toForward);
+
+ void broadcast(ToAllNodesMsg msg);
+
+ void onSessionCreated(UUID msgUid, StreamObserver<ClusterAPIProtos.ToRpcServerMessage> inputStream);
+}
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
new file mode 100644
index 0000000..0f53839
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright © 2016 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.cluster.rpc;
+
+import io.grpc.stub.StreamObserver;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+
+import java.io.Closeable;
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+@Slf4j
+final public 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 boolean connected;
+ private ServerAddress remoteServer;
+
+ public GrpcSession(GrpcSessionListener listener) {
+ this(null, listener);
+ }
+
+ public GrpcSession(ServerAddress remoteServer, GrpcSessionListener listener) {
+ this.sessionId = UUID.randomUUID();
+ this.listener = listener;
+ if (remoteServer != null) {
+ this.client = true;
+ this.connected = true;
+ this.remoteServer = remoteServer;
+ } else {
+ this.client = false;
+ }
+ }
+
+ public void initInputStream() {
+ this.inputStream = new StreamObserver<ClusterAPIProtos.ToRpcServerMessage>() {
+ @Override
+ public void onNext(ClusterAPIProtos.ToRpcServerMessage msg) {
+ if (!connected) {
+ if (msg.hasConnectMsg()) {
+ connected = true;
+ ClusterAPIProtos.ServerAddress rpcAddress = msg.getConnectMsg().getServerAddress();
+ remoteServer = new ServerAddress(rpcAddress.getHost(), rpcAddress.getPort());
+ listener.onConnected(GrpcSession.this);
+ }
+ }
+ if (connected) {
+ 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());
+ }
+ }
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ listener.onError(GrpcSession.this, t);
+ }
+
+ @Override
+ public void onCompleted() {
+ outputStream.onCompleted();
+ listener.onDisconnected(GrpcSession.this);
+ }
+ };
+ }
+
+ public void initOutputStream() {
+ if (client) {
+ listener.onConnected(GrpcSession.this);
+ }
+ }
+
+ public void sendMsg(ClusterAPIProtos.ToRpcServerMessage msg) {
+ outputStream.onNext(msg);
+ }
+
+ public void onError(Throwable t) {
+ outputStream.onError(t);
+ }
+
+ @Override
+ public void close() {
+ try {
+ outputStream.onCompleted();
+ } catch (IllegalStateException e) {
+ log.debug("[{}] Failed to close output stream: {}", sessionId, e.getMessage());
+ }
+ }
+}
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
new file mode 100644
index 0000000..f80b394
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSessionListener.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016 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.cluster.rpc;
+
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface GrpcSessionListener {
+
+ void onConnected(GrpcSession session);
+
+ 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 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
new file mode 100644
index 0000000..982803a
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcMsgListener.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016 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.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.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);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcSessionCreationFuture.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcSessionCreationFuture.java
new file mode 100644
index 0000000..4966553
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcSessionCreationFuture.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 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.cluster.rpc;
+
+import io.grpc.stub.StreamObserver;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+
+import java.util.concurrent.*;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RpcSessionCreationFuture implements Future<StreamObserver<ClusterAPIProtos.ToRpcServerMessage>> {
+
+ private final BlockingQueue<StreamObserver<ClusterAPIProtos.ToRpcServerMessage>> queue = new ArrayBlockingQueue<>(1);
+
+ public void onMsg(StreamObserver<ClusterAPIProtos.ToRpcServerMessage> result) throws InterruptedException {
+ queue.put(result);
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return false;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public boolean isDone() {
+ return false;
+ }
+
+ @Override
+ public StreamObserver<ClusterAPIProtos.ToRpcServerMessage> get() throws InterruptedException, ExecutionException {
+ return this.queue.take();
+ }
+
+ @Override
+ public StreamObserver<ClusterAPIProtos.ToRpcServerMessage> get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ StreamObserver<ClusterAPIProtos.ToRpcServerMessage> result = this.queue.poll(timeout, unit);
+ if (result == null) {
+ throw new TimeoutException();
+ } else {
+ return result;
+ }
+ }
+}
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
new file mode 100644
index 0000000..a51464c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
@@ -0,0 +1,190 @@
+/**
+ * Copyright © 2016 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.component;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Charsets;
+import com.google.common.io.Resources;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
+import org.springframework.core.type.filter.AnnotationTypeFilter;
+import org.springframework.stereotype.Service;
+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.io.IOException;
+import java.lang.annotation.Annotation;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+@Slf4j
+public class AnnotationComponentDiscoveryService implements ComponentDiscoveryService {
+
+ @Value("${plugins.scan_packages}")
+ private String[] scanPackages;
+
+ @Autowired
+ private ComponentDescriptorService componentDescriptorService;
+
+ private Map<String, ComponentDescriptor> components = new HashMap<>();
+
+ private Map<ComponentType, List<ComponentDescriptor>> componentsMap = new HashMap<>();
+
+ private ObjectMapper mapper = new ObjectMapper();
+
+ @PostConstruct
+ public void init() {
+ registerComponents(ComponentType.FILTER, Filter.class);
+
+ registerComponents(ComponentType.PROCESSOR, Processor.class);
+
+ registerComponents(ComponentType.ACTION, Action.class);
+
+ registerComponents(ComponentType.PLUGIN, Plugin.class);
+
+ log.info("Found following definitions: {}", components.values());
+ }
+
+ private void registerComponents(ComponentType type, Class<? extends Annotation> annotation) {
+ List<ComponentDescriptor> components = persist(getBeanDefinitions(annotation), type);
+ componentsMap.put(type, components);
+ registerComponents(components);
+ }
+
+ private void registerComponents(Collection<ComponentDescriptor> comps) {
+ comps.stream().forEach(c -> components.put(c.getClazz(), c));
+ }
+
+ private List<ComponentDescriptor> persist(Set<BeanDefinition> filterDefs, ComponentType type) {
+ List<ComponentDescriptor> result = new ArrayList<>();
+ for (BeanDefinition def : filterDefs) {
+ ComponentDescriptor scannedComponent = new ComponentDescriptor();
+ String clazzName = def.getBeanClassName();
+ try {
+ scannedComponent.setType(type);
+ Class<?> clazz = Class.forName(clazzName);
+ String descriptorResourceName;
+ switch (type) {
+ 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 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.asList(pluginAnnotation.actions()).stream().map(action -> action.getName()).collect(Collectors.joining(",")));
+ 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) {
+ log.error("Can't initialize component {}, due to {}", def.getBeanClassName(), e.getMessage(), e);
+ throw new RuntimeException(e);
+ }
+ ComponentDescriptor persistedComponent = componentDescriptorService.findByClazz(clazzName);
+ if (persistedComponent == null) {
+ log.info("Persisting new component: {}", scannedComponent);
+ scannedComponent = componentDescriptorService.saveComponent(scannedComponent);
+ } else if (scannedComponent.equals(persistedComponent)) {
+ log.info("Component is already persisted: {}", persistedComponent);
+ scannedComponent = persistedComponent;
+ } else {
+ log.info("Component {} will be updated to {}", persistedComponent, scannedComponent);
+ componentDescriptorService.deleteByClazz(persistedComponent.getClazz());
+ scannedComponent.setId(persistedComponent.getId());
+ scannedComponent = componentDescriptorService.saveComponent(scannedComponent);
+ }
+ result.add(scannedComponent);
+ }
+ return result;
+ }
+
+ private Set<BeanDefinition> getBeanDefinitions(Class<? extends Annotation> componentType) {
+ ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
+ scanner.addIncludeFilter(new AnnotationTypeFilter(componentType));
+ Set<BeanDefinition> defs = new HashSet<>();
+ for (String scanPackage : scanPackages) {
+ defs.addAll(scanner.findCandidateComponents(scanPackage));
+ }
+ return defs;
+ }
+
+ @Override
+ public List<ComponentDescriptor> getComponents(ComponentType type) {
+ return Collections.unmodifiableList(componentsMap.get(type));
+ }
+
+ @Override
+ public Optional<ComponentDescriptor> getComponent(String clazz) {
+ return Optional.ofNullable(components.get(clazz));
+ }
+
+ @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!");
+ }
+ }
+}
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
new file mode 100644
index 0000000..d14a60f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 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.component;
+
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ComponentDiscoveryService {
+
+ List<ComponentDescriptor> getComponents(ComponentType type);
+
+ Optional<ComponentDescriptor> getComponent(String clazz);
+
+ List<ComponentDescriptor> getPluginActions(String pluginClazz);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java b/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java
new file mode 100644
index 0000000..f082824
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016 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;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
+
+import java.util.Arrays;
+
+@EnableAutoConfiguration
+@SpringBootApplication
+@ComponentScan({"org.thingsboard.server"})
+public class ThingsboardServerApplication {
+
+ private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name";
+ private static final String DEFAULT_SPRING_CONFIG_PARAM = SPRING_CONFIG_NAME_KEY + "=" + "thingsboard";
+
+ public static void main(String[] args) {
+ SpringApplication.run(ThingsboardServerApplication.class, updateArguments(args));
+ }
+
+ private static String[] updateArguments(String[] args) {
+ if (Arrays.stream(args).noneMatch(arg -> arg.startsWith(SPRING_CONFIG_NAME_KEY))) {
+ String[] modifiedArgs = new String[args.length + 1];
+ System.arraycopy(args, 0, modifiedArgs, 0, args.length);
+ modifiedArgs[args.length] = DEFAULT_SPRING_CONFIG_PARAM;
+ return modifiedArgs;
+ }
+ return args;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java b/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java
new file mode 100644
index 0000000..3af69b4
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright © 2016 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.utils;
+
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+
+import java.nio.charset.Charset;
+
+
+/**
+ * @author Andrew Shvayka
+ */
+public class MiscUtils {
+
+ public static final Charset UTF8 = Charset.forName("UTF-8");
+
+ public static String missingProperty(String propertyName) {
+ return "The " + propertyName + " property need to be set!";
+ }
+
+ public static HashFunction forName(String name) {
+ switch (name) {
+ case "murmur3_32":
+ return Hashing.murmur3_32();
+ case "murmur3_128":
+ return Hashing.murmur3_128();
+ case "crc32":
+ return Hashing.crc32();
+ case "md5":
+ return Hashing.md5();
+ default:
+ throw new IllegalArgumentException("Can't find hash function with name " + name);
+ }
+ }
+}
application/src/main/proto/cluster.proto 97(+97 -0)
diff --git a/application/src/main/proto/cluster.proto b/application/src/main/proto/cluster.proto
new file mode 100644
index 0000000..4ac58cf
--- /dev/null
+++ b/application/src/main/proto/cluster.proto
@@ -0,0 +1,97 @@
+/**
+ * Copyright © 2016 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 cluster;
+
+option java_package = "org.thingsboard.server.gen.cluster";
+option java_outer_classname = "ClusterAPIProtos";
+
+message ServerAddress {
+ string host = 1;
+ int32 port = 2;
+}
+
+message Uid {
+ sint64 pluginUuidMsb = 1;
+ sint64 pluginUuidLsb = 2;
+}
+
+message PluginAddress {
+ Uid pluginId = 1;
+ Uid tenantId = 2;
+}
+
+message ToPluginRpcMessage {
+ PluginAddress address = 1;
+ int32 clazz = 2;
+ bytes data = 3;
+}
+
+message ToDeviceActorRpcMessage {
+ bytes data = 1;
+}
+
+message ToDeviceSessionActorRpcMessage {
+ bytes data = 1;
+}
+
+message ToDeviceActorNotificationRpcMessage {
+ bytes data = 1;
+}
+
+message ToAllNodesRpcMessage {
+ bytes data = 1;
+}
+
+message ConnectRpcMessage {
+ ServerAddress serverAddress = 1;
+}
+
+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 ToPluginRpcResponseRpcMessage {
+ PluginAddress address = 1;
+
+ Uid msgId = 2;
+ string response = 3;
+ string error = 4;
+}
+
+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;
+}
+
+service ClusterRpcService {
+ rpc handlePluginMsgs(stream ToRpcServerMessage) returns (stream ToRpcServerMessage) {}
+}
+
application/src/main/proto/discovery.proto 26(+26 -0)
diff --git a/application/src/main/proto/discovery.proto b/application/src/main/proto/discovery.proto
new file mode 100644
index 0000000..17db5c1
--- /dev/null
+++ b/application/src/main/proto/discovery.proto
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 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 discovery;
+
+option java_package = "org.thingsboard.server.gen.discovery";
+option java_outer_classname = "ServerInstanceProtos";
+
+message ServerInfo {
+ string host = 1;
+ int32 port = 2;
+ int64 ts = 3;
+}
application/src/main/resources/actor-system.conf 163(+163 -0)
diff --git a/application/src/main/resources/actor-system.conf b/application/src/main/resources/actor-system.conf
new file mode 100644
index 0000000..7bd80ab
--- /dev/null
+++ b/application/src/main/resources/actor-system.conf
@@ -0,0 +1,163 @@
+#
+# Copyright © 2016 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.
+#
+
+
+akka {
+ # JVM shutdown, System.exit(-1), in case of a fatal error,
+ # such as OutOfMemoryError
+ jvm-exit-on-fatal-error = off
+ loglevel = "INFO"
+ loggers = ["akka.event.slf4j.Slf4jLogger"]
+}
+
+# This dispatcher is used for app
+app-dispatcher {
+ type = Dispatcher
+ executor = "fork-join-executor"
+ fork-join-executor {
+ # Min number of threads to cap factor-based parallelism number to
+ parallelism-min = 2
+ # Max number of threads to cap factor-based parallelism number to
+ parallelism-max = 12
+
+ # The parallelism factor is used to determine thread pool size using the
+ # following formula: ceil(available processors * factor). Resulting size
+ # is then bounded by the parallelism-min and parallelism-max values.
+ parallelism-factor = 1.0
+ }
+ # How long time the dispatcher will wait for new actors until it shuts down
+ shutdown-timeout = 1s
+
+ # Throughput defines the number of messages that are processed in a batch
+ # before the thread is returned to the pool. Set to 1 for as fair as possible.
+ throughput = 5
+}
+
+# This dispatcher is used for rpc actors
+rpc-dispatcher {
+ type = Dispatcher
+ executor = "fork-join-executor"
+ fork-join-executor {
+ # Min number of threads to cap factor-based parallelism number to
+ parallelism-min = 2
+ # Max number of threads to cap factor-based parallelism number to
+ parallelism-max = 12
+
+ # The parallelism factor is used to determine thread pool size using the
+ # following formula: ceil(available processors * factor). Resulting size
+ # is then bounded by the parallelism-min and parallelism-max values.
+ parallelism-factor = 0.5
+ }
+ # How long time the dispatcher will wait for new actors until it shuts down
+ shutdown-timeout = 1s
+
+ # Throughput defines the number of messages that are processed in a batch
+ # before the thread is returned to the pool. Set to 1 for as fair as possible.
+ throughput = 5
+}
+
+# This dispatcher is used for auth
+core-dispatcher {
+ type = Dispatcher
+ executor = "fork-join-executor"
+ fork-join-executor {
+ # Min number of threads to cap factor-based parallelism number to
+ parallelism-min = 2
+ # Max number of threads to cap factor-based parallelism number to
+ parallelism-max = 12
+
+ # The parallelism factor is used to determine thread pool size using the
+ # following formula: ceil(available processors * factor). Resulting size
+ # is then bounded by the parallelism-min and parallelism-max values.
+ parallelism-factor = 1.0
+ }
+ # How long time the dispatcher will wait for new actors until it shuts down
+ shutdown-timeout = 1s
+
+ # Throughput defines the number of messages that are processed in a batch
+ # before the thread is returned to the pool. Set to 1 for as fair as possible.
+ throughput = 5
+}
+
+# This dispatcher is used for rule actors
+rule-dispatcher {
+ type = Dispatcher
+ executor = "fork-join-executor"
+ fork-join-executor {
+ # Min number of threads to cap factor-based parallelism number to
+ parallelism-min = 2
+ # Max number of threads to cap factor-based parallelism number to
+ parallelism-max = 12
+
+ # The parallelism factor is used to determine thread pool size using the
+ # following formula: ceil(available processors * factor). Resulting size
+ # is then bounded by the parallelism-min and parallelism-max values.
+ parallelism-factor = 1.0
+ }
+ # How long time the dispatcher will wait for new actors until it shuts down
+ shutdown-timeout = 1s
+
+ # Throughput defines the number of messages that are processed in a batch
+ # before the thread is returned to the pool. Set to 1 for as fair as possible.
+ throughput = 5
+}
+
+# This dispatcher is used for rule actors
+plugin-dispatcher {
+ type = Dispatcher
+ executor = "fork-join-executor"
+ fork-join-executor {
+ # Min number of threads to cap factor-based parallelism number to
+ parallelism-min = 2
+ # Max number of threads to cap factor-based parallelism number to
+ parallelism-max = 12
+
+ # The parallelism factor is used to determine thread pool size using the
+ # following formula: ceil(available processors * factor). Resulting size
+ # is then bounded by the parallelism-min and parallelism-max values.
+ parallelism-factor = 1.0
+ }
+ # How long time the dispatcher will wait for new actors until it shuts down
+ shutdown-timeout = 1s
+
+ # Throughput defines the number of messages that are processed in a batch
+ # before the thread is returned to the pool. Set to 1 for as fair as possible.
+ throughput = 5
+}
+
+
+# This dispatcher is used for rule actors
+session-dispatcher {
+ type = Dispatcher
+ executor = "fork-join-executor"
+ fork-join-executor {
+ # Min number of threads to cap factor-based parallelism number to
+ parallelism-min = 2
+ # Max number of threads to cap factor-based parallelism number to
+ parallelism-max = 12
+
+ # The parallelism factor is used to determine thread pool size using the
+ # following formula: ceil(available processors * factor). Resulting size
+ # is then bounded by the parallelism-min and parallelism-max values.
+ parallelism-factor = 1.0
+ }
+ # How long time the dispatcher will wait for new actors until it shuts down
+ shutdown-timeout = 1s
+
+ # Throughput defines the number of messages that are processed in a batch
+ # before the thread is returned to the pool. Set to 1 for as fair as possible.
+ throughput = 5
+}
\ No newline at end of file
diff --git a/application/src/main/resources/banner.txt b/application/src/main/resources/banner.txt
new file mode 100644
index 0000000..791f878
--- /dev/null
+++ b/application/src/main/resources/banner.txt
@@ -0,0 +1,3 @@
+ ===================================================
+ :: ${application.title} :: ${application.formatted-version}
+ ===================================================
diff --git a/application/src/main/resources/i18n/messages.properties b/application/src/main/resources/i18n/messages.properties
new file mode 100644
index 0000000..a78fbe0
--- /dev/null
+++ b/application/src/main/resources/i18n/messages.properties
@@ -0,0 +1,5 @@
+test.message.subject=Test message from Thingsboard
+activation.subject=Your account activation on Thingsboard
+account.activated.subject=Thingsboard - your account has been activated
+reset.password.subject=Thingsboard - Password reset has been requested
+password.was.reset.subject=Thingsboard - your account password has been reset
application/src/main/resources/logback.xml 35(+35 -0)
diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml
new file mode 100644
index 0000000..5506578
--- /dev/null
+++ b/application/src/main/resources/logback.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+
+ Copyright © 2016 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.
+
+-->
+<!DOCTYPE configuration>
+<configuration>
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <logger name="org.thingsboard.server" level="TRACE" />
+ <logger name="akka" level="INFO" />
+
+ <root level="INFO">
+ <appender-ref ref="STDOUT"/>
+ </root>
+
+</configuration>
\ No newline at end of file
diff --git a/application/src/main/resources/templates/account.activated.vm b/application/src/main/resources/templates/account.activated.vm
new file mode 100644
index 0000000..4913acc
--- /dev/null
+++ b/application/src/main/resources/templates/account.activated.vm
@@ -0,0 +1,122 @@
+#*
+ * Copyright © 2016 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.
+ *#
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+<head>
+<meta name="viewport" content="width=device-width" />
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<title>Thingsboard - Account Activated</title>
+
+
+<style type="text/css">
+img {
+max-width: 100%;
+}
+body {
+-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;
+}
+body {
+background-color: #f6f6f6;
+}
+@media only screen and (max-width: 640px) {
+ body {
+ padding: 0 !important;
+ }
+ h1 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h2 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h3 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h4 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h1 {
+ font-size: 22px !important;
+ }
+ h2 {
+ font-size: 18px !important;
+ }
+ h3 {
+ font-size: 16px !important;
+ }
+ .container {
+ padding: 0 !important; width: 100% !important;
+ }
+ .content {
+ padding: 0 !important;
+ }
+ .content-wrap {
+ padding: 10px !important;
+ }
+ .invoice {
+ width: 100% !important;
+ }
+}
+</style>
+</head>
+
+<body itemscope itemtype="http://schema.org/EmailMessage" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">
+
+<table class="body-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+ <td class="container" width="600" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;" valign="top">
+ <div class="content" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;">
+ <table class="main" width="100%" cellpadding="0" cellspacing="0" itemprop="action" itemscope itemtype="http://schema.org/ConfirmAction" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;" bgcolor="#fff"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;" valign="top">
+ <meta itemprop="name" content="Confirm Email" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" /><table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ <h2>Your Thingsboard account has been activated</h2>
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ Congratulations! Your Thingsboard account has been activated.
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ Now you can login to your Thingsboard space.
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" itemprop="handler" itemscope itemtype="http://schema.org/HttpActionHandler" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ <a href="$loginLink" class="btn-primary" itemprop="url" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;">Login</a>
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ — The Thingsboard
+ </td>
+ </tr></table></td>
+ </tr>
+ </table>
+ <div class="footer" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">
+ <table width="100%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="aligncenter content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top">This email was sent to <a href="mailto:$targetEmail" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;">$targetEmail</a> by Thingsboard.</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </td>
+ <td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+ </tr>
+</table>
+</body>
+</html>
diff --git a/application/src/main/resources/templates/activation.vm b/application/src/main/resources/templates/activation.vm
new file mode 100644
index 0000000..7d2beb3
--- /dev/null
+++ b/application/src/main/resources/templates/activation.vm
@@ -0,0 +1,122 @@
+#*
+ * Copyright © 2016 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.
+ *#
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+<head>
+<meta name="viewport" content="width=device-width" />
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<title>Thingsboard - Account Activation</title>
+
+
+<style type="text/css">
+img {
+max-width: 100%;
+}
+body {
+-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;
+}
+body {
+background-color: #f6f6f6;
+}
+@media only screen and (max-width: 640px) {
+ body {
+ padding: 0 !important;
+ }
+ h1 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h2 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h3 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h4 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h1 {
+ font-size: 22px !important;
+ }
+ h2 {
+ font-size: 18px !important;
+ }
+ h3 {
+ font-size: 16px !important;
+ }
+ .container {
+ padding: 0 !important; width: 100% !important;
+ }
+ .content {
+ padding: 0 !important;
+ }
+ .content-wrap {
+ padding: 10px !important;
+ }
+ .invoice {
+ width: 100% !important;
+ }
+}
+</style>
+</head>
+
+<body itemscope itemtype="http://schema.org/EmailMessage" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">
+
+<table class="body-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+ <td class="container" width="600" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;" valign="top">
+ <div class="content" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;">
+ <table class="main" width="100%" cellpadding="0" cellspacing="0" itemprop="action" itemscope itemtype="http://schema.org/ConfirmAction" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;" bgcolor="#fff"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;" valign="top">
+ <meta itemprop="name" content="Confirm Email" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" /><table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ <h2>Activate your Thingsboard account</h2>
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ To confirm your email address and choose a password, just click the button below.
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ We may need to send you critical information about our service and it is important that we have an accurate email address.
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" itemprop="handler" itemscope itemtype="http://schema.org/HttpActionHandler" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ <a href="$activationLink" class="btn-primary" itemprop="url" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;">Activate your account</a>
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ — The Thingsboard
+ </td>
+ </tr></table></td>
+ </tr>
+ </table>
+ <div class="footer" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">
+ <table width="100%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="aligncenter content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top">This email was sent to <a href="mailto:$targetEmail" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;">$targetEmail</a> by Thingsboard.</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </td>
+ <td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+ </tr>
+</table>
+</body>
+</html>
diff --git a/application/src/main/resources/templates/password.was.reset.vm b/application/src/main/resources/templates/password.was.reset.vm
new file mode 100644
index 0000000..22beff6
--- /dev/null
+++ b/application/src/main/resources/templates/password.was.reset.vm
@@ -0,0 +1,122 @@
+#*
+ * Copyright © 2016 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.
+ *#
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+<head>
+<meta name="viewport" content="width=device-width" />
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<title>Thingsboard - Account Password Has Been Reset</title>
+
+
+<style type="text/css">
+img {
+max-width: 100%;
+}
+body {
+-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;
+}
+body {
+background-color: #f6f6f6;
+}
+@media only screen and (max-width: 640px) {
+ body {
+ padding: 0 !important;
+ }
+ h1 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h2 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h3 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h4 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h1 {
+ font-size: 22px !important;
+ }
+ h2 {
+ font-size: 18px !important;
+ }
+ h3 {
+ font-size: 16px !important;
+ }
+ .container {
+ padding: 0 !important; width: 100% !important;
+ }
+ .content {
+ padding: 0 !important;
+ }
+ .content-wrap {
+ padding: 10px !important;
+ }
+ .invoice {
+ width: 100% !important;
+ }
+}
+</style>
+</head>
+
+<body itemscope itemtype="http://schema.org/EmailMessage" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">
+
+<table class="body-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+ <td class="container" width="600" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;" valign="top">
+ <div class="content" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;">
+ <table class="main" width="100%" cellpadding="0" cellspacing="0" itemprop="action" itemscope itemtype="http://schema.org/ConfirmAction" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;" bgcolor="#fff"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;" valign="top">
+ <meta itemprop="name" content="Confirm Email" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" /><table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ <h2>Your Thingsboard account password has been reset</h2>
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ You have successfully created new password for your Thingsboard account.
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ Now you can login to your Thingsboard space using your newly created password.
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" itemprop="handler" itemscope itemtype="http://schema.org/HttpActionHandler" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ <a href="$loginLink" class="btn-primary" itemprop="url" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;">Login</a>
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ — The Thingsboard
+ </td>
+ </tr></table></td>
+ </tr>
+ </table>
+ <div class="footer" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">
+ <table width="100%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="aligncenter content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top">This email was sent to <a href="mailto:$targetEmail" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;">$targetEmail</a> by Thingsboard.</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </td>
+ <td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+ </tr>
+</table>
+</body>
+</html>
diff --git a/application/src/main/resources/templates/reset.password.vm b/application/src/main/resources/templates/reset.password.vm
new file mode 100644
index 0000000..18ebcc0
--- /dev/null
+++ b/application/src/main/resources/templates/reset.password.vm
@@ -0,0 +1,122 @@
+#*
+ * Copyright © 2016 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.
+ *#
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+<head>
+<meta name="viewport" content="width=device-width" />
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<title>Thingsboard - Reset Password Request</title>
+
+
+<style type="text/css">
+img {
+max-width: 100%;
+}
+body {
+-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;
+}
+body {
+background-color: #f6f6f6;
+}
+@media only screen and (max-width: 640px) {
+ body {
+ padding: 0 !important;
+ }
+ h1 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h2 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h3 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h4 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h1 {
+ font-size: 22px !important;
+ }
+ h2 {
+ font-size: 18px !important;
+ }
+ h3 {
+ font-size: 16px !important;
+ }
+ .container {
+ padding: 0 !important; width: 100% !important;
+ }
+ .content {
+ padding: 0 !important;
+ }
+ .content-wrap {
+ padding: 10px !important;
+ }
+ .invoice {
+ width: 100% !important;
+ }
+}
+</style>
+</head>
+
+<body itemscope itemtype="http://schema.org/EmailMessage" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">
+
+<table class="body-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+ <td class="container" width="600" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;" valign="top">
+ <div class="content" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;">
+ <table class="main" width="100%" cellpadding="0" cellspacing="0" itemprop="action" itemscope itemtype="http://schema.org/ConfirmAction" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;" bgcolor="#fff"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;" valign="top">
+ <meta itemprop="name" content="Confirm Email" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" /><table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ <h2>Password reset has been requested</h2>
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ You have requested password reset for your Thingsboard account.
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ Click below in order to proceed password reset procedure.
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" itemprop="handler" itemscope itemtype="http://schema.org/HttpActionHandler" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ <a href="$passwordResetLink" class="btn-primary" itemprop="url" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;">Reset password</a>
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ — The Thingsboard
+ </td>
+ </tr></table></td>
+ </tr>
+ </table>
+ <div class="footer" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">
+ <table width="100%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="aligncenter content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top">This email was sent to <a href="mailto:$targetEmail" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;">$targetEmail</a> by Thingsboard.</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </td>
+ <td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+ </tr>
+</table>
+</body>
+</html>
application/src/main/resources/templates/test.vm 112(+112 -0)
diff --git a/application/src/main/resources/templates/test.vm b/application/src/main/resources/templates/test.vm
new file mode 100644
index 0000000..ecd3d15
--- /dev/null
+++ b/application/src/main/resources/templates/test.vm
@@ -0,0 +1,112 @@
+#*
+ * Copyright © 2016 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.
+ *#
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+<head>
+<meta name="viewport" content="width=device-width" />
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<title>Thingsboard - Test Message</title>
+
+
+<style type="text/css">
+img {
+max-width: 100%;
+}
+body {
+-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;
+}
+body {
+background-color: #f6f6f6;
+}
+@media only screen and (max-width: 640px) {
+ body {
+ padding: 0 !important;
+ }
+ h1 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h2 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h3 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h4 {
+ font-weight: 800 !important; margin: 20px 0 5px !important;
+ }
+ h1 {
+ font-size: 22px !important;
+ }
+ h2 {
+ font-size: 18px !important;
+ }
+ h3 {
+ font-size: 16px !important;
+ }
+ .container {
+ padding: 0 !important; width: 100% !important;
+ }
+ .content {
+ padding: 0 !important;
+ }
+ .content-wrap {
+ padding: 10px !important;
+ }
+ .invoice {
+ width: 100% !important;
+ }
+}
+</style>
+</head>
+
+<body itemscope itemtype="http://schema.org/EmailMessage" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">
+
+<table class="body-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+ <td class="container" width="600" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;" valign="top">
+ <div class="content" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;">
+ <table class="main" width="100%" cellpadding="0" cellspacing="0" itemprop="action" itemscope itemtype="http://schema.org/ConfirmAction" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;" bgcolor="#fff"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;" valign="top">
+ <meta itemprop="name" content="Confirm Email" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" /><table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ <h2>Test message from Thingsboard</h2>
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ This email is indicating that your outgoing mail settings were set up correctly.
+ </td>
+ </tr>
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+ — The Thingsboard
+ </td>
+ </tr></table></td>
+ </tr>
+ </table>
+ <div class="footer" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">
+ <table width="100%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+ <td class="aligncenter content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top">This email was sent to <a href="mailto:$targetEmail" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;">$targetEmail</a> by Thingsboard.</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </td>
+ <td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+ </tr>
+</table>
+</body>
+</html>
application/src/main/resources/thingsboard.yml 176(+176 -0)
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
new file mode 100644
index 0000000..7a8f58f
--- /dev/null
+++ b/application/src/main/resources/thingsboard.yml
@@ -0,0 +1,176 @@
+#
+# Copyright © 2016 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.
+#
+
+server:
+ # Server bind address
+ address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
+ # Server bind port
+ port: "${HTTP_BIND_PORT:8080}"
+# Uncomment the following section to enable ssl
+# ssl:
+# key-store: classpath:keystore/keystore.p12
+# key-store-password: thingsboard
+# keyStoreType: PKCS12
+# keyAlias: tomcat
+
+# Zookeeper connection parameters. Used for service discovery.
+zk:
+ # Enable/disable zookeeper discovery service.
+ enabled: "${ZOOKEEPER_ENABLED:false}"
+ # Zookeeper connect string
+ url: "${ZOOKEEPER_URL:localhost:2181}"
+ # Zookeeper retry interval in milliseconds
+ retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
+ # Zookeeper connection timeout in milliseconds
+ connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
+ # Zookeeper session timeout in milliseconds
+ session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
+ # Name of the directory in zookeeper 'filesystem'
+ zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
+
+# RPC connection parameters. Used only in cluster mode only.
+rpc:
+ bind_host: "${RPC_HOST:localhost}"
+ bind_port: "${RPC_PORT:9001}"
+
+# Clustering properties related to consistent-hashing. See architecture docs for more details.
+cluster:
+ # Name of hash function used for consistent hash ring.
+ 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}"
+
+# Plugins configuration parameters
+plugins:
+ # Comma seperated package list used during classpath scanning for plugins
+ scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions}"
+
+# JWT Token parameters
+security.jwt:
+ tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:900}" # Number of seconds (15 mins)
+ refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:3600}" # Seconds (1 hour)
+ tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}"
+ tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}"
+
+# Device communication protocol parameters
+http:
+ request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}"
+
+# MQTT server parameters
+mqtt:
+ bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}"
+ bind_port: "${MQTT_BIND_PORT:1883}"
+ adaptor: "${MQTT_ADAPTOR_NAME:JsonMqttAdaptor}"
+ timeout: "${MQTT_TIMEOUT:10000}"
+# Uncomment the following lines to enable ssl for MQTT
+# ssl:
+# key-store: keystore/mqttserver.jks
+# key-store-password: password
+# keyStoreType: JKS
+# TrustStore can be the same as KeyStore
+# trust-store: keystore/mqttserver.jks
+# trust-store-password: password
+# trustStoreType: JKS
+
+# CoAP server parameters
+coap:
+ bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}"
+ bind_port: "${COAP_BIND_PORT:5683}"
+ adaptor: "${COAP_ADAPTOR_NAME:JsonCoapAdaptor}"
+ timeout: "${COAP_TIMEOUT:10000}"
+
+# Cassandra driver configuration parameters
+cassandra:
+ # Thingsboard cluster name
+ cluster_name: "${CASSANDRA_CLUSTER_NAME:Thingsboard Cluster}"
+ # Thingsboard keyspace name
+ keyspace_name: "${CASSANDRA_KEYSPACE_NAME:thingsboard}"
+ # Specify node list
+ url: "${CASSANDRA_URL:127.0.0.1:9042}"
+ # Enable/disable secure connection
+ ssl: "${CASSANDRA_USE_SSL:false}"
+ # Enable/disable JMX
+ jmx: "${CASSANDRA_USE_JMX:true}"
+ # Enable/disable metrics collection.
+ metrics: "${CASSANDRA_DISABLE_METRICS:true}"
+ # NONE SNAPPY LZ4
+ compression: "${CASSANDRA_COMPRESSION:none}"
+ # Specify cassandra claster initialization timeout (if no hosts available during startup)
+ init_timeout_ms: "${CASSANDRA_CLUSTER_INIT_TIMEOUT_MS:300000}"
+ # Specify cassandra claster initialization retry interval (if no hosts available during startup)
+ init_retry_interval_ms: "${CASSANDRA_CLUSTER_INIT_RETRY_INTERVAL_MS:3000}"
+
+ # Credential parameters #
+ credentials: "${CASSANDRA_USE_CREDENTIALS:false}"
+ # Specify your username
+ username: "${CASSANDRA_USERNAME:}"
+ # Specify your password
+ password: "${CASSANDRA_PASSWORD:}"
+
+ # Cassandra cluster connection socket parameters #
+ socket:
+ connect_timeout: "${CASSANDRA_SOCKET_TIMEOUT:5000}"
+ read_timeout: "${CASSANDRA_SOCKET_READ_TIMEOUT:20000}"
+ keep_alive: "${CASSANDRA_SOCKET_KEEP_ALIVE:true}"
+ reuse_address: "${CASSANDRA_SOCKET_REUSE_ADDRESS:true}"
+ so_linger: "${CASSANDRA_SOCKET_SO_LINGER:}"
+ tcp_no_delay: "${CASSANDRA_SOCKET_TCP_NO_DELAY:false}"
+ receive_buffer_size: "${CASSANDRA_SOCKET_RECEIVE_BUFFER_SIZE:}"
+ send_buffer_size: "${CASSANDRA_SOCKET_SEND_BUFFER_SIZE:}"
+
+ # Cassandra cluster connection query parameters #
+ query:
+ read_consistency_level: "${CASSANDRA_READ_CONSISTENCY_LEVEL:ONE}"
+ write_consistency_level: "${CASSANDRA_WRITE_CONSISTENCY_LEVEL:ONE}"
+ 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}"
+ # Specify max partitions per request
+ max_limit_per_request: "${TS_KV_MAX_LIMIT_PER_REQUEST:1000}"
+
+# Actor system parameters
+actors:
+ session:
+ 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}"
+ statistics:
+ # Enable/disable actor statistics
+ enabled: "${ACTORS_STATISTICS_ENABLED:true}"
+ persist_frequency: "${ACTORS_STATISTICS_PERSIST_FREQUENCY:60000}"
+
+# Cache parameters
+cache:
+ # Enable/disable cache functionality.
+ enabled: "${CACHE_ENABLED:true}"
+ device_credentials:
+ # default time to store device credentials in cache, in seconds
+ time_to_live: "${DEVICE_CREDENTIAL_CACHE_TTL:3600}"
+ # default maximum size of device credentials cache
+ max_size: "${DEVICE_CREDENTIAL_CACHE_MAX_SIZE:1000000}"
+
diff --git a/application/src/main/scripts/control/deb/postinst b/application/src/main/scripts/control/deb/postinst
new file mode 100644
index 0000000..d4066c0
--- /dev/null
+++ b/application/src/main/scripts/control/deb/postinst
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+chown -R ${pkg.name}: ${pkg.logFolder}
+chown -R ${pkg.name}: ${pkg.installFolder}
+update-rc.d ${pkg.name} defaults
+
diff --git a/application/src/main/scripts/control/deb/postrm b/application/src/main/scripts/control/deb/postrm
new file mode 100644
index 0000000..6186580
--- /dev/null
+++ b/application/src/main/scripts/control/deb/postrm
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+update-rc.d -f ${pkg.name} remove
diff --git a/application/src/main/scripts/control/deb/preinst b/application/src/main/scripts/control/deb/preinst
new file mode 100644
index 0000000..6be5959
--- /dev/null
+++ b/application/src/main/scripts/control/deb/preinst
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+if ! getent group ${pkg.name} >/dev/null; then
+ addgroup --system ${pkg.name}
+fi
+
+if ! getent passwd ${pkg.name} >/dev/null; then
+ adduser --quiet \
+ --system \
+ --ingroup ${pkg.name} \
+ --quiet \
+ --disabled-login \
+ --disabled-password \
+ --home ${pkg.installFolder} \
+ --no-create-home \
+ -gecos "Thingsboard application" \
+ ${pkg.name}
+fi
diff --git a/application/src/main/scripts/control/deb/prerm b/application/src/main/scripts/control/deb/prerm
new file mode 100644
index 0000000..898d3ef
--- /dev/null
+++ b/application/src/main/scripts/control/deb/prerm
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+if [ -e /var/run/${pkg.name}/${pkg.name}.pid ]; then
+ service ${pkg.name} stop
+fi
diff --git a/application/src/main/scripts/control/rpm/postinst b/application/src/main/scripts/control/rpm/postinst
new file mode 100644
index 0000000..8a7a88f
--- /dev/null
+++ b/application/src/main/scripts/control/rpm/postinst
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+chown -R ${pkg.name}: ${pkg.logFolder}
+chown -R ${pkg.name}: ${pkg.installFolder}
+
+if [ $1 -eq 1 ] ; then
+ # Initial installation
+ systemctl --no-reload enable ${pkg.name}.service >/dev/null 2>&1 || :
+fi
diff --git a/application/src/main/scripts/control/rpm/postrm b/application/src/main/scripts/control/rpm/postrm
new file mode 100644
index 0000000..8e1f8a2
--- /dev/null
+++ b/application/src/main/scripts/control/rpm/postrm
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+if [ $1 -ge 1 ] ; then
+ # Package upgrade, not uninstall
+ systemctl try-restart ${pkg.name}.service >/dev/null 2>&1 || :
+fi
diff --git a/application/src/main/scripts/control/rpm/preinst b/application/src/main/scripts/control/rpm/preinst
new file mode 100644
index 0000000..e19fc88
--- /dev/null
+++ b/application/src/main/scripts/control/rpm/preinst
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+getent group ${pkg.name} >/dev/null || groupadd -r ${pkg.name}
+getent passwd ${pkg.name} >/dev/null || \
+useradd -d ${pkg.installFolder} -g ${pkg.name} -M -r ${pkg.name} -s /sbin/nologin \
+-c "Thingsboard application"
diff --git a/application/src/main/scripts/control/rpm/prerm b/application/src/main/scripts/control/rpm/prerm
new file mode 100644
index 0000000..accb487
--- /dev/null
+++ b/application/src/main/scripts/control/rpm/prerm
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+if [ $1 -eq 0 ] ; then
+ # Package removal, not upgrade
+ systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || :
+fi
diff --git a/application/src/main/scripts/control/thingsboard.service b/application/src/main/scripts/control/thingsboard.service
new file mode 100644
index 0000000..d456fc0
--- /dev/null
+++ b/application/src/main/scripts/control/thingsboard.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=${pkg.name}
+After=syslog.target
+
+[Service]
+User=${pkg.name}
+ExecStart=${pkg.installFolder}/bin/${pkg.name}.jar
+SuccessExitStatus=143
+
+[Install]
+WantedBy=multi-user.target
diff --git a/application/src/test/java/org/thingsboard/server/actors/ActorsTestSuite.java b/application/src/test/java/org/thingsboard/server/actors/ActorsTestSuite.java
new file mode 100644
index 0000000..481868b
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/actors/ActorsTestSuite.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 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;
+
+import org.junit.extensions.cpsuite.ClasspathSuite;
+import org.junit.runner.RunWith;
+
+/**
+ * @author Andrew Shvayka
+ */
+@RunWith(ClasspathSuite.class)
+@ClasspathSuite.ClassnameFilters({"org.thingsboard.server.actors.*Test"})
+public class ActorsTestSuite {
+}
diff --git a/application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java b/application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java
new file mode 100644
index 0000000..10471d6
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java
@@ -0,0 +1,242 @@
+/**
+ * Copyright © 2016 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;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.*;
+
+import org.thingsboard.server.actors.service.DefaultActorService;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.session.*;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.event.EventService;
+import org.thingsboard.server.gen.discovery.ServerInstanceProtos;
+import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
+import org.thingsboard.server.service.cluster.discovery.ServerInstance;
+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.common.transport.auth.DeviceAuthResult;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+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.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.common.data.security.DeviceCredentialsFilter;
+import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
+import org.thingsboard.server.common.msg.core.BasicTelemetryUploadRequest;
+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.tenant.TenantService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.extensions.core.plugin.telemetry.TelemetryStoragePlugin;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class DefaultActorServiceTest {
+
+ private static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);
+
+ private static final String PLUGIN_ID = "9fb2e951-e298-4acb-913a-db69af8a15f4";
+ private static final String FILTERS_CONFIGURATION =
+ "[{\"clazz\":\"org.thingsboard.server.extensions.core.filter.MsgTypeFilter\", \"name\":\"TelemetryFilter\", \"configuration\": {\"messageTypes\":[\"POST_TELEMETRY\",\"POST_ATTRIBUTES\",\"GET_ATTRIBUTES\"]}}]";
+ private static final String ACTION_CONFIGURATION = "{\"pluginToken\":\"telemetry\", \"clazz\":\"org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction\", \"name\":\"TelemetryMsgConverterAction\", \"configuration\":{}}";
+ private static final String PLUGIN_CONFIGURATION = "{}";
+ private DefaultActorService actorService;
+ private ActorSystemContext actorContext;
+
+ private PluginService pluginService;
+ private RuleService ruleService;
+ private DeviceAuthService deviceAuthService;
+ private DeviceService deviceService;
+ private TimeseriesService tsService;
+ private TenantService tenantService;
+ private ClusterRpcService rpcService;
+ private DiscoveryService discoveryService;
+ private ClusterRoutingService routingService;
+ private AttributesService attributesService;
+ private ComponentDiscoveryService componentService;
+ private EventService eventService;
+ private ServerInstance serverInstance;
+
+ private RuleMetaData ruleMock;
+ private PluginMetaData pluginMock;
+ private RuleId ruleId = new RuleId(UUID.randomUUID());
+ private PluginId pluginId = new PluginId(UUID.fromString(PLUGIN_ID));
+ private TenantId tenantId = new TenantId(UUID.randomUUID());
+
+
+ @Before
+ public void before() throws Exception {
+ actorService = new DefaultActorService();
+ actorContext = new ActorSystemContext();
+
+ tenantService = mock(TenantService.class);
+ pluginService = mock(PluginService.class);
+ ruleService = mock(RuleService.class);
+ deviceAuthService = mock(DeviceAuthService.class);
+ deviceService = mock(DeviceService.class);
+ tsService = mock(TimeseriesService.class);
+ rpcService = mock(ClusterRpcService.class);
+ discoveryService = mock(DiscoveryService.class);
+ routingService = mock(ClusterRoutingService.class);
+ attributesService = mock(AttributesService.class);
+ componentService = mock(ComponentDiscoveryService.class);
+ eventService = mock(EventService.class);
+ serverInstance = new ServerInstance(ServerInstanceProtos.ServerInfo.newBuilder().setHost("localhost").setPort(8080).build());
+
+ ReflectionTestUtils.setField(actorService, "actorContext", actorContext);
+ ReflectionTestUtils.setField(actorService, "rpcService", rpcService);
+ ReflectionTestUtils.setField(actorService, "discoveryService", discoveryService);
+
+ ReflectionTestUtils.setField(actorContext, "syncSessionTimeout", 10000L);
+ ReflectionTestUtils.setField(actorContext, "pluginActorTerminationDelay", 10000L);
+ ReflectionTestUtils.setField(actorContext, "pluginErrorPersistFrequency", 10000L);
+ ReflectionTestUtils.setField(actorContext, "ruleActorTerminationDelay", 10000L);
+ ReflectionTestUtils.setField(actorContext, "ruleErrorPersistFrequency", 10000L);
+ ReflectionTestUtils.setField(actorContext, "pluginProcessingTimeout", 60000L);
+ ReflectionTestUtils.setField(actorContext, "tenantService", tenantService);
+ ReflectionTestUtils.setField(actorContext, "pluginService", pluginService);
+ ReflectionTestUtils.setField(actorContext, "ruleService", ruleService);
+ ReflectionTestUtils.setField(actorContext, "deviceAuthService", deviceAuthService);
+ ReflectionTestUtils.setField(actorContext, "deviceService", deviceService);
+ ReflectionTestUtils.setField(actorContext, "tsService", tsService);
+ ReflectionTestUtils.setField(actorContext, "rpcService", rpcService);
+ ReflectionTestUtils.setField(actorContext, "discoveryService", discoveryService);
+ ReflectionTestUtils.setField(actorContext, "tsService", tsService);
+ ReflectionTestUtils.setField(actorContext, "routingService", routingService);
+ ReflectionTestUtils.setField(actorContext, "attributesService", attributesService);
+ ReflectionTestUtils.setField(actorContext, "componentService", componentService);
+ ReflectionTestUtils.setField(actorContext, "eventService", eventService);
+
+
+ when(routingService.resolve(any())).thenReturn(Optional.empty());
+
+ when(discoveryService.getCurrentServer()).thenReturn(serverInstance);
+
+ ruleMock = mock(RuleMetaData.class);
+ when(ruleMock.getId()).thenReturn(ruleId);
+ when(ruleMock.getState()).thenReturn(ComponentLifecycleState.ACTIVE);
+ when(ruleMock.getPluginToken()).thenReturn("telemetry");
+ TextPageData<RuleMetaData> systemRules = new TextPageData<>(Collections.emptyList(), null, false);
+ TextPageData<RuleMetaData> tenantRules = new TextPageData<>(Collections.singletonList(ruleMock), null, false);
+ when(ruleService.findSystemRules(any())).thenReturn(systemRules);
+ when(ruleService.findTenantRules(any(), any())).thenReturn(tenantRules);
+ when(ruleService.findRuleById(ruleId)).thenReturn(ruleMock);
+
+ pluginMock = mock(PluginMetaData.class);
+ when(pluginMock.getTenantId()).thenReturn(SYSTEM_TENANT);
+ when(pluginMock.getId()).thenReturn(pluginId);
+ when(pluginMock.getState()).thenReturn(ComponentLifecycleState.ACTIVE);
+ TextPageData<PluginMetaData> systemPlugins = new TextPageData<>(Collections.singletonList(pluginMock), null, false);
+ TextPageData<PluginMetaData> tenantPlugins = new TextPageData<>(Collections.emptyList(), null, false);
+ when(pluginService.findSystemPlugins(any())).thenReturn(systemPlugins);
+ when(pluginService.findTenantPlugins(any(), any())).thenReturn(tenantPlugins);
+ when(pluginService.findPluginByApiToken("telemetry")).thenReturn(pluginMock);
+ when(pluginService.findPluginById(pluginId)).thenReturn(pluginMock);
+
+ TextPageData<Tenant> tenants = new TextPageData<>(Collections.emptyList(), null, false);
+ when(tenantService.findTenants(any())).thenReturn(tenants);
+ }
+
+ private void initActorSystem() {
+ actorService.initActorSystem();
+ }
+
+ @After
+ public void after() {
+ actorService.stopActorSystem();
+ }
+
+ @Test
+ public void testBasicPostWithSyncSession() throws Exception {
+ SessionContext ssnCtx = mock(SessionContext.class);
+ KvEntry entry1 = new StringDataEntry("key1", "value1");
+ KvEntry entry2 = new StringDataEntry("key2", "value2");
+ BasicTelemetryUploadRequest telemetry = new BasicTelemetryUploadRequest();
+ long ts = 42;
+ telemetry.add(ts, entry1);
+ telemetry.add(ts, entry2);
+ BasicAdaptorToSessionActorMsg msg = new BasicAdaptorToSessionActorMsg(ssnCtx, telemetry);
+
+ DeviceId deviceId = new DeviceId(UUID.randomUUID());
+
+ DeviceCredentialsFilter filter = new DeviceTokenCredentials("token1");
+ Device device = mock(Device.class);
+
+ when(device.getId()).thenReturn(deviceId);
+ when(device.getTenantId()).thenReturn(tenantId);
+ when(ssnCtx.getSessionId()).thenReturn(new DummySessionID("session1"));
+ when(ssnCtx.getSessionType()).thenReturn(SessionType.SYNC);
+ when(deviceAuthService.process(filter)).thenReturn(DeviceAuthResult.of(deviceId));
+ when(deviceService.findDeviceById(deviceId)).thenReturn(device);
+
+ ObjectMapper ruleMapper = new ObjectMapper();
+ when(ruleMock.getFilters()).thenReturn(ruleMapper.readTree(FILTERS_CONFIGURATION));
+ when(ruleMock.getAction()).thenReturn(ruleMapper.readTree(ACTION_CONFIGURATION));
+
+ ComponentDescriptor filterComp = new ComponentDescriptor();
+ filterComp.setClazz("org.thingsboard.server.extensions.core.filter.MsgTypeFilter");
+ filterComp.setType(ComponentType.FILTER);
+ when(componentService.getComponent("org.thingsboard.server.extensions.core.filter.MsgTypeFilter"))
+ .thenReturn(Optional.of(filterComp));
+
+ ComponentDescriptor actionComp = new ComponentDescriptor();
+ actionComp.setClazz("org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction");
+ actionComp.setType(ComponentType.ACTION);
+ when(componentService.getComponent("org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction"))
+ .thenReturn(Optional.of(actionComp));
+
+ ObjectMapper pluginMapper = new ObjectMapper();
+ JsonNode pluginAdditionalInfo = pluginMapper.readTree(PLUGIN_CONFIGURATION);
+ when(pluginMock.getConfiguration()).thenReturn(pluginAdditionalInfo);
+ when(pluginMock.getClazz()).thenReturn(TelemetryStoragePlugin.class.getName());
+
+ when(attributesService.findAll(deviceId, DataConstants.CLIENT_SCOPE)).thenReturn(Collections.emptyList());
+
+ initActorSystem();
+ Thread.sleep(1000);
+ actorService.process(new BasicToDeviceActorSessionMsg(device, msg));
+
+ // Check that device data was saved to DB;
+ List<TsKvEntry> expected = new ArrayList<>();
+ expected.add(new BasicTsKvEntry(ts, entry1));
+ expected.add(new BasicTsKvEntry(ts, entry2));
+ verify(tsService, Mockito.timeout(5000)).save(DataConstants.DEVICE, deviceId, expected);
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/actors/DummySessionID.java b/application/src/test/java/org/thingsboard/server/actors/DummySessionID.java
new file mode 100644
index 0000000..ccb9138
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/actors/DummySessionID.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 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;
+
+import org.thingsboard.server.common.data.id.SessionId;
+
+public class DummySessionID implements SessionId {
+
+ @Override
+ public String toString() {
+ return id;
+ }
+
+ private final String id;
+
+ public DummySessionID(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public String toUidStr() {
+ return id;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ DummySessionID other = (DummySessionID) obj;
+ if (id == null) {
+ if (other.id != null)
+ return false;
+ } else if (!id.equals(other.id))
+ return false;
+ return true;
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
new file mode 100644
index 0000000..538e8f9
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
@@ -0,0 +1,375 @@
+/**
+ * Copyright © 2016 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 com.fasterxml.jackson.databind.ObjectMapper;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Header;
+import io.jsonwebtoken.Jwt;
+import io.jsonwebtoken.Jwts;
+import org.apache.commons.lang3.StringUtils;
+import org.hamcrest.Matcher;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.IntegrationTest;
+import org.springframework.boot.test.SpringApplicationContextLoader;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.mock.http.MockHttpInputMessage;
+import org.springframework.mock.http.MockHttpOutputMessage;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.ResultMatcher;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.context.WebApplicationContext;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
+import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.service.mail.MailService;
+import org.thingsboard.server.service.mail.TestMailService;
+import org.thingsboard.server.service.security.auth.rest.LoginRequest;
+import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
+
+@ActiveProfiles("test")
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(classes=AbstractControllerTest.class, loader=SpringApplicationContextLoader.class)
+@TestPropertySource("classpath:cassandra-test.properties")
+@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
+@Configuration
+@EnableAutoConfiguration
+@ComponentScan({"org.thingsboard.server"})
+@WebAppConfiguration
+@IntegrationTest("server.port:0")
+public abstract class AbstractControllerTest {
+
+ protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org";
+ private static final String SYS_ADMIN_PASSWORD = "sysadmin";
+
+ protected static final String TENANT_ADMIN_EMAIL = "tenant@thingsboard.org";
+ private static final String TENANT_ADMIN_PASSWORD = "tenant";
+
+ protected static final String CUSTOMER_USER_EMAIL = "customer@thingsboard.org";
+ private static final String CUSTOMER_USER_PASSWORD = "customer";
+
+ protected MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
+ MediaType.APPLICATION_JSON.getSubtype(),
+ Charset.forName("utf8"));
+
+
+ protected MockMvc mockMvc;
+
+ protected String token;
+ protected String refreshToken;
+ protected String username;
+
+ private TenantId tenantId;
+
+ @SuppressWarnings("rawtypes")
+ private HttpMessageConverter mappingJackson2HttpMessageConverter;
+
+ @Autowired
+ private WebApplicationContext webApplicationContext;
+
+ @Autowired
+ void setConverters(HttpMessageConverter<?>[] converters) {
+
+ this.mappingJackson2HttpMessageConverter = Arrays.asList(converters).stream().filter(
+ hmc -> hmc instanceof MappingJackson2HttpMessageConverter).findAny().get();
+
+ Assert.assertNotNull("the JSON message converter must not be null",
+ this.mappingJackson2HttpMessageConverter);
+ }
+
+ @Before
+ public void setup() throws Exception {
+ if (this.mockMvc == null) {
+ this.mockMvc = webAppContextSetup(webApplicationContext)
+ .apply(springSecurity()).build();
+ }
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+ tenantId = savedTenant.getId();
+
+ User tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(tenantId);
+ tenantAdmin.setEmail(TENANT_ADMIN_EMAIL);
+
+ createUserAndLogin(tenantAdmin, TENANT_ADMIN_PASSWORD);
+
+ Customer customer = new Customer();
+ customer.setTitle("Customer");
+ customer.setTenantId(tenantId);
+ Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+ User customerUser = new User();
+ customerUser.setAuthority(Authority.CUSTOMER_USER);
+ customerUser.setTenantId(tenantId);
+ customerUser.setCustomerId(savedCustomer.getId());
+ customerUser.setEmail(CUSTOMER_USER_EMAIL);
+
+ createUserAndLogin(customerUser, CUSTOMER_USER_PASSWORD);
+
+ logout();
+ }
+
+ @After
+ public void teardown() throws Exception {
+ loginSysAdmin();
+ doDelete("/api/tenant/"+tenantId.getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ protected void loginSysAdmin() throws Exception {
+ login(SYS_ADMIN_EMAIL, SYS_ADMIN_PASSWORD);
+ }
+
+ protected void loginTenantAdmin() throws Exception {
+ login(TENANT_ADMIN_EMAIL, TENANT_ADMIN_PASSWORD);
+ }
+
+ protected void loginCustomerUser() throws Exception {
+ login(CUSTOMER_USER_EMAIL, CUSTOMER_USER_PASSWORD);
+ }
+
+ protected User createUserAndLogin(User user, String password) throws Exception {
+ User savedUser = doPost("/api/user", user, User.class);
+ logout();
+ doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken)
+ .andExpect(status().isPermanentRedirect())
+ .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
+ JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", "activateToken", TestMailService.currentActivateToken, "password", password).andExpect(status().isOk()), JsonNode.class);
+ validateAndSetJwtToken(tokenInfo, user.getEmail());
+ return savedUser;
+ }
+
+ protected void login(String username, String password) throws Exception {
+ this.token = null;
+ this.refreshToken = null;
+ this.username = null;
+ JsonNode tokenInfo = readResponse(doPost("/api/auth/login", new LoginRequest(username, password)).andExpect(status().isOk()), JsonNode.class);
+ validateAndSetJwtToken(tokenInfo, username);
+ }
+
+ protected void refreshToken() throws Exception {
+ this.token = null;
+ JsonNode tokenInfo = readResponse(doPost("/api/auth/token", new RefreshTokenRequest(this.refreshToken)).andExpect(status().isOk()), JsonNode.class);
+ validateAndSetJwtToken(tokenInfo, this.username);
+ }
+
+ protected void validateAndSetJwtToken(JsonNode tokenInfo, String username) {
+ Assert.assertNotNull(tokenInfo);
+ Assert.assertTrue(tokenInfo.has("token"));
+ Assert.assertTrue(tokenInfo.has("refreshToken"));
+ String token = tokenInfo.get("token").asText();
+ String refreshToken = tokenInfo.get("refreshToken").asText();
+ validateJwtToken(token, username);
+ validateJwtToken(refreshToken, username);
+ this.token = token;
+ this.refreshToken = refreshToken;
+ this.username = username;
+ }
+
+ protected void validateJwtToken(String token, String username) {
+ Assert.assertNotNull(token);
+ Assert.assertFalse(token.isEmpty());
+ int i = token.lastIndexOf('.');
+ Assert.assertTrue(i>0);
+ String withoutSignature = token.substring(0, i+1);
+ Jwt<Header,Claims> jwsClaims = Jwts.parser().parseClaimsJwt(withoutSignature);
+ Claims claims = jwsClaims.getBody();
+ String subject = claims.getSubject();
+ Assert.assertEquals(username, subject);
+ }
+
+ protected void logout() throws Exception {
+ this.token = null;
+ this.refreshToken = null;
+ this.username = null;
+ }
+
+ protected void setJwtToken(MockHttpServletRequestBuilder request) {
+ if (this.token != null) {
+ request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token);
+ }
+ }
+
+ protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception {
+ MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables);
+ setJwtToken(getRequest);
+ return mockMvc.perform(getRequest);
+ }
+
+ protected <T> T doGet(String urlTemplate, Class<T> responseClass, Object... urlVariables) throws Exception {
+ return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass);
+ }
+
+ protected <T> T doGetTyped(String urlTemplate, TypeReference<T> responseType, Object... urlVariables) throws Exception {
+ return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseType);
+ }
+
+ protected <T> T doGetTypedWithPageLink(String urlTemplate, TypeReference<T> responseType,
+ TextPageLink pageLink,
+ Object... urlVariables) throws Exception {
+ List<Object> pageLinkVariables = new ArrayList<>();
+ urlTemplate += "limit={limit}";
+ pageLinkVariables.add(pageLink.getLimit());
+ if (StringUtils.isNotEmpty(pageLink.getTextSearch())) {
+ urlTemplate += "&textSearch={textSearch}";
+ pageLinkVariables.add(pageLink.getTextSearch());
+ }
+ if (pageLink.getIdOffset() != null) {
+ urlTemplate += "&idOffset={idOffset}";
+ pageLinkVariables.add(pageLink.getIdOffset().toString());
+ }
+ if (StringUtils.isNotEmpty(pageLink.getTextOffset())) {
+ urlTemplate += "&textOffset={textOffset}";
+ pageLinkVariables.add(pageLink.getTextOffset());
+ }
+
+ Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()];
+ System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length);
+ System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size());
+
+ return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType);
+ }
+
+ protected <T> T doPost(String urlTemplate, Class<T> responseClass, String... params) throws Exception {
+ return readResponse(doPost(urlTemplate, params).andExpect(status().isOk()), responseClass);
+ }
+
+ protected <T> T doPost(String urlTemplate, T content, Class<T> responseClass, String... params) throws Exception {
+ return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass);
+ }
+
+ protected <T> T doDelete(String urlTemplate, Class<T> responseClass, String... params) throws Exception {
+ return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass);
+ }
+
+ protected ResultActions doPost(String urlTemplate, String... params) throws Exception {
+ MockHttpServletRequestBuilder postRequest = post(urlTemplate);
+ setJwtToken(postRequest);
+ populateParams(postRequest, params);
+ return mockMvc.perform(postRequest);
+ }
+
+ protected <T> ResultActions doPost(String urlTemplate, T content, String... params) throws Exception {
+ MockHttpServletRequestBuilder postRequest = post(urlTemplate);
+ setJwtToken(postRequest);
+ String json = json(content);
+ postRequest.contentType(contentType).content(json);
+ populateParams(postRequest, params);
+ return mockMvc.perform(postRequest);
+ }
+
+ protected ResultActions doDelete(String urlTemplate, String... params) throws Exception {
+ MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate);
+ setJwtToken(deleteRequest);
+ populateParams(deleteRequest, params);
+ return mockMvc.perform(deleteRequest);
+ }
+
+ protected void populateParams(MockHttpServletRequestBuilder request, String... params) {
+ if (params != null && params.length > 0) {
+ Assert.assertEquals(params.length % 2, 0);
+ MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<String, String>();
+ for (int i=0;i<params.length;i+=2) {
+ paramsMap.add(params[i], params[i+1]);
+ }
+ request.params(paramsMap);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ protected String json(Object o) throws IOException {
+ MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage();
+ this.mappingJackson2HttpMessageConverter.write(
+ o, MediaType.APPLICATION_JSON, mockHttpOutputMessage);
+ return mockHttpOutputMessage.getBodyAsString();
+ }
+
+ @SuppressWarnings("unchecked")
+ protected <T> T readResponse(ResultActions result, Class<T> responseClass) throws Exception {
+ byte[] content = result.andReturn().getResponse().getContentAsByteArray();
+ MockHttpInputMessage mockHttpInputMessage = new MockHttpInputMessage(content);
+ return (T) this.mappingJackson2HttpMessageConverter.read(responseClass, mockHttpInputMessage);
+ }
+
+ protected <T> T readResponse(ResultActions result, TypeReference<T> type) throws Exception {
+ byte[] content = result.andReturn().getResponse().getContentAsByteArray();
+ ObjectMapper mapper = new ObjectMapper();
+ return mapper.readerFor(type).readValue(content);
+ }
+
+ class IdComparator<D extends BaseData<? extends UUIDBased>> implements Comparator<D> {
+ @Override
+ public int compare(D o1, D o2) {
+ return o1.getId().getId().compareTo(o2.getId().getId());
+ }
+ }
+
+ protected static <T> ResultMatcher statusReason(Matcher<T> matcher) {
+ return jsonPath("$.message", matcher);
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java
new file mode 100644
index 0000000..6712aa2
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java
@@ -0,0 +1,146 @@
+/**
+ * Copyright © 2016 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 static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.thingsboard.server.common.data.AdminSettings;
+import org.junit.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+public class AdminControllerTest extends AbstractControllerTest {
+
+ @Test
+ public void testFindAdminSettingsByKey() throws Exception {
+ loginSysAdmin();
+ doGet("/api/admin/settings/general")
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(contentType))
+ .andExpect(jsonPath("$.id", notNullValue()))
+ .andExpect(jsonPath("$.key", is("general")))
+ .andExpect(jsonPath("$.jsonValue.baseUrl", is("http://localhost:8080")));
+
+ doGet("/api/admin/settings/mail")
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(contentType))
+ .andExpect(jsonPath("$.id", notNullValue()))
+ .andExpect(jsonPath("$.key", is("mail")))
+ .andExpect(jsonPath("$.jsonValue.smtpProtocol", is("smtp")))
+ .andExpect(jsonPath("$.jsonValue.smtpHost", is("localhost")))
+ .andExpect(jsonPath("$.jsonValue.smtpPort", is("25")));
+
+ doGet("/api/admin/settings/unknown")
+ .andExpect(status().isNotFound());
+
+ }
+
+ @Test
+ public void testSaveAdminSettings() throws Exception {
+ loginSysAdmin();
+ AdminSettings adminSettings = doGet("/api/admin/settings/general", AdminSettings.class);
+
+ JsonNode jsonValue = adminSettings.getJsonValue();
+ ((ObjectNode) jsonValue).put("baseUrl", "http://myhost.org");
+ adminSettings.setJsonValue(jsonValue);
+
+ doPost("/api/admin/settings", adminSettings).andExpect(status().isOk());
+
+ doGet("/api/admin/settings/general")
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(contentType))
+ .andExpect(jsonPath("$.jsonValue.baseUrl", is("http://myhost.org")));
+
+ ((ObjectNode) jsonValue).put("baseUrl", "http://localhost:8080");
+ adminSettings.setJsonValue(jsonValue);
+
+ doPost("/api/admin/settings", adminSettings)
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testCreateAdminSettings() throws Exception {
+ loginSysAdmin();
+
+ AdminSettings adminSettings = new AdminSettings();
+ adminSettings.setKey("someKey");
+ adminSettings.setJsonValue(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+
+ doPost("/api/admin/settings", adminSettings)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("is prohibited")));
+ }
+
+ @Test
+ public void testSaveAdminSettingsWithEmptyKey() throws Exception {
+ loginSysAdmin();
+ AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
+ adminSettings.setKey(null);
+ doPost("/api/admin/settings", adminSettings)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Key should be specified")));
+ }
+
+ @Test
+ public void testChangeAdminSettingsKey() throws Exception {
+ loginSysAdmin();
+ AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
+ adminSettings.setKey("newKey");
+ doPost("/api/admin/settings", adminSettings)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("is prohibited")));
+ }
+
+ @Test
+ public void testSaveAdminSettingsWithNewJsonStructure() throws Exception {
+ loginSysAdmin();
+ AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
+ JsonNode json = adminSettings.getJsonValue();
+ ((ObjectNode) json).put("newKey", "my new value");
+ adminSettings.setJsonValue(json);
+ doPost("/api/admin/settings", adminSettings)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Provided json structure is different")));
+ }
+
+ @Test
+ public void testSaveAdminSettingsWithNonTextValue() throws Exception {
+ loginSysAdmin();
+ AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
+ JsonNode json = adminSettings.getJsonValue();
+ ((ObjectNode) json).put("timeout", 10000L);
+ adminSettings.setJsonValue(json);
+ doPost("/api/admin/settings", adminSettings)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Provided json structure can't contain non-text values")));
+ }
+
+ @Test
+ public void testSendTestMail() throws Exception {
+ loginSysAdmin();
+ AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
+ doPost("/api/admin/settings/testMail", adminSettings)
+ .andExpect(status().isOk());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java
new file mode 100644
index 0000000..9fb9fd4
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright © 2016 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 static org.hamcrest.Matchers.is;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.thingsboard.server.common.data.security.Authority;
+import org.junit.Test;
+
+public class AuthControllerTest extends AbstractControllerTest {
+
+ @Test
+ public void testGetUser() throws Exception {
+
+ doGet("/api/auth/user")
+ .andExpect(status().isUnauthorized());
+
+ loginSysAdmin();
+ doGet("/api/auth/user")
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
+ .andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
+
+ loginTenantAdmin();
+ doGet("/api/auth/user")
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.authority",is(Authority.TENANT_ADMIN.name())))
+ .andExpect(jsonPath("$.email",is(TENANT_ADMIN_EMAIL)));
+
+ loginCustomerUser();
+ doGet("/api/auth/user")
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.authority",is(Authority.CUSTOMER_USER.name())))
+ .andExpect(jsonPath("$.email",is(CUSTOMER_USER_EMAIL)));
+ }
+
+ @Test
+ public void testLoginLogout() throws Exception {
+ loginSysAdmin();
+ doGet("/api/auth/user")
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
+ .andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
+
+ logout();
+ doGet("/api/auth/user")
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ public void testRefreshToken() throws Exception {
+ loginSysAdmin();
+ doGet("/api/auth/user")
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
+ .andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
+
+ refreshToken();
+ doGet("/api/auth/user")
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
+ .andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/ComponentDescriptorControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/ComponentDescriptorControllerTest.java
new file mode 100644
index 0000000..b901966
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/ComponentDescriptorControllerTest.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright © 2016 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 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.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;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class ComponentDescriptorControllerTest extends AbstractControllerTest {
+
+ private static final int AMOUNT_OF_DEFAULT_PLUGINS_DESCRIPTORS = 5;
+ private Tenant savedTenant;
+ private User tenantAdmin;
+
+ @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);
+
+ tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(savedTenant.getId());
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ loginSysAdmin();
+
+ doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testGetByClazz() throws Exception {
+ ComponentDescriptor descriptor =
+ doGet("/api/component/" + TelemetryStoragePlugin.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(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>>() {
+ });
+
+ Assert.assertNotNull(descriptors);
+ Assert.assertEquals(AMOUNT_OF_DEFAULT_PLUGINS_DESCRIPTORS, descriptors.size());
+
+ 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/controller/ControllerTestSuite.java b/application/src/test/java/org/thingsboard/server/controller/ControllerTestSuite.java
new file mode 100644
index 0000000..9ac768c
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/ControllerTestSuite.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 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 org.cassandraunit.dataset.cql.ClassPathCQLDataSet;
+import org.junit.ClassRule;
+import org.junit.extensions.cpsuite.ClasspathSuite;
+import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters;
+import org.junit.runner.RunWith;
+import org.thingsboard.server.dao.CustomCassandraCQLUnit;
+
+import java.util.Arrays;
+
+@RunWith(ClasspathSuite.class)
+@ClassnameFilters({"org.thingsboard.server.controller.*Test"})
+public class ControllerTestSuite {
+
+ @ClassRule
+ public static CustomCassandraCQLUnit cassandraUnit =
+ new CustomCassandraCQLUnit(Arrays.asList(
+ new ClassPathCQLDataSet("schema.cql", false, false),
+ new ClassPathCQLDataSet("system-data.cql", false, false),
+ new ClassPathCQLDataSet("system-test.cql", false, false)),
+ "cassandra-test.yaml", 30000l);
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
new file mode 100644
index 0000000..b71a2d1
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
@@ -0,0 +1,366 @@
+/**
+ * Copyright © 2016 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 static org.hamcrest.Matchers.containsString;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+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.security.Authority;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+public class CustomerControllerTest extends AbstractControllerTest {
+
+ private IdComparator<Customer> idComparator = new IdComparator<>();
+
+ @Test
+ public void testSaveCustomer() throws Exception {
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ User tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(savedTenant.getId());
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+
+ Customer customer = new Customer();
+ customer.setTitle("My customer");
+ Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+ Assert.assertNotNull(savedCustomer);
+ Assert.assertNotNull(savedCustomer.getId());
+ Assert.assertTrue(savedCustomer.getCreatedTime() > 0);
+ Assert.assertEquals(customer.getTitle(), savedCustomer.getTitle());
+ savedCustomer.setTitle("My new customer");
+ doPost("/api/customer", savedCustomer, Customer.class);
+
+ Customer foundCustomer = doGet("/api/customer/"+savedCustomer.getId().getId().toString(), Customer.class);
+ Assert.assertEquals(foundCustomer.getTitle(), savedCustomer.getTitle());
+
+ doDelete("/api/customer/"+savedCustomer.getId().getId().toString())
+ .andExpect(status().isOk());
+
+ loginSysAdmin();
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testFindCustomerById() throws Exception {
+
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ User tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(savedTenant.getId());
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+
+ Customer customer = new Customer();
+ customer.setTitle("My customer");
+ Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+ Customer foundCustomer = doGet("/api/customer/"+savedCustomer.getId().getId().toString(), Customer.class);
+ Assert.assertNotNull(foundCustomer);
+ Assert.assertEquals(savedCustomer, foundCustomer);
+
+ doDelete("/api/customer/"+savedCustomer.getId().getId().toString())
+ .andExpect(status().isOk());
+
+ loginSysAdmin();
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testDeleteCustomer() throws Exception {
+
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ User tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(savedTenant.getId());
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+
+ Customer customer = new Customer();
+ customer.setTitle("My customer");
+ Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+ doDelete("/api/customer/"+savedCustomer.getId().getId().toString())
+ .andExpect(status().isOk());
+
+ doGet("/api/customer/"+savedCustomer.getId().getId().toString())
+ .andExpect(status().isNotFound());
+
+ loginSysAdmin();
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSaveCustomerWithEmptyTitle() throws Exception {
+
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ User tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(savedTenant.getId());
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+
+ Customer customer = new Customer();
+ doPost("/api/customer", customer)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Customer title should be specified")));
+
+ loginSysAdmin();
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSaveCustomerWithInvalidEmail() throws Exception {
+
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ User tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(savedTenant.getId());
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+
+ Customer customer = new Customer();
+ customer.setTitle("My customer");
+ customer.setEmail("invalid@mail");
+ doPost("/api/customer", customer)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Invalid email address format 'invalid@mail'")));
+
+ loginSysAdmin();
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testFindCustomers() throws Exception {
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ TenantId tenantId = savedTenant.getId();
+
+ User tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(tenantId);
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+
+ List<Customer> customers = new ArrayList<>();
+ for (int i=0;i<135;i++) {
+ Customer customer = new Customer();
+ customer.setTenantId(tenantId);
+ customer.setTitle("Customer"+i);
+ customers.add(doPost("/api/customer", customer, Customer.class));
+ }
+
+ List<Customer> loadedCustomers = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(23);
+ TextPageData<Customer> pageData = null;
+ do {
+ pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference<TextPageData<Customer>>(){}, pageLink);
+ loadedCustomers.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(customers, idComparator);
+ Collections.sort(loadedCustomers, idComparator);
+
+ Assert.assertEquals(customers, loadedCustomers);
+
+ loginSysAdmin();
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testFindCustomersByTitle() throws Exception {
+
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ TenantId tenantId = savedTenant.getId();
+
+ User tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(tenantId);
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+
+ String title1 = "Customer title 1";
+ List<Customer> customersTitle1 = new ArrayList<>();
+ for (int i=0;i<143;i++) {
+ Customer customer = new Customer();
+ customer.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+ String title = title1+suffix;
+ title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+ customer.setTitle(title);
+ customersTitle1.add(doPost("/api/customer", customer, Customer.class));
+ }
+ String title2 = "Customer title 2";
+ List<Customer> customersTitle2 = new ArrayList<>();
+ for (int i=0;i<175;i++) {
+ Customer customer = new Customer();
+ customer.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+ String title = title2+suffix;
+ title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+ customer.setTitle(title);
+ customersTitle2.add(doPost("/api/customer", customer, Customer.class));
+ }
+
+ List<Customer> loadedCustomersTitle1 = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(15, title1);
+ TextPageData<Customer> pageData = null;
+ do {
+ pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference<TextPageData<Customer>>(){}, pageLink);
+ loadedCustomersTitle1.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(customersTitle1, idComparator);
+ Collections.sort(loadedCustomersTitle1, idComparator);
+
+ Assert.assertEquals(customersTitle1, loadedCustomersTitle1);
+
+ List<Customer> loadedCustomersTitle2 = new ArrayList<>();
+ pageLink = new TextPageLink(4, title2);
+ do {
+ pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference<TextPageData<Customer>>(){}, pageLink);
+ loadedCustomersTitle2.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(customersTitle2, idComparator);
+ Collections.sort(loadedCustomersTitle2, idComparator);
+
+ Assert.assertEquals(customersTitle2, loadedCustomersTitle2);
+
+ for (Customer customer : loadedCustomersTitle1) {
+ doDelete("/api/customer/"+customer.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ pageLink = new TextPageLink(4, title1);
+ pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference<TextPageData<Customer>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ for (Customer customer : loadedCustomersTitle2) {
+ doDelete("/api/customer/"+customer.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ pageLink = new TextPageLink(4, title2);
+ pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference<TextPageData<Customer>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ loginSysAdmin();
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java
new file mode 100644
index 0000000..7520807
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java
@@ -0,0 +1,431 @@
+/**
+ * Copyright © 2016 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 static org.hamcrest.Matchers.containsString;
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Dashboard;
+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.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.core.type.TypeReference;
+
+public class DashboardControllerTest extends AbstractControllerTest {
+
+ private IdComparator<Dashboard> idComparator = new IdComparator<>();
+
+ private Tenant savedTenant;
+ private User tenantAdmin;
+
+ @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);
+
+ tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(savedTenant.getId());
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ loginSysAdmin();
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSaveDashboard() throws Exception {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTitle("My dashboard");
+ Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+
+ Assert.assertNotNull(savedDashboard);
+ Assert.assertNotNull(savedDashboard.getId());
+ Assert.assertTrue(savedDashboard.getCreatedTime() > 0);
+ Assert.assertEquals(savedTenant.getId(), savedDashboard.getTenantId());
+ Assert.assertNotNull(savedDashboard.getCustomerId());
+ Assert.assertEquals(NULL_UUID, savedDashboard.getCustomerId().getId());
+ Assert.assertEquals(dashboard.getTitle(), savedDashboard.getTitle());
+
+ savedDashboard.setTitle("My new dashboard");
+ doPost("/api/dashboard", savedDashboard, Dashboard.class);
+
+ Dashboard foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
+ Assert.assertEquals(foundDashboard.getTitle(), savedDashboard.getTitle());
+ }
+
+ @Test
+ public void testFindDashboardById() throws Exception {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTitle("My dashboard");
+ Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+ Dashboard foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
+ Assert.assertNotNull(foundDashboard);
+ Assert.assertEquals(savedDashboard, foundDashboard);
+ }
+
+ @Test
+ public void testDeleteDashboard() throws Exception {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTitle("My dashboard");
+ Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+
+ doDelete("/api/dashboard/"+savedDashboard.getId().getId().toString())
+ .andExpect(status().isOk());
+
+ doGet("/api/dashboard/"+savedDashboard.getId().getId().toString())
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ public void testSaveDashboardWithEmptyTitle() throws Exception {
+ Dashboard dashboard = new Dashboard();
+ doPost("/api/dashboard", dashboard)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Dashboard title should be specified")));
+ }
+
+ @Test
+ public void testAssignUnassignDashboardToCustomer() throws Exception {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTitle("My dashboard");
+ Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+
+ Customer customer = new Customer();
+ customer.setTitle("My customer");
+ Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+ Dashboard assignedDashboard = doPost("/api/customer/" + savedCustomer.getId().getId().toString()
+ + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
+ Assert.assertEquals(savedCustomer.getId(), assignedDashboard.getCustomerId());
+
+ Dashboard foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
+ Assert.assertEquals(savedCustomer.getId(), foundDashboard.getCustomerId());
+
+ Dashboard unassignedDashboard =
+ doDelete("/api/customer/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
+ Assert.assertEquals(ModelConstants.NULL_UUID, unassignedDashboard.getCustomerId().getId());
+
+ foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
+ Assert.assertEquals(ModelConstants.NULL_UUID, foundDashboard.getCustomerId().getId());
+ }
+
+ @Test
+ public void testAssignDashboardToNonExistentCustomer() throws Exception {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTitle("My dashboard");
+ Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+
+ doPost("/api/customer/" + UUIDs.timeBased().toString()
+ + "/dashboard/" + savedDashboard.getId().getId().toString())
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ public void testAssignDashboardToCustomerFromDifferentTenant() throws Exception {
+ loginSysAdmin();
+
+ Tenant tenant2 = new Tenant();
+ tenant2.setTitle("Different tenant");
+ Tenant savedTenant2 = doPost("/api/tenant", tenant2, Tenant.class);
+ Assert.assertNotNull(savedTenant2);
+
+ User tenantAdmin2 = new User();
+ tenantAdmin2.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin2.setTenantId(savedTenant2.getId());
+ tenantAdmin2.setEmail("tenant3@thingsboard.org");
+ tenantAdmin2.setFirstName("Joe");
+ tenantAdmin2.setLastName("Downs");
+
+ tenantAdmin2 = createUserAndLogin(tenantAdmin2, "testPassword1");
+
+ Customer customer = new Customer();
+ customer.setTitle("Different customer");
+ Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+ login(tenantAdmin.getEmail(), "testPassword1");
+
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTitle("My dashboard");
+ Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+
+ doPost("/api/customer/" + savedCustomer.getId().getId().toString()
+ + "/dashboard/" + savedDashboard.getId().getId().toString())
+ .andExpect(status().isForbidden());
+
+ loginSysAdmin();
+
+ doDelete("/api/tenant/"+savedTenant2.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testFindTenantDashboards() throws Exception {
+ List<Dashboard> dashboards = new ArrayList<>();
+ for (int i=0;i<173;i++) {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTitle("Dashboard"+i);
+ dashboards.add(doPost("/api/dashboard", dashboard, Dashboard.class));
+ }
+ List<Dashboard> loadedDashboards = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(24);
+ TextPageData<Dashboard> pageData = null;
+ do {
+ pageData = doGetTypedWithPageLink("/api/tenant/dashboards?",
+ new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+ loadedDashboards.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(dashboards, idComparator);
+ Collections.sort(loadedDashboards, idComparator);
+
+ Assert.assertEquals(dashboards, loadedDashboards);
+ }
+
+ @Test
+ public void testFindTenantDashboardsByTitle() throws Exception {
+ String title1 = "Dashboard title 1";
+ List<Dashboard> dashboardsTitle1 = new ArrayList<>();
+ for (int i=0;i<134;i++) {
+ Dashboard dashboard = new Dashboard();
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+ String title = title1+suffix;
+ title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+ dashboard.setTitle(title);
+ dashboardsTitle1.add(doPost("/api/dashboard", dashboard, Dashboard.class));
+ }
+ String title2 = "Dashboard title 2";
+ List<Dashboard> dashboardsTitle2 = new ArrayList<>();
+ for (int i=0;i<112;i++) {
+ Dashboard dashboard = new Dashboard();
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+ String title = title2+suffix;
+ title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+ dashboard.setTitle(title);
+ dashboardsTitle2.add(doPost("/api/dashboard", dashboard, Dashboard.class));
+ }
+
+ List<Dashboard> loadedDashboardsTitle1 = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(15, title1);
+ TextPageData<Dashboard> pageData = null;
+ do {
+ pageData = doGetTypedWithPageLink("/api/tenant/dashboards?",
+ new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+ loadedDashboardsTitle1.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(dashboardsTitle1, idComparator);
+ Collections.sort(loadedDashboardsTitle1, idComparator);
+
+ Assert.assertEquals(dashboardsTitle1, loadedDashboardsTitle1);
+
+ List<Dashboard> loadedDashboardsTitle2 = new ArrayList<>();
+ pageLink = new TextPageLink(4, title2);
+ do {
+ pageData = doGetTypedWithPageLink("/api/tenant/dashboards?",
+ new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+ loadedDashboardsTitle2.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(dashboardsTitle2, idComparator);
+ Collections.sort(loadedDashboardsTitle2, idComparator);
+
+ Assert.assertEquals(dashboardsTitle2, loadedDashboardsTitle2);
+
+ for (Dashboard dashboard : loadedDashboardsTitle1) {
+ doDelete("/api/dashboard/"+dashboard.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ pageLink = new TextPageLink(4, title1);
+ pageData = doGetTypedWithPageLink("/api/tenant/dashboards?",
+ new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ for (Dashboard dashboard : loadedDashboardsTitle2) {
+ doDelete("/api/dashboard/"+dashboard.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ pageLink = new TextPageLink(4, title2);
+ pageData = doGetTypedWithPageLink("/api/tenant/dashboards?",
+ new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+ }
+
+ @Test
+ public void testFindCustomerDashboards() throws Exception {
+ Customer customer = new Customer();
+ customer.setTitle("Test customer");
+ customer = doPost("/api/customer", customer, Customer.class);
+ CustomerId customerId = customer.getId();
+
+ List<Dashboard> dashboards = new ArrayList<>();
+ for (int i=0;i<173;i++) {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTitle("Dashboard"+i);
+ dashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+ dashboards.add(doPost("/api/customer/" + customerId.getId().toString()
+ + "/dashboard/" + dashboard.getId().getId().toString(), Dashboard.class));
+ }
+
+ List<Dashboard> loadedDashboards = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(21);
+ TextPageData<Dashboard> pageData = null;
+ do {
+ pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?",
+ new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+ loadedDashboards.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(dashboards, idComparator);
+ Collections.sort(loadedDashboards, idComparator);
+
+ Assert.assertEquals(dashboards, loadedDashboards);
+ }
+
+ @Test
+ public void testFindCustomerDashboardsByTitle() throws Exception {
+ Customer customer = new Customer();
+ customer.setTitle("Test customer");
+ customer = doPost("/api/customer", customer, Customer.class);
+ CustomerId customerId = customer.getId();
+
+ String title1 = "Dashboard title 1";
+ List<Dashboard> dashboardsTitle1 = new ArrayList<>();
+ for (int i=0;i<125;i++) {
+ Dashboard dashboard = new Dashboard();
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+ String title = title1+suffix;
+ title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+ dashboard.setTitle(title);
+ dashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+ dashboardsTitle1.add(doPost("/api/customer/" + customerId.getId().toString()
+ + "/dashboard/" + dashboard.getId().getId().toString(), Dashboard.class));
+ }
+ String title2 = "Dashboard title 2";
+ List<Dashboard> dashboardsTitle2 = new ArrayList<>();
+ for (int i=0;i<143;i++) {
+ Dashboard dashboard = new Dashboard();
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+ String title = title2+suffix;
+ title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+ dashboard.setTitle(title);
+ dashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+ dashboardsTitle2.add(doPost("/api/customer/" + customerId.getId().toString()
+ + "/dashboard/" + dashboard.getId().getId().toString(), Dashboard.class));
+ }
+
+ List<Dashboard> loadedDashboardsTitle1 = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(18, title1);
+ TextPageData<Dashboard> pageData = null;
+ do {
+ pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?",
+ new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+ loadedDashboardsTitle1.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(dashboardsTitle1, idComparator);
+ Collections.sort(loadedDashboardsTitle1, idComparator);
+
+ Assert.assertEquals(dashboardsTitle1, loadedDashboardsTitle1);
+
+ List<Dashboard> loadedDashboardsTitle2 = new ArrayList<>();
+ pageLink = new TextPageLink(7, title2);
+ do {
+ pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?",
+ new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+ loadedDashboardsTitle2.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(dashboardsTitle2, idComparator);
+ Collections.sort(loadedDashboardsTitle2, idComparator);
+
+ Assert.assertEquals(dashboardsTitle2, loadedDashboardsTitle2);
+
+ for (Dashboard dashboard : loadedDashboardsTitle1) {
+ doDelete("/api/customer/dashboard/" + dashboard.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ pageLink = new TextPageLink(5, title1);
+ pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?",
+ new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ for (Dashboard dashboard : loadedDashboardsTitle2) {
+ doDelete("/api/customer/dashboard/" + dashboard.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ pageLink = new TextPageLink(9, title2);
+ pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?",
+ new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
new file mode 100644
index 0000000..50ceb75
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
@@ -0,0 +1,549 @@
+/**
+ * Copyright © 2016 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 static org.hamcrest.Matchers.containsString;
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang3.RandomStringUtils;
+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.DeviceCredentialsId;
+import org.thingsboard.server.common.data.id.DeviceId;
+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.DeviceCredentials;
+import org.thingsboard.server.common.data.security.DeviceCredentialsType;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.core.type.TypeReference;
+
+public class DeviceControllerTest extends AbstractControllerTest {
+
+ private IdComparator<Device> idComparator = new IdComparator<>();
+
+ private Tenant savedTenant;
+ private User tenantAdmin;
+
+ @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);
+
+ tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(savedTenant.getId());
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ loginSysAdmin();
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSaveDevice() throws Exception {
+ Device device = new Device();
+ device.setName("My device");
+ Device savedDevice = doPost("/api/device", device, Device.class);
+
+ Assert.assertNotNull(savedDevice);
+ Assert.assertNotNull(savedDevice.getId());
+ Assert.assertTrue(savedDevice.getCreatedTime() > 0);
+ Assert.assertEquals(savedTenant.getId(), savedDevice.getTenantId());
+ Assert.assertNotNull(savedDevice.getCustomerId());
+ Assert.assertEquals(NULL_UUID, savedDevice.getCustomerId().getId());
+ Assert.assertEquals(device.getName(), savedDevice.getName());
+
+ DeviceCredentials deviceCredentials =
+ doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+
+ Assert.assertNotNull(deviceCredentials);
+ Assert.assertNotNull(deviceCredentials.getId());
+ Assert.assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
+ Assert.assertEquals(DeviceCredentialsType.ACCESS_TOKEN, deviceCredentials.getCredentialsType());
+ Assert.assertNotNull(deviceCredentials.getCredentialsId());
+ Assert.assertEquals(20, deviceCredentials.getCredentialsId().length());
+
+ savedDevice.setName("My new device");
+ doPost("/api/device", savedDevice, Device.class);
+
+ Device foundDevice = doGet("/api/device/" + savedDevice.getId().getId().toString(), Device.class);
+ Assert.assertEquals(foundDevice.getName(), savedDevice.getName());
+ }
+
+ @Test
+ public void testFindDeviceById() throws Exception {
+ Device device = new Device();
+ device.setName("My device");
+ Device savedDevice = doPost("/api/device", device, Device.class);
+ Device foundDevice = doGet("/api/device/" + savedDevice.getId().getId().toString(), Device.class);
+ Assert.assertNotNull(foundDevice);
+ Assert.assertEquals(savedDevice, foundDevice);
+ }
+
+ @Test
+ public void testDeleteDevice() throws Exception {
+ Device device = new Device();
+ device.setName("My device");
+ Device savedDevice = doPost("/api/device", device, Device.class);
+
+ doDelete("/api/device/"+savedDevice.getId().getId().toString())
+ .andExpect(status().isOk());
+
+ doGet("/api/device/"+savedDevice.getId().getId().toString())
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ public void testSaveDeviceWithEmptyName() throws Exception {
+ Device device = new Device();
+ doPost("/api/device", device)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Device name should be specified")));
+ }
+
+ @Test
+ public void testAssignUnassignDeviceToCustomer() throws Exception {
+ Device device = new Device();
+ device.setName("My device");
+ Device savedDevice = doPost("/api/device", device, Device.class);
+
+ Customer customer = new Customer();
+ customer.setTitle("My customer");
+ Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+ Device assignedDevice = doPost("/api/customer/" + savedCustomer.getId().getId().toString()
+ + "/device/" + savedDevice.getId().getId().toString(), Device.class);
+ Assert.assertEquals(savedCustomer.getId(), assignedDevice.getCustomerId());
+
+ Device foundDevice = doGet("/api/device/" + savedDevice.getId().getId().toString(), Device.class);
+ Assert.assertEquals(savedCustomer.getId(), foundDevice.getCustomerId());
+
+ Device unassignedDevice =
+ doDelete("/api/customer/device/" + savedDevice.getId().getId().toString(), Device.class);
+ Assert.assertEquals(ModelConstants.NULL_UUID, unassignedDevice.getCustomerId().getId());
+
+ foundDevice = doGet("/api/device/" + savedDevice.getId().getId().toString(), Device.class);
+ Assert.assertEquals(ModelConstants.NULL_UUID, foundDevice.getCustomerId().getId());
+ }
+
+ @Test
+ public void testAssignDeviceToNonExistentCustomer() throws Exception {
+ Device device = new Device();
+ device.setName("My device");
+ Device savedDevice = doPost("/api/device", device, Device.class);
+
+ doPost("/api/customer/" + UUIDs.timeBased().toString()
+ + "/device/" + savedDevice.getId().getId().toString())
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ public void testAssignDeviceToCustomerFromDifferentTenant() throws Exception {
+ loginSysAdmin();
+
+ Tenant tenant2 = new Tenant();
+ tenant2.setTitle("Different tenant");
+ Tenant savedTenant2 = doPost("/api/tenant", tenant2, Tenant.class);
+ Assert.assertNotNull(savedTenant2);
+
+ User tenantAdmin2 = new User();
+ tenantAdmin2.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin2.setTenantId(savedTenant2.getId());
+ tenantAdmin2.setEmail("tenant3@thingsboard.org");
+ tenantAdmin2.setFirstName("Joe");
+ tenantAdmin2.setLastName("Downs");
+
+ tenantAdmin2 = createUserAndLogin(tenantAdmin2, "testPassword1");
+
+ Customer customer = new Customer();
+ customer.setTitle("Different customer");
+ Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+ login(tenantAdmin.getEmail(), "testPassword1");
+
+ Device device = new Device();
+ device.setName("My device");
+ Device savedDevice = doPost("/api/device", device, Device.class);
+
+ doPost("/api/customer/" + savedCustomer.getId().getId().toString()
+ + "/device/" + savedDevice.getId().getId().toString())
+ .andExpect(status().isForbidden());
+
+ loginSysAdmin();
+
+ doDelete("/api/tenant/"+savedTenant2.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testFindDeviceCredentialsByDeviceId() throws Exception {
+ Device device = new Device();
+ device.setName("My device");
+ Device savedDevice = doPost("/api/device", device, Device.class);
+ DeviceCredentials deviceCredentials =
+ doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+ Assert.assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
+ }
+
+ @Test
+ public void testSaveDeviceCredentials() throws Exception {
+ Device device = new Device();
+ device.setName("My device");
+ Device savedDevice = doPost("/api/device", device, Device.class);
+ DeviceCredentials deviceCredentials =
+ doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+ Assert.assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
+ deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
+ deviceCredentials.setCredentialsId("access_token");
+ doPost("/api/device/credentials", deviceCredentials)
+ .andExpect(status().isOk());
+
+ DeviceCredentials foundDeviceCredentials =
+ doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+
+ Assert.assertEquals(deviceCredentials, foundDeviceCredentials);
+ }
+
+ @Test
+ public void testSaveDeviceCredentialsWithEmptyDevice() throws Exception {
+ DeviceCredentials deviceCredentials = new DeviceCredentials();
+ doPost("/api/device/credentials", deviceCredentials)
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ public void testSaveDeviceCredentialsWithEmptyCredentialsType() throws Exception {
+ Device device = new Device();
+ device.setName("My device");
+ Device savedDevice = doPost("/api/device", device, Device.class);
+ DeviceCredentials deviceCredentials =
+ doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+ deviceCredentials.setCredentialsType(null);
+ doPost("/api/device/credentials", deviceCredentials)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Device credentials type should be specified")));
+ }
+
+ @Test
+ public void testSaveDeviceCredentialsWithEmptyCredentialsId() throws Exception {
+ Device device = new Device();
+ device.setName("My device");
+ Device savedDevice = doPost("/api/device", device, Device.class);
+ DeviceCredentials deviceCredentials =
+ doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+ deviceCredentials.setCredentialsId(null);
+ doPost("/api/device/credentials", deviceCredentials)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Device credentials id should be specified")));
+ }
+
+ @Test
+ public void testSaveNonExistentDeviceCredentials() throws Exception {
+ Device device = new Device();
+ device.setName("My device");
+ Device savedDevice = doPost("/api/device", device, Device.class);
+ DeviceCredentials deviceCredentials =
+ doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+ DeviceCredentials newDeviceCredentials = new DeviceCredentials(new DeviceCredentialsId(UUIDs.timeBased()));
+ newDeviceCredentials.setCreatedTime(deviceCredentials.getCreatedTime());
+ newDeviceCredentials.setDeviceId(deviceCredentials.getDeviceId());
+ newDeviceCredentials.setCredentialsType(deviceCredentials.getCredentialsType());
+ newDeviceCredentials.setCredentialsId(deviceCredentials.getCredentialsId());
+ doPost("/api/device/credentials", newDeviceCredentials)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Unable to update non-existent device credentials")));
+ }
+
+ @Test
+ public void testSaveDeviceCredentialsWithNonExistentDevice() throws Exception {
+ Device device = new Device();
+ device.setName("My device");
+ Device savedDevice = doPost("/api/device", device, Device.class);
+ DeviceCredentials deviceCredentials =
+ doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+ deviceCredentials.setDeviceId(new DeviceId(UUIDs.timeBased()));
+ doPost("/api/device/credentials", deviceCredentials)
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ public void testSaveDeviceCredentialsWithInvalidCredemtialsIdLength() throws Exception {
+ Device device = new Device();
+ device.setName("My device");
+ Device savedDevice = doPost("/api/device", device, Device.class);
+ DeviceCredentials deviceCredentials =
+ doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+ deviceCredentials.setCredentialsId(RandomStringUtils.randomAlphanumeric(21));
+ doPost("/api/device/credentials", deviceCredentials)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Incorrect access token length")));
+ }
+
+ @Test
+ public void testFindTenantDevices() throws Exception {
+ List<Device> devices = new ArrayList<>();
+ for (int i=0;i<178;i++) {
+ Device device = new Device();
+ device.setName("Device"+i);
+ devices.add(doPost("/api/device", device, Device.class));
+ }
+ List<Device> loadedDevices = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(23);
+ TextPageData<Device> pageData = null;
+ do {
+ pageData = doGetTypedWithPageLink("/api/tenant/devices?",
+ new TypeReference<TextPageData<Device>>(){}, pageLink);
+ loadedDevices.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(devices, idComparator);
+ Collections.sort(loadedDevices, idComparator);
+
+ Assert.assertEquals(devices, loadedDevices);
+ }
+
+ @Test
+ public void testFindTenantDevicesByName() throws Exception {
+ String title1 = "Device title 1";
+ List<Device> devicesTitle1 = new ArrayList<>();
+ for (int i=0;i<143;i++) {
+ Device device = new Device();
+ String suffix = RandomStringUtils.randomAlphanumeric(15);
+ String name = title1+suffix;
+ name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+ device.setName(name);
+ devicesTitle1.add(doPost("/api/device", device, Device.class));
+ }
+ String title2 = "Device title 2";
+ List<Device> devicesTitle2 = new ArrayList<>();
+ for (int i=0;i<75;i++) {
+ Device device = new Device();
+ String suffix = RandomStringUtils.randomAlphanumeric(15);
+ String name = title2+suffix;
+ name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+ device.setName(name);
+ devicesTitle2.add(doPost("/api/device", device, Device.class));
+ }
+
+ List<Device> loadedDevicesTitle1 = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(15, title1);
+ TextPageData<Device> pageData = null;
+ do {
+ pageData = doGetTypedWithPageLink("/api/tenant/devices?",
+ new TypeReference<TextPageData<Device>>(){}, pageLink);
+ loadedDevicesTitle1.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(devicesTitle1, idComparator);
+ Collections.sort(loadedDevicesTitle1, idComparator);
+
+ Assert.assertEquals(devicesTitle1, loadedDevicesTitle1);
+
+ List<Device> loadedDevicesTitle2 = new ArrayList<>();
+ pageLink = new TextPageLink(4, title2);
+ do {
+ pageData = doGetTypedWithPageLink("/api/tenant/devices?",
+ new TypeReference<TextPageData<Device>>(){}, pageLink);
+ loadedDevicesTitle2.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(devicesTitle2, idComparator);
+ Collections.sort(loadedDevicesTitle2, idComparator);
+
+ Assert.assertEquals(devicesTitle2, loadedDevicesTitle2);
+
+ for (Device device : loadedDevicesTitle1) {
+ doDelete("/api/device/"+device.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ pageLink = new TextPageLink(4, title1);
+ pageData = doGetTypedWithPageLink("/api/tenant/devices?",
+ new TypeReference<TextPageData<Device>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ for (Device device : loadedDevicesTitle2) {
+ doDelete("/api/device/"+device.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ pageLink = new TextPageLink(4, title2);
+ pageData = doGetTypedWithPageLink("/api/tenant/devices?",
+ new TypeReference<TextPageData<Device>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+ }
+
+ @Test
+ public void testFindCustomerDevices() throws Exception {
+ Customer customer = new Customer();
+ customer.setTitle("Test customer");
+ customer = doPost("/api/customer", customer, Customer.class);
+ CustomerId customerId = customer.getId();
+
+ List<Device> devices = new ArrayList<>();
+ for (int i=0;i<128;i++) {
+ Device device = new Device();
+ device.setName("Device"+i);
+ device = doPost("/api/device", device, Device.class);
+ devices.add(doPost("/api/customer/" + customerId.getId().toString()
+ + "/device/" + device.getId().getId().toString(), Device.class));
+ }
+
+ List<Device> loadedDevices = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(23);
+ TextPageData<Device> pageData = null;
+ do {
+ pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?",
+ new TypeReference<TextPageData<Device>>(){}, pageLink);
+ loadedDevices.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(devices, idComparator);
+ Collections.sort(loadedDevices, idComparator);
+
+ Assert.assertEquals(devices, loadedDevices);
+ }
+
+ @Test
+ public void testFindCustomerDevicesByName() throws Exception {
+ Customer customer = new Customer();
+ customer.setTitle("Test customer");
+ customer = doPost("/api/customer", customer, Customer.class);
+ CustomerId customerId = customer.getId();
+
+ String title1 = "Device title 1";
+ List<Device> devicesTitle1 = new ArrayList<>();
+ for (int i=0;i<125;i++) {
+ Device device = new Device();
+ String suffix = RandomStringUtils.randomAlphanumeric(15);
+ String name = title1+suffix;
+ name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+ device.setName(name);
+ device = doPost("/api/device", device, Device.class);
+ devicesTitle1.add(doPost("/api/customer/" + customerId.getId().toString()
+ + "/device/" + device.getId().getId().toString(), Device.class));
+ }
+ String title2 = "Device title 2";
+ List<Device> devicesTitle2 = new ArrayList<>();
+ for (int i=0;i<143;i++) {
+ Device device = new Device();
+ String suffix = RandomStringUtils.randomAlphanumeric(15);
+ String name = title2+suffix;
+ name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+ device.setName(name);
+ device = doPost("/api/device", device, Device.class);
+ devicesTitle2.add(doPost("/api/customer/" + customerId.getId().toString()
+ + "/device/" + device.getId().getId().toString(), Device.class));
+ }
+
+ List<Device> loadedDevicesTitle1 = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(15, title1);
+ TextPageData<Device> pageData = null;
+ do {
+ pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?",
+ new TypeReference<TextPageData<Device>>(){}, pageLink);
+ loadedDevicesTitle1.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(devicesTitle1, idComparator);
+ Collections.sort(loadedDevicesTitle1, idComparator);
+
+ Assert.assertEquals(devicesTitle1, loadedDevicesTitle1);
+
+ List<Device> loadedDevicesTitle2 = new ArrayList<>();
+ pageLink = new TextPageLink(4, title2);
+ do {
+ pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?",
+ new TypeReference<TextPageData<Device>>(){}, pageLink);
+ loadedDevicesTitle2.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(devicesTitle2, idComparator);
+ Collections.sort(loadedDevicesTitle2, idComparator);
+
+ Assert.assertEquals(devicesTitle2, loadedDevicesTitle2);
+
+ for (Device device : loadedDevicesTitle1) {
+ doDelete("/api/customer/device/" + device.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ pageLink = new TextPageLink(4, title1);
+ pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?",
+ new TypeReference<TextPageData<Device>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ for (Device device : loadedDevicesTitle2) {
+ doDelete("/api/customer/device/" + device.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ pageLink = new TextPageLink(4, title2);
+ pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?",
+ new TypeReference<TextPageData<Device>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/PluginControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/PluginControllerTest.java
new file mode 100644
index 0000000..5d978e3
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/PluginControllerTest.java
@@ -0,0 +1,232 @@
+/**
+ * Copyright © 2016 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.ObjectMapper;
+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.User;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+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.extensions.core.plugin.telemetry.TelemetryStoragePlugin;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class PluginControllerTest extends AbstractControllerTest {
+
+ private IdComparator<PluginMetaData> idComparator = new IdComparator<>();
+
+ private final ObjectMapper mapper = new ObjectMapper();
+ private Tenant savedTenant;
+ private User tenantAdmin;
+
+ @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);
+
+ tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(savedTenant.getId());
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ loginSysAdmin();
+
+ doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSavePlugin() throws Exception {
+ PluginMetaData plugin = new PluginMetaData();
+ doPost("/api/plugin", plugin).andExpect(status().isBadRequest());
+ plugin.setName("My plugin");
+ doPost("/api/plugin", plugin).andExpect(status().isBadRequest());
+ plugin.setApiToken("myplugin");
+ doPost("/api/plugin", plugin).andExpect(status().isBadRequest());
+ plugin.setConfiguration(mapper.readTree("{}"));
+ doPost("/api/plugin", plugin).andExpect(status().isBadRequest());
+ plugin.setClazz(TelemetryStoragePlugin.class.getName());
+ PluginMetaData savedPlugin = doPost("/api/plugin", plugin, PluginMetaData.class);
+
+ Assert.assertNotNull(savedPlugin);
+ Assert.assertNotNull(savedPlugin.getId());
+ Assert.assertTrue(savedPlugin.getCreatedTime() > 0);
+ Assert.assertEquals(savedTenant.getId(), savedPlugin.getTenantId());
+ }
+
+ @Test
+ public void testFindPluginById() throws Exception {
+ PluginMetaData plugin = new PluginMetaData();
+ plugin.setName("My plugin");
+ plugin.setApiToken("myplugin");
+ plugin.setConfiguration(mapper.readTree("{}"));
+ plugin.setClazz(TelemetryStoragePlugin.class.getName());
+
+ PluginMetaData savedPlugin = doPost("/api/plugin", plugin, PluginMetaData.class);
+ PluginMetaData foundPlugin = doGet("/api/plugin/" + savedPlugin.getId().getId().toString(), PluginMetaData.class);
+ Assert.assertNotNull(foundPlugin);
+ Assert.assertEquals(savedPlugin, foundPlugin);
+ }
+
+ @Test
+ public void testActivatePlugin() throws Exception {
+ PluginMetaData plugin = new PluginMetaData();
+ plugin.setName("My plugin");
+ plugin.setApiToken("myplugin");
+ plugin.setConfiguration(mapper.readTree("{}"));
+ plugin.setClazz(TelemetryStoragePlugin.class.getName());
+
+ PluginMetaData savedPlugin = doPost("/api/plugin", plugin, PluginMetaData.class);
+
+ doPost("/api/plugin/" + savedPlugin.getId().getId().toString() + "/activate").andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSuspendPlugin() throws Exception {
+ PluginMetaData plugin = new PluginMetaData();
+ plugin.setName("My plugin");
+ plugin.setApiToken("myplugin");
+ plugin.setConfiguration(mapper.readTree("{}"));
+ plugin.setClazz(TelemetryStoragePlugin.class.getName());
+
+ PluginMetaData savedPlugin = doPost("/api/plugin", plugin, PluginMetaData.class);
+
+ doPost("/api/plugin/" + savedPlugin.getId().getId().toString() + "/activate").andExpect(status().isOk());
+
+ RuleMetaData rule = RuleControllerTest.createRuleMetaData(savedPlugin);
+ RuleMetaData savedRule = doPost("/api/rule", rule, RuleMetaData.class);
+ doPost("/api/rule/" + savedRule.getId().getId().toString() + "/activate").andExpect(status().isOk());
+
+ doPost("/api/plugin/" + savedPlugin.getId().getId().toString() + "/suspend").andExpect(status().isBadRequest());
+
+ doPost("/api/rule/" + savedRule.getId().getId().toString() + "/suspend").andExpect(status().isOk());
+
+ doPost("/api/plugin/" + savedPlugin.getId().getId().toString() + "/suspend").andExpect(status().isOk());
+ }
+
+ @Test
+ public void testDeletePluginById() throws Exception {
+ PluginMetaData plugin = new PluginMetaData();
+ plugin.setName("My plugin");
+ plugin.setApiToken("myplugin");
+ plugin.setConfiguration(mapper.readTree("{}"));
+ plugin.setClazz(TelemetryStoragePlugin.class.getName());
+
+ PluginMetaData savedPlugin = doPost("/api/plugin", plugin, PluginMetaData.class);
+
+ RuleMetaData rule = RuleControllerTest.createRuleMetaData(savedPlugin);
+ RuleMetaData savedRule = doPost("/api/rule", rule, RuleMetaData.class);
+
+ doDelete("/api/plugin/" + savedPlugin.getId().getId()).andExpect(status().isBadRequest());
+
+ doDelete("/api/rule/" + savedRule.getId().getId()).andExpect(status().isOk());
+
+ doDelete("/api/plugin/" + savedPlugin.getId().getId()).andExpect(status().isOk());
+ doGet("/api/plugin/" + savedPlugin.getId().getId().toString()).andExpect(status().isNotFound());
+ }
+
+ @Test
+ public void testFindPluginByToken() throws Exception {
+ PluginMetaData plugin = new PluginMetaData();
+ plugin.setName("My plugin");
+ plugin.setApiToken("myplugin");
+ plugin.setConfiguration(mapper.readTree("{}"));
+ plugin.setClazz(TelemetryStoragePlugin.class.getName());
+
+ PluginMetaData savedPlugin = doPost("/api/plugin", plugin, PluginMetaData.class);
+ PluginMetaData foundPlugin = doGet("/api/plugin/token/" + "myplugin", PluginMetaData.class);
+ Assert.assertNotNull(foundPlugin);
+ Assert.assertEquals(savedPlugin, foundPlugin);
+ }
+
+ @Test
+ public void testFindCurrentTenantPlugins() throws Exception {
+ List<PluginMetaData> plugins = testPluginsCreation("/api/plugin");
+ for (PluginMetaData plugin : plugins) {
+ doDelete("/api/plugin/" + plugin.getId().getId()).andExpect(status().isOk());
+ }
+ }
+
+ @Test
+ public void testFindSystemPlugins() throws Exception {
+ loginSysAdmin();
+ List<PluginMetaData> plugins = testPluginsCreation("/api/plugin/system");
+ for (PluginMetaData plugin : plugins) {
+ doDelete("/api/plugin/" + plugin.getId().getId()).andExpect(status().isOk());
+ }
+ }
+
+ private List<PluginMetaData> testPluginsCreation(String url) throws Exception {
+ List<PluginMetaData> plugins = new ArrayList<>();
+ for (int i = 0; i < 111; i++) {
+ PluginMetaData plugin = new PluginMetaData();
+ plugin.setName("My plugin");
+ plugin.setApiToken("myplugin" + i);
+ plugin.setConfiguration(mapper.readTree("{}"));
+ plugin.setClazz(TelemetryStoragePlugin.class.getName());
+ plugins.add(doPost("/api/plugin", plugin, PluginMetaData.class));
+ }
+
+ List<PluginMetaData> loadedPlugins = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(23);
+ TextPageData<PluginMetaData> pageData;
+ do {
+ pageData = doGetTypedWithPageLink(url + "?",
+ new TypeReference<TextPageData<PluginMetaData>>() {
+ }, pageLink);
+ loadedPlugins.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ loadedPlugins = loadedPlugins.stream()
+ .filter(p -> !p.getName().equals("System Telemetry Plugin"))
+ .filter(p -> !p.getName().equals("Mail Sender Plugin"))
+ .filter(p -> !p.getName().equals("System RPC Plugin"))
+ .collect(Collectors.toList());
+
+ Collections.sort(plugins, idComparator);
+ Collections.sort(loadedPlugins, idComparator);
+
+ Assert.assertEquals(plugins, loadedPlugins);
+ return loadedPlugins;
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleControllerTest.java
new file mode 100644
index 0000000..ddbc833
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/RuleControllerTest.java
@@ -0,0 +1,246 @@
+/**
+ * Copyright © 2016 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.ObjectMapper;
+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.User;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+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.extensions.core.plugin.telemetry.TelemetryStoragePlugin;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class RuleControllerTest extends AbstractControllerTest {
+
+ private IdComparator<RuleMetaData> idComparator = new IdComparator<>();
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+ private Tenant savedTenant;
+ private User tenantAdmin;
+ private PluginMetaData sysPlugin;
+ private PluginMetaData tenantPlugin;
+
+ @Before
+ public void beforeTest() throws Exception {
+ loginSysAdmin();
+
+ sysPlugin = new PluginMetaData();
+ sysPlugin.setName("Sys plugin");
+ sysPlugin.setApiToken("sysplugin");
+ sysPlugin.setConfiguration(mapper.readTree("{}"));
+ sysPlugin.setClazz(TelemetryStoragePlugin.class.getName());
+ sysPlugin = doPost("/api/plugin", sysPlugin, PluginMetaData.class);
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(savedTenant.getId());
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+
+ tenantPlugin = new PluginMetaData();
+ tenantPlugin.setName("My plugin");
+ tenantPlugin.setApiToken("myplugin");
+ tenantPlugin.setConfiguration(mapper.readTree("{}"));
+ tenantPlugin.setClazz(TelemetryStoragePlugin.class.getName());
+ tenantPlugin = doPost("/api/plugin", tenantPlugin, PluginMetaData.class);
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ loginSysAdmin();
+
+ doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+
+ doDelete("/api/plugin/" + sysPlugin.getId().getId()).andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSaveRule() throws Exception {
+ RuleMetaData rule = new RuleMetaData();
+ doPost("/api/rule", rule).andExpect(status().isBadRequest());
+ rule.setName("My Rule");
+ doPost("/api/rule", rule).andExpect(status().isBadRequest());
+ rule.setPluginToken(tenantPlugin.getApiToken());
+ doPost("/api/rule", rule).andExpect(status().isBadRequest());
+ rule.setFilters(mapper.readTree("[{\"clazz\":\"org.thingsboard.server.extensions.core.filter.MsgTypeFilter\", " +
+ "\"name\":\"TelemetryFilter\", " +
+ "\"configuration\": {\"messageTypes\":[\"POST_TELEMETRY\",\"POST_ATTRIBUTES\",\"GET_ATTRIBUTES\"]}}]"));
+ doPost("/api/rule", rule).andExpect(status().isBadRequest());
+ rule.setAction(mapper.readTree("{\"clazz\":\"org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction\", \"name\":\"TelemetryMsgConverterAction\", \"configuration\":{}}"));
+
+ RuleMetaData savedRule = doPost("/api/rule", rule, RuleMetaData.class);
+ Assert.assertNotNull(savedRule);
+ Assert.assertNotNull(savedRule.getId());
+ Assert.assertTrue(savedRule.getCreatedTime() > 0);
+ Assert.assertEquals(savedTenant.getId(), savedRule.getTenantId());
+ }
+
+ @Test
+ public void testFindRuleById() throws Exception {
+ RuleMetaData rule = createRuleMetaData(tenantPlugin);
+ RuleMetaData savedRule = doPost("/api/rule", rule, RuleMetaData.class);
+
+ RuleMetaData foundRule = doGet("/api/rule/" + savedRule.getId().getId().toString(), RuleMetaData.class);
+ Assert.assertNotNull(foundRule);
+ Assert.assertEquals(savedRule, foundRule);
+ }
+
+ @Test
+ public void testFindRuleByPluginToken() throws Exception {
+ RuleMetaData rule = createRuleMetaData(tenantPlugin);
+ RuleMetaData savedRule = doPost("/api/rule", rule, RuleMetaData.class);
+
+ List<RuleMetaData> foundRules = doGetTyped("/api/rule/token/" + savedRule.getPluginToken(),
+ new TypeReference<List<RuleMetaData>>() {
+ });
+ Assert.assertNotNull(foundRules);
+ Assert.assertEquals(1, foundRules.size());
+ Assert.assertEquals(savedRule, foundRules.get(0));
+ }
+
+ @Test
+ public void testActivateRule() throws Exception {
+ RuleMetaData rule = createRuleMetaData(tenantPlugin);
+ RuleMetaData savedRule = doPost("/api/rule", rule, RuleMetaData.class);
+
+ doPost("/api/rule/" + savedRule.getId().getId().toString() + "/activate").andExpect(status().isBadRequest());
+
+ doPost("/api/plugin/" + tenantPlugin.getId().getId().toString() + "/activate").andExpect(status().isOk());
+
+ doPost("/api/rule/" + savedRule.getId().getId().toString() + "/activate").andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSuspendRule() throws Exception {
+ RuleMetaData rule = createRuleMetaData(tenantPlugin);
+ RuleMetaData savedRule = doPost("/api/rule", rule, RuleMetaData.class);
+
+ doPost("/api/plugin/" + tenantPlugin.getId().getId().toString() + "/activate").andExpect(status().isOk());
+ doPost("/api/rule/" + savedRule.getId().getId().toString() + "/activate").andExpect(status().isOk());
+ doPost("/api/rule/" + savedRule.getId().getId().toString() + "/suspend").andExpect(status().isOk());
+ }
+
+ @Test
+ public void testFindSystemRules() throws Exception {
+ loginSysAdmin();
+ List<RuleMetaData> rules = testRulesCreation("/api/rule/system", sysPlugin);
+ for (RuleMetaData rule : rules) {
+ doDelete("/api/rule/" + rule.getId().getId()).andExpect(status().isOk());
+ }
+ loginTenantAdmin();
+ }
+
+ @Test
+ public void testFindCurrentTenantPlugins() throws Exception {
+ List<RuleMetaData> rules = testRulesCreation("/api/rule", tenantPlugin);
+ for (RuleMetaData rule : rules) {
+ doDelete("/api/rule/" + rule.getId().getId()).andExpect(status().isOk());
+ }
+ }
+
+ @Test
+ public void testFindTenantPlugins() throws Exception {
+ List<RuleMetaData> rules = testRulesCreation("/api/rule", tenantPlugin);
+ loginSysAdmin();
+ List<RuleMetaData> loadedRules = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(3);
+ TextPageData<RuleMetaData> pageData;
+ do {
+ pageData = doGetTypedWithPageLink("/api/rule/tenant/" + savedTenant.getId().getId().toString() + "?",
+ new TypeReference<TextPageData<RuleMetaData>>() {
+ }, pageLink);
+ loadedRules.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(rules, idComparator);
+ Collections.sort(loadedRules, idComparator);
+
+ Assert.assertEquals(rules, loadedRules);
+
+ for (RuleMetaData rule : rules) {
+ doDelete("/api/rule/" + rule.getId().getId()).andExpect(status().isOk());
+ }
+ }
+
+ private List<RuleMetaData> testRulesCreation(String url, PluginMetaData plugin) throws Exception {
+ List<RuleMetaData> rules = new ArrayList<>();
+ for (int i = 0; i < 6; i++) {
+ RuleMetaData rule = createRuleMetaData(plugin);
+ rule.setPluginToken(plugin.getApiToken());
+ rule.setName(rule.getName() + i);
+ rules.add(doPost("/api/rule", rule, RuleMetaData.class));
+ }
+
+ List<RuleMetaData> loadedRules = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(3);
+ TextPageData<RuleMetaData> pageData;
+ do {
+ pageData = doGetTypedWithPageLink(url + "?",
+ new TypeReference<TextPageData<RuleMetaData>>() {
+ }, pageLink);
+ loadedRules.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ loadedRules = loadedRules.stream().filter(p -> !p.getName().equals("System Telemetry Rule")).collect(Collectors.toList());
+
+ Collections.sort(rules, idComparator);
+ Collections.sort(loadedRules, idComparator);
+
+ Assert.assertEquals(rules, loadedRules);
+ return loadedRules;
+ }
+
+ public static RuleMetaData createRuleMetaData(PluginMetaData plugin) throws IOException {
+ RuleMetaData rule = new RuleMetaData();
+ rule.setName("My Rule");
+ rule.setPluginToken(plugin.getApiToken());
+ rule.setFilters(mapper.readTree("[{\"clazz\":\"org.thingsboard.server.extensions.core.filter.MsgTypeFilter\", " +
+ "\"name\":\"TelemetryFilter\", " +
+ "\"configuration\": {\"messageTypes\":[\"POST_TELEMETRY\",\"POST_ATTRIBUTES\",\"GET_ATTRIBUTES\"]}}]"));
+ rule.setAction(mapper.readTree("{\"clazz\":\"org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction\", \"name\":\"TelemetryMsgConverterAction\", \"configuration\":{}}"));
+ return rule;
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
new file mode 100644
index 0000000..03f2d69
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
@@ -0,0 +1,220 @@
+/**
+ * Copyright © 2016 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 static org.hamcrest.Matchers.containsString;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+public class TenantControllerTest extends AbstractControllerTest {
+
+ private IdComparator<Tenant> idComparator = new IdComparator<>();
+
+ @Test
+ public void testSaveTenant() throws Exception {
+ loginSysAdmin();
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+ Assert.assertNotNull(savedTenant.getId());
+ Assert.assertTrue(savedTenant.getCreatedTime() > 0);
+ Assert.assertEquals(tenant.getTitle(), savedTenant.getTitle());
+ savedTenant.setTitle("My new tenant");
+ doPost("/api/tenant", savedTenant, Tenant.class);
+ Tenant foundTenant = doGet("/api/tenant/"+savedTenant.getId().getId().toString(), Tenant.class);
+ Assert.assertEquals(foundTenant.getTitle(), savedTenant.getTitle());
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testFindTenantById() throws Exception {
+ loginSysAdmin();
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Tenant foundTenant = doGet("/api/tenant/"+savedTenant.getId().getId().toString(), Tenant.class);
+ Assert.assertNotNull(foundTenant);
+ Assert.assertEquals(savedTenant, foundTenant);
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSaveTenantWithEmptyTitle() throws Exception {
+ loginSysAdmin();
+ Tenant tenant = new Tenant();
+ doPost("/api/tenant", tenant)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Tenant title should be specified")));
+ }
+
+ @Test
+ public void testSaveTenantWithInvalidEmail() throws Exception {
+ loginSysAdmin();
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ tenant.setEmail("invalid@mail");
+ doPost("/api/tenant", tenant)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Invalid email address format")));
+ }
+
+ @Test
+ public void testDeleteTenant() throws Exception {
+ loginSysAdmin();
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ doGet("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ public void testFindTenants() throws Exception {
+ loginSysAdmin();
+ List<Tenant> tenants = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(17);
+ TextPageData<Tenant> pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference<TextPageData<Tenant>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(1, pageData.getData().size());
+ tenants.addAll(pageData.getData());
+
+ for (int i=0;i<56;i++) {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Tenant"+i);
+ tenants.add(doPost("/api/tenant", tenant, Tenant.class));
+ }
+
+ List<Tenant> loadedTenants = new ArrayList<>();
+ pageLink = new TextPageLink(17);
+ do {
+ pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference<TextPageData<Tenant>>(){}, pageLink);
+ loadedTenants.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(tenants, idComparator);
+ Collections.sort(loadedTenants, idComparator);
+
+ Assert.assertEquals(tenants, loadedTenants);
+
+ for (Tenant tenant : loadedTenants) {
+ if (!tenant.getTitle().equals("Tenant")) {
+ doDelete("/api/tenant/"+tenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+ }
+
+ pageLink = new TextPageLink(17);
+ pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference<TextPageData<Tenant>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(1, pageData.getData().size());
+ }
+
+ @Test
+ public void testFindTenantsByTitle() throws Exception {
+ loginSysAdmin();
+ String title1 = "Tenant title 1";
+ List<Tenant> tenantsTitle1 = new ArrayList<>();
+ for (int i=0;i<134;i++) {
+ Tenant tenant = new Tenant();
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+ String title = title1+suffix;
+ title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+ tenant.setTitle(title);
+ tenantsTitle1.add(doPost("/api/tenant", tenant, Tenant.class));
+ }
+ String title2 = "Tenant title 2";
+ List<Tenant> tenantsTitle2 = new ArrayList<>();
+ for (int i=0;i<127;i++) {
+ Tenant tenant = new Tenant();
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+ String title = title2+suffix;
+ title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+ tenant.setTitle(title);
+ tenantsTitle2.add(doPost("/api/tenant", tenant, Tenant.class));
+ }
+
+ List<Tenant> loadedTenantsTitle1 = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(15, title1);
+ TextPageData<Tenant> pageData = null;
+ do {
+ pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference<TextPageData<Tenant>>(){}, pageLink);
+ loadedTenantsTitle1.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(tenantsTitle1, idComparator);
+ Collections.sort(loadedTenantsTitle1, idComparator);
+
+ Assert.assertEquals(tenantsTitle1, loadedTenantsTitle1);
+
+ List<Tenant> loadedTenantsTitle2 = new ArrayList<>();
+ pageLink = new TextPageLink(4, title2);
+ do {
+ pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference<TextPageData<Tenant>>(){}, pageLink);
+ loadedTenantsTitle2.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(tenantsTitle2, idComparator);
+ Collections.sort(loadedTenantsTitle2, idComparator);
+
+ Assert.assertEquals(tenantsTitle2, loadedTenantsTitle2);
+
+ for (Tenant tenant : loadedTenantsTitle1) {
+ doDelete("/api/tenant/"+tenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ pageLink = new TextPageLink(4, title1);
+ pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference<TextPageData<Tenant>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ for (Tenant tenant : loadedTenantsTitle2) {
+ doDelete("/api/tenant/"+tenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ pageLink = new TextPageLink(4, title2);
+ pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference<TextPageData<Tenant>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
new file mode 100644
index 0000000..a48630c
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
@@ -0,0 +1,618 @@
+/**
+ * Copyright © 2016 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.apache.commons.lang3.RandomStringUtils;
+import org.junit.Assert;
+import org.junit.Test;
+import org.springframework.http.HttpHeaders;
+import org.thingsboard.server.common.data.Customer;
+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.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.service.mail.TestMailService;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+public class UserControllerTest extends AbstractControllerTest {
+
+ private IdComparator<User> idComparator = new IdComparator<>();
+
+ @Test
+ public void testSaveUser() throws Exception {
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ String email = "tenant2@thingsboard.org";
+ User user = new User();
+ user.setAuthority(Authority.TENANT_ADMIN);
+ user.setTenantId(savedTenant.getId());
+ user.setEmail(email);
+ user.setFirstName("Joe");
+ user.setLastName("Downs");
+ User savedUser = doPost("/api/user", user, User.class);
+ Assert.assertNotNull(savedUser);
+ Assert.assertNotNull(savedUser.getId());
+ Assert.assertTrue(savedUser.getCreatedTime() > 0);
+ Assert.assertEquals(user.getEmail(), savedUser.getEmail());
+
+ User foundUser = doGet("/api/user/"+savedUser.getId().getId().toString(), User.class);
+ Assert.assertEquals(foundUser, savedUser);
+
+ logout();
+ doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken)
+ .andExpect(status().isPermanentRedirect())
+ .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
+
+ JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", "activateToken", TestMailService.currentActivateToken, "password", "testPassword").andExpect(status().isOk()), JsonNode.class);
+ validateAndSetJwtToken(tokenInfo, email);
+
+ doGet("/api/auth/user")
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.authority",is(Authority.TENANT_ADMIN.name())))
+ .andExpect(jsonPath("$.email",is(email)));
+
+ logout();
+
+ login(email, "testPassword");
+
+ doGet("/api/auth/user")
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.authority",is(Authority.TENANT_ADMIN.name())))
+ .andExpect(jsonPath("$.email",is(email)));
+
+ loginSysAdmin();
+ doDelete("/api/user/"+savedUser.getId().getId().toString())
+ .andExpect(status().isOk());
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testResetPassword() throws Exception {
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ String email = "tenant2@thingsboard.org";
+ User user = new User();
+ user.setAuthority(Authority.TENANT_ADMIN);
+ user.setTenantId(savedTenant.getId());
+ user.setEmail(email);
+ user.setFirstName("Joe");
+ user.setLastName("Downs");
+
+ User savedUser = createUserAndLogin(user, "testPassword1");
+ logout();
+ doPost("/api/noauth/resetPasswordByEmail", "email", email)
+ .andExpect(status().isOk());
+ doGet("/api/noauth/resetPassword?resetToken={resetToken}", TestMailService.currentResetPasswordToken)
+ .andExpect(status().isPermanentRedirect())
+ .andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + TestMailService.currentResetPasswordToken));
+
+ JsonNode tokenInfo = readResponse(doPost("/api/noauth/resetPassword", "resetToken", TestMailService.currentResetPasswordToken, "password", "testPassword2").andExpect(status().isOk()), JsonNode.class);
+ validateAndSetJwtToken(tokenInfo, email);
+
+ doGet("/api/auth/user")
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.authority",is(Authority.TENANT_ADMIN.name())))
+ .andExpect(jsonPath("$.email",is(email)));
+
+ logout();
+
+ login(email, "testPassword2");
+ doGet("/api/auth/user")
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.authority",is(Authority.TENANT_ADMIN.name())))
+ .andExpect(jsonPath("$.email",is(email)));
+
+ loginSysAdmin();
+ doDelete("/api/user/"+savedUser.getId().getId().toString())
+ .andExpect(status().isOk());
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testFindUserById() throws Exception {
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ String email = "tenant2@thingsboard.org";
+ User user = new User();
+ user.setAuthority(Authority.TENANT_ADMIN);
+ user.setTenantId(savedTenant.getId());
+ user.setEmail(email);
+ user.setFirstName("Joe");
+ user.setLastName("Downs");
+
+ User savedUser = doPost("/api/user", user, User.class);
+ User foundUser = doGet("/api/user/"+savedUser.getId().getId().toString(), User.class);
+ Assert.assertNotNull(foundUser);
+ Assert.assertEquals(savedUser, foundUser);
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSaveUserWithSameEmail() throws Exception {
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ String email = "tenant@thingsboard.org";
+ User user = new User();
+ user.setAuthority(Authority.TENANT_ADMIN);
+ user.setTenantId(savedTenant.getId());
+ user.setEmail(email);
+ user.setFirstName("Joe");
+ user.setLastName("Downs");
+
+ doPost("/api/user", user)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("User with email '" + email + "' already present in database")));
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSaveUserWithInvalidEmail() throws Exception {
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ String email = "tenant_thingsboard.org";
+ User user = new User();
+ user.setAuthority(Authority.TENANT_ADMIN);
+ user.setTenantId(savedTenant.getId());
+ user.setEmail(email);
+ user.setFirstName("Joe");
+ user.setLastName("Downs");
+
+ doPost("/api/user", user)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Invalid email address format '" + email + "'")));
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSaveUserWithEmptyEmail() throws Exception {
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ User user = new User();
+ user.setAuthority(Authority.TENANT_ADMIN);
+ user.setTenantId(savedTenant.getId());
+ user.setFirstName("Joe");
+ user.setLastName("Downs");
+
+ doPost("/api/user", user)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("User email should be specified")));
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSaveUserWithoutTenant() throws Exception {
+ loginSysAdmin();
+
+ User user = new User();
+ user.setAuthority(Authority.TENANT_ADMIN);
+ user.setEmail("tenant2@thingsboard.org");
+ user.setFirstName("Joe");
+ user.setLastName("Downs");
+
+ doPost("/api/user", user)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Tenant administrator should be assigned to tenant")));
+ }
+
+ @Test
+ public void testDeleteUser() throws Exception {
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ String email = "tenant2@thingsboard.org";
+ User user = new User();
+ user.setAuthority(Authority.TENANT_ADMIN);
+ user.setTenantId(savedTenant.getId());
+ user.setEmail(email);
+ user.setFirstName("Joe");
+ user.setLastName("Downs");
+
+ User savedUser = doPost("/api/user", user, User.class);
+ User foundUser = doGet("/api/user/"+savedUser.getId().getId().toString(), User.class);
+ Assert.assertNotNull(foundUser);
+
+ doDelete("/api/user/"+savedUser.getId().getId().toString())
+ .andExpect(status().isOk());
+
+ doGet("/api/user/"+savedUser.getId().getId().toString())
+ .andExpect(status().isNotFound());
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testFindTenantAdmins() throws Exception {
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ TenantId tenantId = savedTenant.getId();
+
+ List<User> tenantAdmins = new ArrayList<>();
+ for (int i=0;i<64;i++) {
+ User user = new User();
+ user.setAuthority(Authority.TENANT_ADMIN);
+ user.setTenantId(tenantId);
+ user.setEmail("testTenant" + i + "@thingsboard.org");
+ tenantAdmins.add(doPost("/api/user", user, User.class));
+ }
+
+ List<User> loadedTenantAdmins = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(33);
+ TextPageData<User> pageData = null;
+ do {
+ pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?",
+ new TypeReference<TextPageData<User>>(){}, pageLink);
+ loadedTenantAdmins.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(tenantAdmins, idComparator);
+ Collections.sort(loadedTenantAdmins, idComparator);
+
+ Assert.assertEquals(tenantAdmins, loadedTenantAdmins);
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+
+ pageLink = new TextPageLink(33);
+ pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?",
+ new TypeReference<TextPageData<User>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertTrue(pageData.getData().isEmpty());
+ }
+
+ @Test
+ public void testFindTenantAdminsByEmail() throws Exception {
+
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ TenantId tenantId = savedTenant.getId();
+
+ String email1 = "testEmail1";
+ List<User> tenantAdminsEmail1 = new ArrayList<>();
+
+ for (int i=0;i<124;i++) {
+ User user = new User();
+ user.setAuthority(Authority.TENANT_ADMIN);
+ user.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
+ String email = email1+suffix+ "@thingsboard.org";
+ email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase();
+ user.setEmail(email);
+ tenantAdminsEmail1.add(doPost("/api/user", user, User.class));
+ }
+
+ String email2 = "testEmail2";
+ List<User> tenantAdminsEmail2 = new ArrayList<>();
+
+ for (int i=0;i<112;i++) {
+ User user = new User();
+ user.setAuthority(Authority.TENANT_ADMIN);
+ user.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
+ String email = email2+suffix+ "@thingsboard.org";
+ email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase();
+ user.setEmail(email);
+ tenantAdminsEmail2.add(doPost("/api/user", user, User.class));
+ }
+
+ List<User> loadedTenantAdminsEmail1 = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(33, email1);
+ TextPageData<User> pageData = null;
+ do {
+ pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?",
+ new TypeReference<TextPageData<User>>(){}, pageLink);
+ loadedTenantAdminsEmail1.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(tenantAdminsEmail1, idComparator);
+ Collections.sort(loadedTenantAdminsEmail1, idComparator);
+
+ Assert.assertEquals(tenantAdminsEmail1, loadedTenantAdminsEmail1);
+
+ List<User> loadedTenantAdminsEmail2 = new ArrayList<>();
+ pageLink = new TextPageLink(16, email2);
+ do {
+ pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?",
+ new TypeReference<TextPageData<User>>(){}, pageLink);
+ loadedTenantAdminsEmail2.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(tenantAdminsEmail2, idComparator);
+ Collections.sort(loadedTenantAdminsEmail2, idComparator);
+
+ Assert.assertEquals(tenantAdminsEmail2, loadedTenantAdminsEmail2);
+
+ for (User user : loadedTenantAdminsEmail1) {
+ doDelete("/api/user/"+user.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ pageLink = new TextPageLink(4, email1);
+ pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?",
+ new TypeReference<TextPageData<User>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ for (User user : loadedTenantAdminsEmail2) {
+ doDelete("/api/user/"+user.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ pageLink = new TextPageLink(4, email2);
+ pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?",
+ new TypeReference<TextPageData<User>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testFindCustomerUsers() throws Exception {
+
+ loginSysAdmin();
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ TenantId tenantId = savedTenant.getId();
+ User tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(tenantId);
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+
+ Customer customer = new Customer();
+ customer.setTitle("My customer");
+ Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+ CustomerId customerId = savedCustomer.getId();
+
+ List<User> customerUsers = new ArrayList<>();
+ for (int i=0;i<56;i++) {
+ User user = new User();
+ user.setAuthority(Authority.CUSTOMER_USER);
+ user.setCustomerId(customerId);
+ user.setEmail("testCustomer" + i + "@thingsboard.org");
+ customerUsers.add(doPost("/api/user", user, User.class));
+ }
+
+ List<User> loadedCustomerUsers = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(33);
+ TextPageData<User> pageData = null;
+ do {
+ pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/users?",
+ new TypeReference<TextPageData<User>>(){}, pageLink);
+ loadedCustomerUsers.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(customerUsers, idComparator);
+ Collections.sort(loadedCustomerUsers, idComparator);
+
+ Assert.assertEquals(customerUsers, loadedCustomerUsers);
+
+ doDelete("/api/customer/"+customerId.getId().toString())
+ .andExpect(status().isOk());
+
+ loginSysAdmin();
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testFindCustomerUsersByEmail() throws Exception {
+
+ loginSysAdmin();
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ TenantId tenantId = savedTenant.getId();
+ User tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(tenantId);
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+
+ Customer customer = new Customer();
+ customer.setTitle("My customer");
+ Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+ CustomerId customerId = savedCustomer.getId();
+
+ String email1 = "testEmail1";
+ List<User> customerUsersEmail1 = new ArrayList<>();
+
+ for (int i=0;i<74;i++) {
+ User user = new User();
+ user.setAuthority(Authority.CUSTOMER_USER);
+ user.setCustomerId(customerId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
+ String email = email1+suffix+ "@thingsboard.org";
+ email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase();
+ user.setEmail(email);
+ customerUsersEmail1.add(doPost("/api/user", user, User.class));
+ }
+
+ String email2 = "testEmail2";
+ List<User> customerUsersEmail2 = new ArrayList<>();
+
+ for (int i=0;i<92;i++) {
+ User user = new User();
+ user.setAuthority(Authority.CUSTOMER_USER);
+ user.setCustomerId(customerId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
+ String email = email2+suffix+ "@thingsboard.org";
+ email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase();
+ user.setEmail(email);
+ customerUsersEmail2.add(doPost("/api/user", user, User.class));
+ }
+
+ List<User> loadedCustomerUsersEmail1 = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(33, email1);
+ TextPageData<User> pageData = null;
+ do {
+ pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/users?",
+ new TypeReference<TextPageData<User>>(){}, pageLink);
+ loadedCustomerUsersEmail1.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(customerUsersEmail1, idComparator);
+ Collections.sort(loadedCustomerUsersEmail1, idComparator);
+
+ Assert.assertEquals(customerUsersEmail1, loadedCustomerUsersEmail1);
+
+ List<User> loadedCustomerUsersEmail2 = new ArrayList<>();
+ pageLink = new TextPageLink(16, email2);
+ do {
+ pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/users?",
+ new TypeReference<TextPageData<User>>(){}, pageLink);
+ loadedCustomerUsersEmail2.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(customerUsersEmail2, idComparator);
+ Collections.sort(loadedCustomerUsersEmail2, idComparator);
+
+ Assert.assertEquals(customerUsersEmail2, loadedCustomerUsersEmail2);
+
+ for (User user : loadedCustomerUsersEmail1) {
+ doDelete("/api/user/"+user.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ pageLink = new TextPageLink(4, email1);
+ pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/users?",
+ new TypeReference<TextPageData<User>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ for (User user : loadedCustomerUsersEmail2) {
+ doDelete("/api/user/"+user.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ pageLink = new TextPageLink(4, email2);
+ pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/users?",
+ new TypeReference<TextPageData<User>>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ doDelete("/api/customer/"+customerId.getId().toString())
+ .andExpect(status().isOk());
+
+ loginSysAdmin();
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java
new file mode 100644
index 0000000..d558188
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java
@@ -0,0 +1,316 @@
+/**
+ * Copyright © 2016 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 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.User;
+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.widget.WidgetsBundle;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class WidgetsBundleControllerTest extends AbstractControllerTest {
+
+ private IdComparator<WidgetsBundle> idComparator = new IdComparator<>();
+
+ private Tenant savedTenant;
+ private User tenantAdmin;
+
+ @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);
+
+ tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(savedTenant.getId());
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ loginSysAdmin();
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSaveWidgetsBundle() throws Exception {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTitle("My widgets bundle");
+ WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class);
+
+ Assert.assertNotNull(savedWidgetsBundle);
+ Assert.assertNotNull(savedWidgetsBundle.getId());
+ Assert.assertNotNull(savedWidgetsBundle.getAlias());
+ Assert.assertTrue(savedWidgetsBundle.getCreatedTime() > 0);
+ Assert.assertEquals(savedTenant.getId(), savedWidgetsBundle.getTenantId());
+ Assert.assertEquals(widgetsBundle.getTitle(), savedWidgetsBundle.getTitle());
+
+ savedWidgetsBundle.setTitle("My new widgets bundle");
+ doPost("/api/widgetsBundle", savedWidgetsBundle, WidgetsBundle.class);
+
+ WidgetsBundle foundWidgetsBundle = doGet("/api/widgetsBundle/" + savedWidgetsBundle.getId().getId().toString(), WidgetsBundle.class);
+ Assert.assertEquals(foundWidgetsBundle.getTitle(), savedWidgetsBundle.getTitle());
+ }
+
+ @Test
+ public void testFindWidgetsBundleById() throws Exception {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTitle("My widgets bundle");
+ WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class);
+ WidgetsBundle foundWidgetsBundle = doGet("/api/widgetsBundle/" + savedWidgetsBundle.getId().getId().toString(), WidgetsBundle.class);
+ Assert.assertNotNull(foundWidgetsBundle);
+ Assert.assertEquals(savedWidgetsBundle, foundWidgetsBundle);
+ }
+
+ @Test
+ public void testDeleteWidgetsBundle() throws Exception {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTitle("My widgets bundle");
+ WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class);
+
+ doDelete("/api/widgetsBundle/"+savedWidgetsBundle.getId().getId().toString())
+ .andExpect(status().isOk());
+
+ doGet("/api/widgetsBundle/"+savedWidgetsBundle.getId().getId().toString())
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ public void testSaveWidgetsBundleWithEmptyTitle() throws Exception {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ doPost("/api/widgetsBundle", widgetsBundle)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Widgets bundle title should be specified")));
+ }
+
+ @Test
+ public void testUpdateWidgetsBundleAlias() throws Exception {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTitle("My widgets bundle");
+ WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class);
+ savedWidgetsBundle.setAlias("new_alias");
+ doPost("/api/widgetsBundle", savedWidgetsBundle)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Update of widgets bundle alias is prohibited")));
+
+ }
+
+ @Test
+ public void testFindTenantWidgetsBundlesByPageLink() throws Exception {
+
+ login(tenantAdmin.getEmail(), "testPassword1");
+
+ List<WidgetsBundle> sysWidgetsBundles = doGetTyped("/api/widgetsBundles?",
+ new TypeReference<List<WidgetsBundle>>(){});
+
+
+ List<WidgetsBundle> widgetsBundles = new ArrayList<>();
+ for (int i=0;i<73;i++) {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTitle("Widgets bundle"+i);
+ widgetsBundles.add(doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class));
+ }
+
+ widgetsBundles.addAll(sysWidgetsBundles);
+
+ List<WidgetsBundle> loadedWidgetsBundles = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(14);
+ TextPageData<WidgetsBundle> pageData;
+ do {
+ pageData = doGetTypedWithPageLink("/api/widgetsBundles?",
+ new TypeReference<TextPageData<WidgetsBundle>>(){}, pageLink);
+ loadedWidgetsBundles.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(widgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(widgetsBundles, loadedWidgetsBundles);
+ }
+
+ @Test
+ public void testFindSystemWidgetsBundlesByPageLink() throws Exception {
+
+ loginSysAdmin();
+
+ List<WidgetsBundle> sysWidgetsBundles = doGetTyped("/api/widgetsBundles?",
+ new TypeReference<List<WidgetsBundle>>(){});
+
+ List<WidgetsBundle> createdWidgetsBundles = new ArrayList<>();
+ for (int i=0;i<120;i++) {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTitle("Widgets bundle"+i);
+ createdWidgetsBundles.add(doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class));
+ }
+
+ List<WidgetsBundle> widgetsBundles = new ArrayList<>(createdWidgetsBundles);
+ widgetsBundles.addAll(sysWidgetsBundles);
+
+ List<WidgetsBundle> loadedWidgetsBundles = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(14);
+ TextPageData<WidgetsBundle> pageData;
+ do {
+ pageData = doGetTypedWithPageLink("/api/widgetsBundles?",
+ new TypeReference<TextPageData<WidgetsBundle>>(){}, pageLink);
+ loadedWidgetsBundles.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(widgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(widgetsBundles, loadedWidgetsBundles);
+
+ for (WidgetsBundle widgetsBundle : createdWidgetsBundles) {
+ doDelete("/api/widgetsBundle/"+widgetsBundle.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ pageLink = new TextPageLink(17);
+ loadedWidgetsBundles.clear();
+ do {
+ pageData = doGetTypedWithPageLink("/api/widgetsBundles?",
+ new TypeReference<TextPageData<WidgetsBundle>>(){}, pageLink);
+ loadedWidgetsBundles.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(sysWidgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(sysWidgetsBundles, loadedWidgetsBundles);
+ }
+
+
+ @Test
+ public void testFindTenantWidgetsBundles() throws Exception {
+
+ login(tenantAdmin.getEmail(), "testPassword1");
+
+ List<WidgetsBundle> sysWidgetsBundles = doGetTyped("/api/widgetsBundles?",
+ new TypeReference<List<WidgetsBundle>>(){});
+
+ List<WidgetsBundle> widgetsBundles = new ArrayList<>();
+ for (int i=0;i<73;i++) {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTitle("Widgets bundle"+i);
+ widgetsBundles.add(doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class));
+ }
+
+ widgetsBundles.addAll(sysWidgetsBundles);
+
+ List<WidgetsBundle> loadedWidgetsBundles = doGetTyped("/api/widgetsBundles?",
+ new TypeReference<List<WidgetsBundle>>(){});
+
+ Collections.sort(widgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(widgetsBundles, loadedWidgetsBundles);
+ }
+
+ @Test
+ public void testFindSystemAndTenantWidgetsBundles() throws Exception {
+
+ loginSysAdmin();
+
+
+ List<WidgetsBundle> sysWidgetsBundles = doGetTyped("/api/widgetsBundles?",
+ new TypeReference<List<WidgetsBundle>>(){});
+
+ List<WidgetsBundle> createdSystemWidgetsBundles = new ArrayList<>();
+ for (int i=0;i<82;i++) {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTitle("Sys widgets bundle"+i);
+ createdSystemWidgetsBundles.add(doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class));
+ }
+
+ List<WidgetsBundle> systemWidgetsBundles = new ArrayList<>(createdSystemWidgetsBundles);
+ systemWidgetsBundles.addAll(sysWidgetsBundles);
+
+ List<WidgetsBundle> widgetsBundles = new ArrayList<>();
+ widgetsBundles.addAll(systemWidgetsBundles);
+
+ login(tenantAdmin.getEmail(), "testPassword1");
+
+ for (int i=0;i<127;i++) {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTitle("Tenant widgets bundle"+i);
+ widgetsBundles.add(doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class));
+ }
+
+ List<WidgetsBundle> loadedWidgetsBundles = doGetTyped("/api/widgetsBundles?",
+ new TypeReference<List<WidgetsBundle>>(){});
+
+ Collections.sort(widgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(widgetsBundles, loadedWidgetsBundles);
+
+ loginSysAdmin();
+
+ loadedWidgetsBundles = doGetTyped("/api/widgetsBundles?",
+ new TypeReference<List<WidgetsBundle>>(){});
+
+ Collections.sort(systemWidgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(systemWidgetsBundles, loadedWidgetsBundles);
+
+ for (WidgetsBundle widgetsBundle : createdSystemWidgetsBundles) {
+ doDelete("/api/widgetsBundle/"+widgetsBundle.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ loadedWidgetsBundles = doGetTyped("/api/widgetsBundles?",
+ new TypeReference<List<WidgetsBundle>>(){});
+
+ Collections.sort(sysWidgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(sysWidgetsBundles, loadedWidgetsBundles);
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/WidgetTypeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/WidgetTypeControllerTest.java
new file mode 100644
index 0000000..3a65055
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/WidgetTypeControllerTest.java
@@ -0,0 +1,233 @@
+/**
+ * Copyright © 2016 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 com.fasterxml.jackson.databind.ObjectMapper;
+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.User;
+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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class WidgetTypeControllerTest extends AbstractControllerTest {
+
+ private IdComparator<WidgetType> idComparator = new IdComparator<>();
+
+ private Tenant savedTenant;
+ private WidgetsBundle savedWidgetsBundle;
+ private User tenantAdmin;
+
+ @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);
+
+ tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(savedTenant.getId());
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTitle("My widgets bundle");
+ savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class);
+
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ loginSysAdmin();
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSaveWidgetType() throws Exception {
+ WidgetType widgetType = new WidgetType();
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
+
+ Assert.assertNotNull(savedWidgetType);
+ Assert.assertNotNull(savedWidgetType.getId());
+ Assert.assertNotNull(savedWidgetType.getAlias());
+ Assert.assertTrue(savedWidgetType.getCreatedTime() > 0);
+ Assert.assertEquals(savedTenant.getId(), savedWidgetType.getTenantId());
+ Assert.assertEquals(widgetType.getName(), savedWidgetType.getName());
+ Assert.assertEquals(widgetType.getDescriptor(), savedWidgetType.getDescriptor());
+ Assert.assertEquals(savedWidgetsBundle.getAlias(), savedWidgetType.getBundleAlias());
+
+ savedWidgetType.setName("New Widget Type");
+
+ doPost("/api/widgetType", savedWidgetType, WidgetType.class);
+
+ WidgetType foundWidgetType = doGet("/api/widgetType/" + savedWidgetType.getId().getId().toString(), WidgetType.class);
+ Assert.assertEquals(foundWidgetType.getName(), savedWidgetType.getName());
+ }
+
+ @Test
+ public void testFindWidgetTypeById() throws Exception {
+ WidgetType widgetType = new WidgetType();
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
+ WidgetType foundWidgetType = doGet("/api/widgetType/" + savedWidgetType.getId().getId().toString(), WidgetType.class);
+ Assert.assertNotNull(foundWidgetType);
+ Assert.assertEquals(savedWidgetType, foundWidgetType);
+ }
+
+ @Test
+ public void testDeleteWidgetType() throws Exception {
+ WidgetType widgetType = new WidgetType();
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
+
+ doDelete("/api/widgetType/"+savedWidgetType.getId().getId().toString())
+ .andExpect(status().isOk());
+
+ doGet("/api/widgetType/"+savedWidgetType.getId().getId().toString())
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ public void testSaveWidgetTypeWithEmptyName() throws Exception {
+ WidgetType widgetType = new WidgetType();
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ doPost("/api/widgetType", widgetType)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Widgets type name should be specified")));
+ }
+
+ @Test
+ public void testSaveWidgetTypeWithEmptyBundleAlias() throws Exception {
+ WidgetType widgetType = new WidgetType();
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ doPost("/api/widgetType", widgetType)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Widgets type bundle alias should be specified")));
+ }
+
+ @Test
+ public void testSaveWidgetTypeWithEmptyDescriptor() throws Exception {
+ WidgetType widgetType = new WidgetType();
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{}", JsonNode.class));
+ doPost("/api/widgetType", widgetType)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Widgets type descriptor can't be empty")));
+ }
+
+ @Test
+ public void testSaveWidgetTypeWithInvalidBundleAlias() throws Exception {
+ WidgetType widgetType = new WidgetType();
+ widgetType.setBundleAlias("some_alias");
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ doPost("/api/widgetType", widgetType)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Widget type is referencing to non-existent widgets bundle")));
+ }
+
+ @Test
+ public void testUpdateWidgetTypeBundleAlias() throws Exception {
+ WidgetType widgetType = new WidgetType();
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
+ savedWidgetType.setBundleAlias("some_alias");
+ doPost("/api/widgetType", savedWidgetType)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Update of widget type bundle alias is prohibited")));
+
+ }
+
+ @Test
+ public void testUpdateWidgetTypeAlias() throws Exception {
+ WidgetType widgetType = new WidgetType();
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
+ savedWidgetType.setAlias("some_alias");
+ doPost("/api/widgetType", savedWidgetType)
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Update of widget type alias is prohibited")));
+
+ }
+
+ @Test
+ public void testGetBundleWidgetTypes() throws Exception {
+ List<WidgetType> widgetTypes = new ArrayList<>();
+ for (int i=0;i<89;i++) {
+ WidgetType widgetType = new WidgetType();
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setName("Widget Type " + i);
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ widgetTypes.add(doPost("/api/widgetType", widgetType, WidgetType.class));
+ }
+
+ List<WidgetType> loadedWidgetTypes = doGetTyped("/api/widgetTypes?isSystem={isSystem}&bundleAlias={bundleAlias}",
+ new TypeReference<List<WidgetType>>(){}, false, savedWidgetsBundle.getAlias());
+
+ Collections.sort(widgetTypes, idComparator);
+ Collections.sort(loadedWidgetTypes, idComparator);
+
+ Assert.assertEquals(widgetTypes, loadedWidgetTypes);
+ }
+
+ @Test
+ public void testGetWidgetType() throws Exception {
+ WidgetType widgetType = new WidgetType();
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
+ WidgetType foundWidgetType = doGet("/api/widgetType?isSystem={isSystem}&bundleAlias={bundleAlias}&alias={alias}",
+ WidgetType.class, false, savedWidgetsBundle.getAlias(), savedWidgetType.getAlias());
+ Assert.assertNotNull(foundWidgetType);
+ Assert.assertEquals(savedWidgetType, foundWidgetType);
+ }
+
+}
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
new file mode 100644
index 0000000..6ba0207
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright © 2016 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.mail;
+
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+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;
+
+@Profile("test")
+@Configuration
+public class TestMailService {
+
+ public static String currentActivateToken;
+ public static String currentResetPasswordToken;
+
+ @Bean
+ @Primary
+ public MailService mailService() throws ThingsboardException {
+ MailService mailService = Mockito.mock(MailService.class);
+ Mockito.doAnswer(new Answer<Void>() {
+ public Void answer(InvocationOnMock invocation) {
+ Object[] args = invocation.getArguments();
+ String activationLink = (String) args[0];
+ currentActivateToken = activationLink.split("=")[1];
+ return null;
+ }
+ }).when(mailService).sendActivationEmail(Mockito.anyString(), Mockito.anyString());
+ Mockito.doAnswer(new Answer<Void>() {
+ public Void answer(InvocationOnMock invocation) {
+ Object[] args = invocation.getArguments();
+ String passwordResetLink = (String) args[0];
+ currentResetPasswordToken = passwordResetLink.split("=")[1];
+ return null;
+ }
+ }).when(mailService).sendResetPasswordEmail(Mockito.anyString(), Mockito.anyString());
+ return mailService;
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/system/HttpDeviceApiTest.java b/application/src/test/java/org/thingsboard/server/system/HttpDeviceApiTest.java
new file mode 100644
index 0000000..10d41be
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/system/HttpDeviceApiTest.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright © 2016 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.system;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.controller.AbstractControllerTest;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class HttpDeviceApiTest extends AbstractControllerTest {
+
+ private static final AtomicInteger idSeq = new AtomicInteger(new Random(System.currentTimeMillis()).nextInt());
+
+ protected Device device;
+ protected DeviceCredentials deviceCredentials;
+
+ @Before
+ public void before() throws Exception {
+ loginTenantAdmin();
+ device = new Device();
+ device.setName("My device");
+ device = doPost("/api/device", device, Device.class);
+
+ deviceCredentials =
+ doGet("/api/device/" + device.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+ }
+
+ @Test
+ public void testGetAttributes() throws Exception {
+ doGetAsync("/api/v1/" + "WRONG_TOKEN" + "/attributes?clientKeys=keyA,keyB,keyC").andExpect(status().isUnauthorized());
+ doGetAsync("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes?clientKeys=keyA,keyB,keyC").andExpect(status().isNotFound());
+
+ Map<String, String> attrMap = new HashMap<>();
+ attrMap.put("keyA", "valueA");
+ mockMvc.perform(
+ asyncDispatch(doPost("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes", attrMap, new String[]{}).andReturn()))
+ .andExpect(status().isOk());
+ doGetAsync("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes?clientKeys=keyA,keyB,keyC").andExpect(status().isOk());
+ }
+
+ protected ResultActions doGetAsync(String urlTemplate, Object... urlVariables) throws Exception {
+ MockHttpServletRequestBuilder getRequest;
+ getRequest = get(urlTemplate, urlVariables);
+ setJwtToken(getRequest);
+ return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn()));
+ }
+
+ protected ResultActions doPostAsync(String urlTemplate, Object... urlVariables) throws Exception {
+ MockHttpServletRequestBuilder getRequest = post(urlTemplate, urlVariables);
+ setJwtToken(getRequest);
+ return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn()));
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/system/SystemTestSuite.java b/application/src/test/java/org/thingsboard/server/system/SystemTestSuite.java
new file mode 100644
index 0000000..9d565f7
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/system/SystemTestSuite.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 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.system;
+
+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 java.util.Arrays;
+
+/**
+ * @author Andrew Shvayka
+ */
+@RunWith(ClasspathSuite.class)
+@ClasspathSuite.ClassnameFilters({"org.thingsboard.server.system.*Test"})
+public class SystemTestSuite {
+
+ @ClassRule
+ public static CustomCassandraCQLUnit cassandraUnit =
+ new CustomCassandraCQLUnit(Arrays.asList(
+ new ClassPathCQLDataSet("schema.cql", false, false),
+ new ClassPathCQLDataSet("system-data.cql", false, false)),
+ "cassandra-test.yaml", 30000l);
+}
diff --git a/application/src/test/java/org/thingsboard/server/ThingsboardApplicationTests.java b/application/src/test/java/org/thingsboard/server/ThingsboardApplicationTests.java
new file mode 100644
index 0000000..63b62d2
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/ThingsboardApplicationTests.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright © 2016 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;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.boot.test.IntegrationTest;
+import org.springframework.boot.test.SpringApplicationConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringApplicationConfiguration(classes = ThingsboardServerApplication.class)
+@WebAppConfiguration
+@IntegrationTest("server.port:0")
+public class ThingsboardApplicationTests {
+
+ @Test
+ public void contextLoads() {
+ String test = "[ \n" +
+ " {\n" +
+ " \"key\": \"name\",\n" +
+ "\t\"type\": \"text\" \n" +
+ " },\n" +
+ " {\n" +
+ "\t\"key\": \"name2\",\n" +
+ "\t\"type\": \"color\"\n" +
+ " },\n" +
+ " {\n" +
+ "\t\"key\": \"name3\",\n" +
+ "\t\"type\": \"javascript\"\n" +
+ " } \n" +
+ "]";
+ }
+
+}
application/src/test/resources/logback.xml 21(+21 -0)
diff --git a/application/src/test/resources/logback.xml b/application/src/test/resources/logback.xml
new file mode 100644
index 0000000..f32acec
--- /dev/null
+++ b/application/src/test/resources/logback.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<configuration>
+ <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <logger name="org.thingsboard.server" level="DEBUG"/>
+ <logger name="org.springframework" level="WARN"/>
+ <logger name="org.apache.cassandra" level="WARN"/>
+ <logger name="org.cassandraunit" level="INFO"/>
+
+ <logger name="akka" level="DEBUG" />
+
+ <root level="WARN">
+ <appender-ref ref="console"/>
+ </root>
+
+</configuration>
common/data/pom.xml 76(+76 -0)
diff --git a/common/data/pom.xml b/common/data/pom.xml
new file mode 100644
index 0000000..b458ef1
--- /dev/null
+++ b/common/data/pom.xml
@@ -0,0 +1,76 @@
+<!--
+
+ Copyright © 2016 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.server</groupId>
+ <version>0.0.1-SNAPSHOT</version>
+ <artifactId>common</artifactId>
+ </parent>
+ <groupId>org.thingsboard.server.common</groupId>
+ <artifactId>data</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Thingsboard Server Common Data</name>
+ <url>http://thingsboard.org</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <main.dir>${basedir}/../..</main.dir>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>log4j-over-slf4j</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ </plugins>
+ </build>
+
+</project>
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java
new file mode 100644
index 0000000..c91e4ee
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright © 2016 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;
+
+import org.thingsboard.server.common.data.id.AdminSettingsId;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class AdminSettings extends BaseData<AdminSettingsId> {
+
+ private static final long serialVersionUID = -7670322981725511892L;
+
+ private String key;
+ private JsonNode jsonValue;
+
+ public AdminSettings() {
+ super();
+ }
+
+ public AdminSettings(AdminSettingsId id) {
+ super(id);
+ }
+
+ public AdminSettings(AdminSettings adminSettings) {
+ super(adminSettings);
+ this.key = adminSettings.getKey();
+ this.jsonValue = adminSettings.getJsonValue();
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public JsonNode getJsonValue() {
+ return jsonValue;
+ }
+
+ public void setJsonValue(JsonNode jsonValue) {
+ this.jsonValue = jsonValue;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + ((jsonValue == null) ? 0 : jsonValue.hashCode());
+ result = prime * result + ((key == null) ? 0 : key.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ AdminSettings other = (AdminSettings) obj;
+ if (jsonValue == null) {
+ if (other.jsonValue != null)
+ return false;
+ } else if (!jsonValue.equals(other.jsonValue))
+ return false;
+ if (key == null) {
+ if (other.key != null)
+ return false;
+ } else if (!key.equals(other.key))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("AdminSettings [key=");
+ builder.append(key);
+ builder.append(", jsonValue=");
+ builder.append(jsonValue);
+ builder.append(", createdTime=");
+ builder.append(createdTime);
+ builder.append(", id=");
+ builder.append(id);
+ builder.append("]");
+ return builder.toString();
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java b/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java
new file mode 100644
index 0000000..452efa0
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright © 2016 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;
+
+import java.io.Serializable;
+
+import org.thingsboard.server.common.data.id.IdBased;
+import org.thingsboard.server.common.data.id.UUIDBased;
+
+public abstract class BaseData<I extends UUIDBased> extends IdBased<I> implements Serializable {
+
+ private static final long serialVersionUID = 5422817607129962637L;
+
+ protected long createdTime;
+
+ public BaseData() {
+ super();
+ }
+
+ public BaseData(I id) {
+ super(id);
+ }
+
+ public BaseData(BaseData<I> data) {
+ super(data.getId());
+ this.createdTime = data.getCreatedTime();
+ }
+
+ public long getCreatedTime() {
+ return createdTime;
+ }
+
+ public void setCreatedTime(long createdTime) {
+ this.createdTime = createdTime;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + (int) (createdTime ^ (createdTime >>> 32));
+ return result;
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ BaseData other = (BaseData) obj;
+ if (createdTime != other.createdTime)
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("BaseData [createdTime=");
+ builder.append(createdTime);
+ builder.append(", id=");
+ builder.append(id);
+ builder.append("]");
+ return builder.toString();
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
new file mode 100644
index 0000000..ae62f41
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright © 2016 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;
+
+public class CacheConstants {
+ public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials";
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java
new file mode 100644
index 0000000..397435e
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java
@@ -0,0 +1,185 @@
+/**
+ * Copyright © 2016 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;
+
+import org.thingsboard.server.common.data.id.UUIDBased;
+
+public abstract class ContactBased<I extends UUIDBased> extends SearchTextBased<I> {
+
+ private static final long serialVersionUID = 5047448057830660988L;
+
+ protected String country;
+ protected String state;
+ protected String city;
+ protected String address;
+ protected String address2;
+ protected String zip;
+ protected String phone;
+ protected String email;
+
+ public ContactBased() {
+ super();
+ }
+
+ public ContactBased(I id) {
+ super(id);
+ }
+
+ public ContactBased(ContactBased<I> contact) {
+ super(contact);
+ this.country = contact.getCountry();
+ this.state = contact.getState();
+ this.city = contact.getCity();
+ this.address = contact.getAddress();
+ this.address2 = contact.getAddress2();
+ this.zip = contact.getZip();
+ this.phone = contact.getPhone();
+ this.email = contact.getEmail();
+ }
+
+ public String getCountry() {
+ return country;
+ }
+
+ public void setCountry(String country) {
+ this.country = country;
+ }
+
+ public String getState() {
+ return state;
+ }
+
+ public void setState(String state) {
+ this.state = state;
+ }
+
+ public String getCity() {
+ return city;
+ }
+
+ public void setCity(String city) {
+ this.city = city;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ public String getAddress2() {
+ return address2;
+ }
+
+ public void setAddress2(String address2) {
+ this.address2 = address2;
+ }
+
+ public String getZip() {
+ return zip;
+ }
+
+ public void setZip(String zip) {
+ this.zip = zip;
+ }
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public void setPhone(String phone) {
+ this.phone = phone;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + ((address == null) ? 0 : address.hashCode());
+ result = prime * result + ((address2 == null) ? 0 : address2.hashCode());
+ result = prime * result + ((city == null) ? 0 : city.hashCode());
+ result = prime * result + ((country == null) ? 0 : country.hashCode());
+ result = prime * result + ((email == null) ? 0 : email.hashCode());
+ result = prime * result + ((phone == null) ? 0 : phone.hashCode());
+ result = prime * result + ((state == null) ? 0 : state.hashCode());
+ result = prime * result + ((zip == null) ? 0 : zip.hashCode());
+ return result;
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ ContactBased other = (ContactBased) obj;
+ if (address == null) {
+ if (other.address != null)
+ return false;
+ } else if (!address.equals(other.address))
+ return false;
+ if (address2 == null) {
+ if (other.address2 != null)
+ return false;
+ } else if (!address2.equals(other.address2))
+ return false;
+ if (city == null) {
+ if (other.city != null)
+ return false;
+ } else if (!city.equals(other.city))
+ return false;
+ if (country == null) {
+ if (other.country != null)
+ return false;
+ } else if (!country.equals(other.country))
+ return false;
+ if (email == null) {
+ if (other.email != null)
+ return false;
+ } else if (!email.equals(other.email))
+ return false;
+ if (phone == null) {
+ if (other.phone != null)
+ return false;
+ } else if (!phone.equals(other.phone))
+ return false;
+ if (state == null) {
+ if (other.state != null)
+ return false;
+ } else if (!state.equals(other.state))
+ return false;
+ if (zip == null) {
+ if (other.zip != null)
+ return false;
+ } else if (!zip.equals(other.zip))
+ return false;
+ return true;
+ }
+
+}
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
new file mode 100644
index 0000000..3d06bdc
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
@@ -0,0 +1,145 @@
+/**
+ * Copyright © 2016 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;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class Customer extends ContactBased<CustomerId>{
+
+ private static final long serialVersionUID = -1599722990298929275L;
+
+ private String title;
+ private TenantId tenantId;
+ private JsonNode additionalInfo;
+
+ public Customer() {
+ super();
+ }
+
+ public Customer(CustomerId id) {
+ super(id);
+ }
+
+ public Customer(Customer customer) {
+ super(customer);
+ this.tenantId = customer.getTenantId();
+ this.title = customer.getTitle();
+ this.additionalInfo = customer.getAdditionalInfo();
+ }
+
+ public TenantId getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(TenantId tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public JsonNode getAdditionalInfo() {
+ return additionalInfo;
+ }
+
+ public void setAdditionalInfo(JsonNode additionalInfo) {
+ this.additionalInfo = additionalInfo;
+ }
+
+ @Override
+ public String getSearchText() {
+ return title;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
+ result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
+ result = prime * result + ((title == null) ? 0 : title.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Customer other = (Customer) obj;
+ if (additionalInfo == null) {
+ if (other.additionalInfo != null)
+ return false;
+ } else if (!additionalInfo.equals(other.additionalInfo))
+ return false;
+ if (tenantId == null) {
+ if (other.tenantId != null)
+ return false;
+ } else if (!tenantId.equals(other.tenantId))
+ return false;
+ if (title == null) {
+ if (other.title != null)
+ return false;
+ } else if (!title.equals(other.title))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Customer [title=");
+ builder.append(title);
+ builder.append(", tenantId=");
+ builder.append(tenantId);
+ builder.append(", additionalInfo=");
+ builder.append(additionalInfo);
+ builder.append(", country=");
+ builder.append(country);
+ builder.append(", state=");
+ builder.append(state);
+ builder.append(", city=");
+ builder.append(city);
+ builder.append(", address=");
+ builder.append(address);
+ builder.append(", address2=");
+ builder.append(address2);
+ builder.append(", zip=");
+ builder.append(zip);
+ builder.append(", phone=");
+ builder.append(phone);
+ builder.append(", email=");
+ builder.append(email);
+ builder.append(", createdTime=");
+ builder.append(createdTime);
+ builder.append(", id=");
+ builder.append(id);
+ builder.append("]");
+ return builder.toString();
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java
new file mode 100644
index 0000000..72879c9
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java
@@ -0,0 +1,144 @@
+/**
+ * Copyright © 2016 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;
+
+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 com.fasterxml.jackson.databind.JsonNode;
+
+public class Dashboard extends SearchTextBased<DashboardId> {
+
+ private static final long serialVersionUID = 872682138346187503L;
+
+ private TenantId tenantId;
+ private CustomerId customerId;
+ private String title;
+ private JsonNode configuration;
+
+ public Dashboard() {
+ super();
+ }
+
+ public Dashboard(DashboardId id) {
+ super(id);
+ }
+
+ public Dashboard(Dashboard dashboard) {
+ super(dashboard);
+ this.tenantId = dashboard.getTenantId();
+ this.customerId = dashboard.getCustomerId();
+ this.title = dashboard.getTitle();
+ this.configuration = dashboard.getConfiguration();
+ }
+
+ public TenantId getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(TenantId tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public CustomerId getCustomerId() {
+ return customerId;
+ }
+
+ public void setCustomerId(CustomerId customerId) {
+ this.customerId = customerId;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public JsonNode getConfiguration() {
+ return configuration;
+ }
+
+ public void setConfiguration(JsonNode configuration) {
+ this.configuration = configuration;
+ }
+
+ @Override
+ public String getSearchText() {
+ return title;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + ((configuration == null) ? 0 : configuration.hashCode());
+ result = prime * result + ((customerId == null) ? 0 : customerId.hashCode());
+ result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
+ result = prime * result + ((title == null) ? 0 : title.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Dashboard other = (Dashboard) obj;
+ if (configuration == null) {
+ if (other.configuration != null)
+ return false;
+ } else if (!configuration.equals(other.configuration))
+ return false;
+ if (customerId == null) {
+ if (other.customerId != null)
+ return false;
+ } else if (!customerId.equals(other.customerId))
+ return false;
+ if (tenantId == null) {
+ if (other.tenantId != null)
+ return false;
+ } else if (!tenantId.equals(other.tenantId))
+ return false;
+ if (title == null) {
+ if (other.title != null)
+ return false;
+ } else if (!title.equals(other.title))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Dashboard [tenantId=");
+ builder.append(tenantId);
+ builder.append(", customerId=");
+ builder.append(customerId);
+ builder.append(", title=");
+ builder.append(title);
+ builder.append(", configuration=");
+ builder.append(configuration);
+ builder.append("]");
+ return builder.toString();
+ }
+
+}
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
new file mode 100644
index 0000000..34ae9b2
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 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;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class DataConstants {
+
+ public static final String SYSTEM = "SYSTEM";
+ public static final String TENANT = "TENANT";
+ public static final String CUSTOMER = "CUSTOMER";
+ public static final String DEVICE = "DEVICE";
+
+ public static final String CLIENT_SCOPE = "CLIENT_SCOPE";
+ public static final String SERVER_SCOPE = "SERVER_SCOPE";
+ public static final String SHARED_SCOPE = "SHARED_SCOPE";
+
+ public static final String ALARM = "ALARM";
+ public static final String ERROR = "ERROR";
+ public static final String LC_EVENT = "LC_EVENT";
+ public static final String STATS = "STATS";
+
+ public static final String ONEWAY = "ONEWAY";
+ public static final String TWOWAY = "TWOWAY";
+}
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
new file mode 100644
index 0000000..aece691
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
@@ -0,0 +1,148 @@
+/**
+ * Copyright © 2016 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;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class Device extends SearchTextBased<DeviceId> {
+
+ private static final long serialVersionUID = 2807343040519543363L;
+
+ private TenantId tenantId;
+ private CustomerId customerId;
+ private String name;
+ private JsonNode additionalInfo;
+
+ public Device() {
+ super();
+ }
+
+ public Device(DeviceId id) {
+ super(id);
+ }
+
+ public Device(Device device) {
+ super(device);
+ this.tenantId = device.getTenantId();
+ this.customerId = device.getCustomerId();
+ this.name = device.getName();
+ this.additionalInfo = device.getAdditionalInfo();
+ }
+
+ public TenantId getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(TenantId tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public CustomerId getCustomerId() {
+ return customerId;
+ }
+
+ public void setCustomerId(CustomerId customerId) {
+ this.customerId = customerId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public JsonNode getAdditionalInfo() {
+ return additionalInfo;
+ }
+
+ public void setAdditionalInfo(JsonNode additionalInfo) {
+ this.additionalInfo = additionalInfo;
+ }
+
+ @Override
+ public String getSearchText() {
+ return name;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
+ result = prime * result + ((customerId == null) ? 0 : customerId.hashCode());
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Device other = (Device) obj;
+ if (additionalInfo == null) {
+ if (other.additionalInfo != null)
+ return false;
+ } else if (!additionalInfo.equals(other.additionalInfo))
+ return false;
+ if (customerId == null) {
+ if (other.customerId != null)
+ return false;
+ } else if (!customerId.equals(other.customerId))
+ return false;
+ if (name == null) {
+ if (other.name != null)
+ return false;
+ } else if (!name.equals(other.name))
+ return false;
+ if (tenantId == null) {
+ if (other.tenantId != null)
+ return false;
+ } else if (!tenantId.equals(other.tenantId))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Device [tenantId=");
+ builder.append(tenantId);
+ builder.append(", customerId=");
+ builder.append(customerId);
+ builder.append(", name=");
+ builder.append(name);
+ builder.append(", additionalInfo=");
+ builder.append(additionalInfo);
+ builder.append(", createdTime=");
+ builder.append(createdTime);
+ builder.append(", id=");
+ builder.append(id);
+ builder.append("]");
+ return builder.toString();
+ }
+
+}
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
new file mode 100644
index 0000000..b1247ea
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016 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;
+
+/**
+ * @author Andrew Shvayka
+ */
+public enum EntityType {
+ TENANT, DEVICE, CUSTOMER, RULE, PLUGIN
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Event.java b/common/data/src/main/java/org/thingsboard/server/common/data/Event.java
new file mode 100644
index 0000000..1298907
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Event.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016 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;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EventId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class Event extends BaseData<EventId> {
+
+ private TenantId tenantId;
+ private String type;
+ private String uid;
+ private EntityId entityId;
+ private JsonNode body;
+
+ public Event() {
+ super();
+ }
+
+ public Event(EventId id) {
+ super(id);
+ }
+
+ public Event(Event event) {
+ super(event);
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java
new file mode 100644
index 0000000..2f31136
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 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.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class AdminSettingsId extends UUIDBased {
+
+ @JsonCreator
+ public AdminSettingsId(@JsonProperty("id") UUID id){
+ super(id);
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/ComponentDescriptorId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/ComponentDescriptorId.java
new file mode 100644
index 0000000..43b023b
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/ComponentDescriptorId.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 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.id;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.UUID;
+
+public final class ComponentDescriptorId extends UUIDBased {
+
+ private static final long serialVersionUID = 1L;
+
+ @JsonCreator
+ public ComponentDescriptorId(@JsonProperty("id") UUID id) {
+ super(id);
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java
new file mode 100644
index 0000000..83fc95f
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 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.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.thingsboard.server.common.data.EntityType;
+
+public final class CustomerId extends UUIDBased implements EntityId {
+
+ private static final long serialVersionUID = 1L;
+
+ @JsonCreator
+ public CustomerId(@JsonProperty("id") UUID id) {
+ super(id);
+ }
+
+ @JsonIgnore
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.CUSTOMER;
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java
new file mode 100644
index 0000000..16968a4
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DashboardId extends UUIDBased {
+
+ @JsonCreator
+ public DashboardId(@JsonProperty("id") UUID id){
+ super(id);
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceCredentialsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceCredentialsId.java
new file mode 100644
index 0000000..58b86d0
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceCredentialsId.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DeviceCredentialsId extends UUIDBased {
+
+ @JsonCreator
+ public DeviceCredentialsId(@JsonProperty("id") UUID id) {
+ super(id);
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java
new file mode 100644
index 0000000..04d6790
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016 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.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.thingsboard.server.common.data.EntityType;
+
+public class DeviceId extends UUIDBased implements EntityId {
+
+ private static final long serialVersionUID = 1L;
+
+ @JsonCreator
+ public DeviceId(@JsonProperty("id") UUID id) {
+ super(id);
+ }
+
+ public static DeviceId fromString(String deviceId) {
+ return new DeviceId(UUID.fromString(deviceId));
+ }
+
+ @JsonIgnore
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.DEVICE;
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java
new file mode 100644
index 0000000..30612a1
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 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.id;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import org.thingsboard.server.common.data.EntityType;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface EntityId {
+
+ UUID NULL_UUID = UUID.fromString("13814000-1dd2-11b2-8080-808080808080");
+
+ UUID getId();
+
+ EntityType getEntityType();
+
+ @JsonIgnore
+ default boolean isNullUid() {
+ return NULL_UUID.equals(getId());
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EventId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EventId.java
new file mode 100644
index 0000000..12fd9d1
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EventId.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 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.id;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.UUID;
+
+public class EventId extends UUIDBased {
+
+ private static final long serialVersionUID = 1L;
+
+ @JsonCreator
+ public EventId(@JsonProperty("id") UUID id) {
+ super(id);
+ }
+
+ public static EventId fromString(String eventId) {
+ return new EventId(UUID.fromString(eventId));
+ }
+}
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
new file mode 100644
index 0000000..1f98db8
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright © 2016 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.id;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import java.util.UUID;
+
+public abstract class IdBased<I extends UUIDBased> {
+
+ protected I id;
+
+ public IdBased() {
+ super();
+ }
+
+ public IdBased(I id) {
+ super();
+ this.id = id;
+ }
+
+ public void setId(I id) {
+ this.id = id;
+ }
+
+ public I getId() {
+ return id;
+ }
+
+ @JsonIgnore
+ public UUID getUuidId() {
+ if (id != null) {
+ return id.getId();
+ }
+ return null;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ return result;
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ IdBased other = (IdBased) obj;
+ if (id == null) {
+ if (other.id != null)
+ return false;
+ } else if (!id.equals(other.id))
+ return false;
+ return true;
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NodeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NodeId.java
new file mode 100644
index 0000000..e77302d
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NodeId.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 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.id;
+
+import java.util.UUID;
+
+public class NodeId extends UUIDBased {
+
+ public NodeId(UUID id){
+ super(id);
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/PluginId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/PluginId.java
new file mode 100644
index 0000000..f86e8dd
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/PluginId.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 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.id;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.thingsboard.server.common.data.EntityType;
+
+import java.util.UUID;
+
+public final class PluginId extends UUIDBased implements EntityId {
+
+ private static final long serialVersionUID = 1L;
+
+ @JsonCreator
+ public PluginId(@JsonProperty("id") UUID id) {
+ super(id);
+ }
+
+ @JsonIgnore
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.PLUGIN;
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleId.java
new file mode 100644
index 0000000..05d822f
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleId.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016 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.id;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.thingsboard.server.common.data.EntityType;
+
+import java.util.UUID;
+
+public class RuleId extends UUIDBased implements EntityId {
+
+ @JsonCreator
+ public RuleId(@JsonProperty("id") UUID id) {
+ super(id);
+ }
+
+ @JsonIgnore
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.RULE;
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/SessionId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/SessionId.java
new file mode 100644
index 0000000..dc6f883
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/SessionId.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 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.id;
+
+import java.io.Serializable;
+
+public interface SessionId extends Serializable {
+
+ String toUidStr();
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java
new file mode 100644
index 0000000..8ef157b
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 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.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.thingsboard.server.common.data.EntityType;
+
+public final class TenantId extends UUIDBased implements EntityId {
+
+ private static final long serialVersionUID = 1L;
+
+ @JsonCreator
+ public TenantId(@JsonProperty("id") UUID id) {
+ super(id);
+ }
+
+ @JsonIgnore
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.TENANT;
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserCredentialsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserCredentialsId.java
new file mode 100644
index 0000000..d172793
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserCredentialsId.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 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.id;
+
+import java.util.UUID;
+
+public class UserCredentialsId extends UUIDBased {
+
+ public UserCredentialsId(UUID id){
+ super(id);
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java
new file mode 100644
index 0000000..53531d7
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class UserId extends UUIDBased {
+
+ @JsonCreator
+ public UserId(@JsonProperty("id") UUID id){
+ super(id);
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java
new file mode 100644
index 0000000..b8d07e0
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright © 2016 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.id;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+public abstract class UUIDBased implements Serializable {
+
+ public static final UUID EMPTY = new UUID(0L, 0L);
+
+ private static final long serialVersionUID = 1L;
+
+ private final UUID id;
+
+ public UUIDBased() {
+ this(UUID.randomUUID());
+ }
+
+ public UUIDBased(UUID id) {
+ super();
+ this.id = id;
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ UUIDBased other = (UUIDBased) obj;
+ if (id == null) {
+ if (other.id != null)
+ return false;
+ } else if (!id.equals(other.id))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return id.toString();
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java
new file mode 100644
index 0000000..2e97310
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 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.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public final class WidgetsBundleId extends UUIDBased {
+
+ private static final long serialVersionUID = 1L;
+
+ @JsonCreator
+ public WidgetsBundleId(@JsonProperty("id") UUID id) {
+ super(id);
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java
new file mode 100644
index 0000000..39d6518
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 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.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public final class WidgetTypeId extends UUIDBased {
+
+ private static final long serialVersionUID = 1L;
+
+ @JsonCreator
+ public WidgetTypeId(@JsonProperty("id") UUID id) {
+ super(id);
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKey.java
new file mode 100644
index 0000000..ae23773
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKey.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.kv;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class AttributeKey implements Serializable {
+ private final String scope;
+ private final String attributeKey;
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKvEntry.java
new file mode 100644
index 0000000..42af854
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKvEntry.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 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.kv;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface AttributeKvEntry extends KvEntry {
+
+ long getLastUpdateTs();
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java
new file mode 100644
index 0000000..afe430c
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright © 2016 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.kv;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class BaseAttributeKvEntry implements AttributeKvEntry {
+
+ private final long lastUpdateTs;
+ private final KvEntry kv;
+
+ public BaseAttributeKvEntry(KvEntry kv, long lastUpdateTs) {
+ this.kv = kv;
+ this.lastUpdateTs = lastUpdateTs;
+ }
+
+ @Override
+ public long getLastUpdateTs() {
+ return lastUpdateTs;
+ }
+
+ @Override
+ public String getKey() {
+ return kv.getKey();
+ }
+
+ @Override
+ public DataType getDataType() {
+ return kv.getDataType();
+ }
+
+ @Override
+ public Optional<String> getStrValue() {
+ return kv.getStrValue();
+ }
+
+ @Override
+ public Optional<Long> getLongValue() {
+ return kv.getLongValue();
+ }
+
+ @Override
+ public Optional<Boolean> getBooleanValue() {
+ return kv.getBooleanValue();
+ }
+
+ @Override
+ public Optional<Double> getDoubleValue() {
+ return kv.getDoubleValue();
+ }
+
+ @Override
+ public String getValueAsString() {
+ return kv.getValueAsString();
+ }
+
+ @Override
+ public Object getValue() {
+ return kv.getValue();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ BaseAttributeKvEntry that = (BaseAttributeKvEntry) o;
+
+ if (lastUpdateTs != that.lastUpdateTs) return false;
+ return kv.equals(that.kv);
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) (lastUpdateTs ^ (lastUpdateTs >>> 32));
+ result = 31 * result + kv.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "BaseAttributeKvEntry{" +
+ "lastUpdateTs=" + lastUpdateTs +
+ ", kv=" + kv +
+ '}';
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java
new file mode 100644
index 0000000..e74a91e
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright © 2016 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.kv;
+
+import java.util.Optional;
+
+public class BaseTsKvQuery implements TsKvQuery {
+
+ private String key;
+ private Optional<Long> startTs;
+ private Optional<Long> endTs;
+ private Optional<Integer> limit;
+
+ public BaseTsKvQuery(String key, Optional<Long> startTs, Optional<Long> endTs, Optional<Integer> limit) {
+ this.key = key;
+ this.startTs = startTs;
+ this.endTs = endTs;
+ this.limit = limit;
+ }
+
+ public BaseTsKvQuery(String key, Long startTs, Long endTs, Integer limit) {
+ this(key, Optional.ofNullable(startTs), Optional.ofNullable(endTs), Optional.ofNullable(limit));
+ }
+
+ public BaseTsKvQuery(String key, Long startTs, Integer limit) {
+ this(key, startTs, null, limit);
+ }
+
+ public BaseTsKvQuery(String key, Long startTs, Long endTs) {
+ this(key, startTs, endTs, null);
+ }
+
+ public BaseTsKvQuery(String key, Long startTs) {
+ this(key, startTs, null, null);
+ }
+
+ public BaseTsKvQuery(String key, Integer limit) {
+ this(key, null, null, limit);
+ }
+
+ @Override
+ public String getKey() {
+ return key;
+ }
+
+ @Override
+ public Optional<Long> getStartTs() {
+ return startTs;
+ }
+
+ @Override
+ public Optional<Long> getEndTs() {
+ return endTs;
+ }
+
+ @Override
+ public Optional<Integer> getLimit() {
+ return limit;
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicKvEntry.java
new file mode 100644
index 0000000..e0e0a4b
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicKvEntry.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright © 2016 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.kv;
+
+import java.util.Objects;
+import java.util.Optional;
+
+public abstract class BasicKvEntry implements KvEntry {
+
+ private final String key;
+
+ protected BasicKvEntry(String key) {
+ this.key = key;
+ }
+
+ @Override
+ public String getKey() {
+ return key;
+ }
+
+ @Override
+ public Optional<String> getStrValue() {
+ return Optional.ofNullable(null);
+ }
+
+ @Override
+ public Optional<Long> getLongValue() {
+ return Optional.ofNullable(null);
+ }
+
+ @Override
+ public Optional<Boolean> getBooleanValue() {
+ return Optional.ofNullable(null);
+ }
+
+ @Override
+ public Optional<Double> getDoubleValue() {
+ return Optional.ofNullable(null);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof BasicKvEntry)) return false;
+ BasicKvEntry that = (BasicKvEntry) o;
+ return Objects.equals(key, that.key);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key);
+ }
+
+ @Override
+ public String toString() {
+ return "BasicKvEntry{" +
+ "key='" + key + '\'' +
+ '}';
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java
new file mode 100644
index 0000000..b4442fa
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright © 2016 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.kv;
+
+import java.util.Objects;
+import java.util.Optional;
+
+public class BasicTsKvEntry implements TsKvEntry {
+
+ private final long ts;
+ private final KvEntry kv;
+
+ public BasicTsKvEntry(long ts, KvEntry kv) {
+ this.ts = ts;
+ this.kv = kv;
+ }
+
+ @Override
+ public String getKey() {
+ return kv.getKey();
+ }
+
+ @Override
+ public DataType getDataType() {
+ return kv.getDataType();
+ }
+
+ @Override
+ public Optional<String> getStrValue() {
+ return kv.getStrValue();
+ }
+
+ @Override
+ public Optional<Long> getLongValue() {
+ return kv.getLongValue();
+ }
+
+ @Override
+ public Optional<Boolean> getBooleanValue() {
+ return kv.getBooleanValue();
+ }
+
+ @Override
+ public Optional<Double> getDoubleValue() {
+ return kv.getDoubleValue();
+ }
+
+ @Override
+ public Object getValue() {
+ return kv.getValue();
+ }
+
+ @Override
+ public long getTs() {
+ return ts;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof BasicTsKvEntry)) return false;
+ BasicTsKvEntry that = (BasicTsKvEntry) o;
+ return getTs() == that.getTs() &&
+ Objects.equals(kv, that.kv);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getTs(), kv);
+ }
+
+ @Override
+ public String toString() {
+ return "BasicTsKvEntry{" +
+ "ts=" + ts +
+ ", kv=" + kv +
+ '}';
+ }
+
+ @Override
+ public String getValueAsString() {
+ return kv.getValueAsString();
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BooleanDataEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BooleanDataEntry.java
new file mode 100644
index 0000000..f13c723
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BooleanDataEntry.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright © 2016 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.kv;
+
+import java.util.Objects;
+import java.util.Optional;
+
+public class BooleanDataEntry extends BasicKvEntry {
+ private final Boolean value;
+
+ public BooleanDataEntry(String key, Boolean value) {
+ super(key);
+ this.value = value;
+ }
+
+ @Override
+ public DataType getDataType() {
+ return DataType.BOOLEAN;
+ }
+
+ @Override
+ public Optional<Boolean> getBooleanValue() {
+ return Optional.of(value);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof BooleanDataEntry)) return false;
+ if (!super.equals(o)) return false;
+ BooleanDataEntry that = (BooleanDataEntry) o;
+ return Objects.equals(value, that.value);
+ }
+
+ @Override
+ public Object getValue() {
+ return value;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), value);
+ }
+
+ @Override
+ public String toString() {
+ return "BooleanDataEntry{" +
+ "value=" + value +
+ "} " + super.toString();
+ }
+
+ @Override
+ public String getValueAsString() {
+ return Boolean.toString(value);
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/DataType.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/DataType.java
new file mode 100644
index 0000000..68f5358
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/DataType.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 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.kv;
+
+public enum DataType {
+
+ STRING, LONG, BOOLEAN, DOUBLE;
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/DoubleDataEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/DoubleDataEntry.java
new file mode 100644
index 0000000..e9cfe1c
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/DoubleDataEntry.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016 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.kv;
+
+import java.util.Objects;
+import java.util.Optional;
+
+public class DoubleDataEntry extends BasicKvEntry {
+
+ private final Double value;
+
+ public DoubleDataEntry(String key, Double value) {
+ super(key);
+ this.value = value;
+ }
+
+ @Override
+ public DataType getDataType() {
+ return DataType.DOUBLE;
+ }
+
+ @Override
+ public Optional<Double> getDoubleValue() {
+ return Optional.of(value);
+ }
+
+ @Override
+ public Object getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DoubleDataEntry)) return false;
+ if (!super.equals(o)) return false;
+ DoubleDataEntry that = (DoubleDataEntry) o;
+ return Objects.equals(value, that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), value);
+ }
+
+ @Override
+ public String toString() {
+ return "DoubleDataEntry{" +
+ "value=" + value +
+ "} " + super.toString();
+ }
+
+ @Override
+ public String getValueAsString() {
+ return Double.toString(value);
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/KvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/KvEntry.java
new file mode 100644
index 0000000..eeaa70c
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/KvEntry.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016 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.kv;
+
+import java.io.Serializable;
+import java.util.Optional;
+
+/**
+ * Represents attribute or any other KV data entry
+ *
+ * @author ashvayka
+ */
+public interface KvEntry extends Serializable {
+
+ String getKey();
+
+ DataType getDataType();
+
+ Optional<String> getStrValue();
+
+ Optional<Long> getLongValue();
+
+ Optional<Boolean> getBooleanValue();
+
+ Optional<Double> getDoubleValue();
+
+ String getValueAsString();
+
+ Object getValue();
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/LongDataEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/LongDataEntry.java
new file mode 100644
index 0000000..b72ee00
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/LongDataEntry.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016 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.kv;
+
+import java.util.Objects;
+import java.util.Optional;
+
+public class LongDataEntry extends BasicKvEntry {
+
+ private final Long value;
+
+ public LongDataEntry(String key, Long value) {
+ super(key);
+ this.value = value;
+ }
+
+ @Override
+ public DataType getDataType() {
+ return DataType.LONG;
+ }
+
+ @Override
+ public Optional<Long> getLongValue() {
+ return Optional.of(value);
+ }
+
+ @Override
+ public Object getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof LongDataEntry)) return false;
+ if (!super.equals(o)) return false;
+ LongDataEntry that = (LongDataEntry) o;
+ return Objects.equals(value, that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), value);
+ }
+
+ @Override
+ public String toString() {
+ return "LongDataEntry{" +
+ "value=" + value +
+ "} " + super.toString();
+ }
+
+ @Override
+ public String getValueAsString() {
+ return Long.toString(value);
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java
new file mode 100644
index 0000000..2512d70
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright © 2016 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.kv;
+
+import java.util.Objects;
+import java.util.Optional;
+
+public class StringDataEntry extends BasicKvEntry {
+
+ private static final long serialVersionUID = 1L;
+ private final String value;
+
+ public StringDataEntry(String key, String value) {
+ super(key);
+ this.value = value;
+ }
+
+ @Override
+ public DataType getDataType() {
+ return DataType.STRING;
+ }
+
+ @Override
+ public Optional<String> getStrValue() {
+ return Optional.of(value);
+ }
+
+ @Override
+ public Object getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (!(o instanceof StringDataEntry))
+ return false;
+ if (!super.equals(o))
+ return false;
+ StringDataEntry that = (StringDataEntry) o;
+ return Objects.equals(value, that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), value);
+ }
+
+ @Override
+ public String toString() {
+ return "StringDataEntry{" + "value='" + value + '\'' + "} " + super.toString();
+ }
+
+ @Override
+ public String getValueAsString() {
+ return value;
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java
new file mode 100644
index 0000000..cf5e275
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 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.kv;
+
+/**
+ * Represents time series KV data entry
+ *
+ * @author ashvayka
+ *
+ */
+public interface TsKvEntry extends KvEntry {
+
+ long getTs();
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java
new file mode 100644
index 0000000..f1e89e6
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 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.kv;
+
+import java.util.Optional;
+
+public interface TsKvQuery {
+
+ String getKey();
+
+ Optional<Long> getStartTs();
+
+ Optional<Long> getEndTs();
+
+ Optional<Integer> getLimit();
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageLink.java
new file mode 100644
index 0000000..ae19f39
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageLink.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016 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.page;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.UUID;
+
+@RequiredArgsConstructor
+@AllArgsConstructor
+public abstract class BasePageLink implements Serializable {
+
+ private static final long serialVersionUID = -4189954843653250481L;
+
+ @Getter protected final int limit;
+
+ @Getter @Setter protected UUID idOffset;
+
+}
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
new file mode 100644
index 0000000..692d472
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright © 2016 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.page;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.UUIDBased;
+
+public class PageDataIterable<T extends SearchTextBased<? extends UUIDBased>> implements Iterable<T>, Iterator<T> {
+
+ private final FetchFunction<T> function;
+ private final int fetchSize;
+
+ private List<T> currentItems;
+ private int currentIdx;
+ private boolean hasNextPack;
+ private TextPageLink nextPackLink;
+ private boolean initialized;
+
+ public PageDataIterable(FetchFunction<T> function, int fetchSize) {
+ super();
+ this.function = function;
+ this.fetchSize = fetchSize;
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ return this;
+ }
+
+ @Override
+ public boolean hasNext() {
+ if(!initialized){
+ fetch(new TextPageLink(fetchSize));
+ initialized = true;
+ }
+ if(currentIdx == currentItems.size()){
+ if(hasNextPack){
+ fetch(nextPackLink);
+ }
+ }
+ return currentIdx != currentItems.size();
+ }
+
+ private void fetch(TextPageLink link) {
+ TextPageData<T> pageData = function.fetch(link);
+ currentIdx = 0;
+ currentItems = pageData.getData();
+ hasNextPack = pageData.hasNext();
+ nextPackLink = pageData.getNextPageLink();
+ }
+
+ @Override
+ public T next() {
+ return currentItems.get(currentIdx++);
+ }
+
+ public static interface FetchFunction<T extends SearchTextBased<? extends UUIDBased>> {
+
+ TextPageData<T> fetch(TextPageLink link);
+
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageData.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageData.java
new file mode 100644
index 0000000..cc9d22d
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageData.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright © 2016 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.page;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.UUIDBased;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class TextPageData<T extends SearchTextBased<? extends UUIDBased>> {
+
+ private final List<T> data;
+ private final TextPageLink nextPageLink;
+ private final boolean hasNext;
+
+ public TextPageData(List<T> data, TextPageLink pageLink) {
+ super();
+ this.data = data;
+ int limit = pageLink.getLimit();
+ if (data != null && data.size() == limit) {
+ int index = data.size()-1;
+ UUID idOffset = data.get(index).getId().getId();
+ String textOffset = data.get(index).getSearchText();
+ nextPageLink = new TextPageLink(limit, pageLink.getTextSearch(), idOffset, textOffset);
+ hasNext = true;
+ } else {
+ nextPageLink = null;
+ hasNext = false;
+ }
+ }
+
+ @JsonCreator
+ public TextPageData(@JsonProperty("data") List<T> data,
+ @JsonProperty("nextPageLink") TextPageLink nextPageLink,
+ @JsonProperty("hasNext") boolean hasNext) {
+ this.data = data;
+ this.nextPageLink = nextPageLink;
+ this.hasNext = hasNext;
+ }
+
+ public List<T> getData() {
+ return data;
+ }
+
+ @JsonProperty("hasNext")
+ public boolean hasNext() {
+ return hasNext;
+ }
+
+ public TextPageLink getNextPageLink() {
+ return nextPageLink;
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageLink.java
new file mode 100644
index 0000000..d91b90f
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageLink.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright © 2016 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.page;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+import lombok.ToString;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.UUID;
+
+@ToString
+public class TextPageLink extends BasePageLink implements Serializable {
+
+ private static final long serialVersionUID = -4189954843653250480L;
+
+ @Getter private final String textSearch;
+ @Getter private final String textSearchBound;
+ @Getter private final String textOffset;
+
+ public TextPageLink(int limit) {
+ this(limit, null, null, null);
+ }
+
+ public TextPageLink(int limit, String textSearch) {
+ this(limit, textSearch, null, null);
+ }
+
+ public TextPageLink(int limit, String textSearch, UUID idOffset, String textOffset) {
+ super(limit, idOffset);
+ this.textSearch = textSearch != null ? textSearch.toLowerCase() : null;
+ this.textSearchBound = nextSequence(this.textSearch);
+ this.textOffset = textOffset != null ? textOffset.toLowerCase() : null;
+ }
+
+ @JsonCreator
+ public TextPageLink(@JsonProperty("limit") int limit,
+ @JsonProperty("textSearch") String textSearch,
+ @JsonProperty("textSearchBound") String textSearchBound,
+ @JsonProperty("textOffset") String textOffset,
+ @JsonProperty("idOffset") UUID idOffset) {
+ super(limit, idOffset);
+ this.textSearch = textSearch;
+ this.textSearchBound = textSearchBound;
+ this.textOffset = textOffset;
+ this.idOffset = idOffset;
+ }
+
+ private static String nextSequence(String input) {
+ if (input != null && input.length() > 0) {
+ char[] chars = input.toCharArray();
+ int i = chars.length - 1;
+ while (i >= 0 && ++chars[i--] == Character.MIN_VALUE) ;
+ if (i == -1 && (chars.length == 0 || chars[0] == Character.MIN_VALUE)) {
+ char buf[] = Arrays.copyOf(input.toCharArray(), input.length() + 1);
+ buf[buf.length - 1] = Character.MIN_VALUE;
+ return new String(buf);
+ }
+ return new String(chars);
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageData.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageData.java
new file mode 100644
index 0000000..6ed927e
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageData.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016 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.page;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.UUIDBased;
+
+import java.util.List;
+import java.util.UUID;
+
+public class TimePageData<T extends BaseData<? extends UUIDBased>> {
+
+ private final List<T> data;
+ private final TimePageLink nextPageLink;
+ private final boolean hasNext;
+
+ public TimePageData(List<T> data, TimePageLink pageLink) {
+ super();
+ this.data = data;
+ int limit = pageLink.getLimit();
+ if (data != null && data.size() == limit) {
+ int index = data.size() - 1;
+ UUID idOffset = data.get(index).getId().getId();
+ nextPageLink = new TimePageLink(limit, pageLink.getStartTime(), pageLink.getEndTime(), pageLink.isAscOrder(), idOffset);
+ hasNext = true;
+ } else {
+ nextPageLink = null;
+ hasNext = false;
+ }
+ }
+
+ @JsonCreator
+ public TimePageData(@JsonProperty("data") List<T> data,
+ @JsonProperty("nextPageLink") TimePageLink nextPageLink,
+ @JsonProperty("hasNext") boolean hasNext) {
+ this.data = data;
+ this.nextPageLink = nextPageLink;
+ this.hasNext = hasNext;
+ }
+
+ public List<T> getData() {
+ return data;
+ }
+
+ @JsonProperty("hasNext")
+ public boolean hasNext() {
+ return hasNext;
+ }
+
+ public TimePageLink getNextPageLink() {
+ return nextPageLink;
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageLink.java
new file mode 100644
index 0000000..5296a7f
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageLink.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 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.page;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+import lombok.ToString;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.UUID;
+
+@ToString
+public class TimePageLink extends BasePageLink implements Serializable {
+
+ private static final long serialVersionUID = -4189954843653250480L;
+
+ @Getter private final Long startTime;
+ @Getter private final Long endTime;
+ @Getter private final boolean ascOrder;
+
+ public TimePageLink(int limit) {
+ this(limit, null, null, false, null);
+ }
+
+ public TimePageLink(int limit, Long startTime) {
+ this(limit, startTime, null, false, null);
+ }
+
+ public TimePageLink(int limit, Long startTime, Long endTime) {
+ this(limit, startTime, endTime, false, null);
+ }
+
+ public TimePageLink(int limit, Long startTime, Long endTime, boolean ascOrder) {
+ this(limit, startTime, endTime, ascOrder, null);
+ }
+
+ @JsonCreator
+ public TimePageLink(@JsonProperty("limit") int limit,
+ @JsonProperty("startTime") Long startTime,
+ @JsonProperty("endTime") Long endTime,
+ @JsonProperty("ascOrder") boolean ascOrder,
+ @JsonProperty("idOffset") UUID idOffset) {
+ super(limit, idOffset);
+ this.startTime = startTime;
+ this.endTime = endTime;
+ this.ascOrder = ascOrder;
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java
new file mode 100644
index 0000000..dac6ef7
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java
@@ -0,0 +1,87 @@
+/**
+ * Copyright © 2016 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.plugin;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.*;
+import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.ComponentDescriptorId;
+
+/**
+ * @author Andrew Shvayka
+ */
+@ToString
+public class ComponentDescriptor extends SearchTextBased<ComponentDescriptorId> {
+
+ private static final long serialVersionUID = 1L;
+
+ @Getter @Setter private ComponentType type;
+ @Getter @Setter private ComponentScope scope;
+ @Getter @Setter private String name;
+ @Getter @Setter private String clazz;
+ @Getter @Setter private JsonNode configurationDescriptor;
+ @Getter @Setter private String actions;
+
+ public ComponentDescriptor() {
+ super();
+ }
+
+ public ComponentDescriptor(ComponentDescriptorId id) {
+ super(id);
+ }
+
+ public ComponentDescriptor(ComponentDescriptor plugin) {
+ super(plugin);
+ this.type = plugin.getType();
+ this.scope = plugin.getScope();
+ this.name = plugin.getName();
+ this.clazz = plugin.getClazz();
+ this.configurationDescriptor = plugin.getConfigurationDescriptor();
+ this.actions = plugin.getActions();
+ }
+
+ @Override
+ public String getSearchText() {
+ return name;
+ }
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ComponentDescriptor that = (ComponentDescriptor) o;
+
+ if (type != that.type) return false;
+ if (scope != that.scope) return false;
+ if (name != null ? !name.equals(that.name) : that.name != null) return false;
+ if (actions != null ? !actions.equals(that.actions) : that.actions != null) return false;
+ if (configurationDescriptor != null ? !configurationDescriptor.equals(that.configurationDescriptor) : that.configurationDescriptor != null) return false;
+ return clazz != null ? clazz.equals(that.clazz) : that.clazz == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (type != null ? type.hashCode() : 0);
+ result = 31 * result + (scope != null ? scope.hashCode() : 0);
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ result = 31 * result + (clazz != null ? clazz.hashCode() : 0);
+ result = 31 * result + (actions != null ? actions.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java
new file mode 100644
index 0000000..4126b52
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 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.plugin;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+public enum ComponentLifecycleEvent implements Serializable {
+ CREATED, STARTED, ACTIVATED, SUSPENDED, UPDATED, STOPPED, DELETED
+}
\ No newline at end of file
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleState.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleState.java
new file mode 100644
index 0000000..0ee6736
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleState.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016 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.plugin;
+
+/**
+ * @author Andrew Shvayka
+ */
+public enum ComponentLifecycleState {
+ ACTIVE, SUSPENDED
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentScope.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentScope.java
new file mode 100644
index 0000000..332563b
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentScope.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016 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.plugin;
+
+/**
+ * @author Andrew Shvayka
+ */
+public enum ComponentScope {
+ SYSTEM, TENANT
+}
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
new file mode 100644
index 0000000..439a1d8
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 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.plugin;
+
+/**
+ * @author Andrew Shvayka
+ */
+public enum ComponentType {
+
+ FILTER, PROCESSOR, ACTION, PLUGIN
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java
new file mode 100644
index 0000000..7cad18f
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java
@@ -0,0 +1,175 @@
+/**
+ * Copyright © 2016 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.plugin;
+
+import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class PluginMetaData extends SearchTextBased<PluginId> {
+
+ private static final long serialVersionUID = 1L;
+
+ private String apiToken;
+ private TenantId tenantId;
+ private String name;
+ private String clazz;
+ private boolean publicAccess;
+ private ComponentLifecycleState state;
+ private JsonNode configuration;
+ private JsonNode additionalInfo;
+
+ public PluginMetaData() {
+ super();
+ }
+
+ public PluginMetaData(PluginId id) {
+ super(id);
+ }
+
+ public PluginMetaData(PluginMetaData plugin) {
+ super(plugin);
+ this.apiToken = plugin.getApiToken();
+ this.tenantId = plugin.getTenantId();
+ this.name = plugin.getName();
+ this.clazz = plugin.getClazz();
+ this.publicAccess = plugin.isPublicAccess();
+ this.state = plugin.getState();
+ this.configuration = plugin.getConfiguration();
+ this.additionalInfo = plugin.getAdditionalInfo();
+ }
+
+ @Override
+ public String getSearchText() {
+ return name;
+ }
+
+ public String getApiToken() {
+ return apiToken;
+ }
+
+ public void setApiToken(String apiToken) {
+ this.apiToken = apiToken;
+ }
+
+ public TenantId getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(TenantId tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getClazz() {
+ return clazz;
+ }
+
+ public void setClazz(String clazz) {
+ this.clazz = clazz;
+ }
+
+ public JsonNode getConfiguration() {
+ return configuration;
+ }
+
+ public void setConfiguration(JsonNode configuration) {
+ this.configuration = configuration;
+ }
+
+ public boolean isPublicAccess() {
+ return publicAccess;
+ }
+
+ public void setPublicAccess(boolean publicAccess) {
+ this.publicAccess = publicAccess;
+ }
+
+ public void setState(ComponentLifecycleState state) {
+ this.state = state;
+ }
+
+ public ComponentLifecycleState getState() {
+ return state;
+ }
+
+ public JsonNode getAdditionalInfo() {
+ return additionalInfo;
+ }
+
+ public void setAdditionalInfo(JsonNode additionalInfo) {
+ this.additionalInfo = additionalInfo;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + ((apiToken == null) ? 0 : apiToken.hashCode());
+ result = prime * result + ((clazz == null) ? 0 : clazz.hashCode());
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ PluginMetaData other = (PluginMetaData) obj;
+ if (apiToken == null) {
+ if (other.apiToken != null)
+ return false;
+ } else if (!apiToken.equals(other.apiToken))
+ return false;
+ if (clazz == null) {
+ if (other.clazz != null)
+ return false;
+ } else if (!clazz.equals(other.clazz))
+ return false;
+ if (name == null) {
+ if (other.name != null)
+ return false;
+ } else if (!name.equals(other.name))
+ return false;
+ if (tenantId == null) {
+ if (other.tenantId != null)
+ return false;
+ } else if (!tenantId.equals(other.tenantId))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "PluginMetaData [apiToken=" + apiToken + ", tenantId=" + tenantId + ", name=" + name + ", clazz=" + clazz + ", publicAccess=" + publicAccess
+ + ", configuration=" + configuration + "]";
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java
new file mode 100644
index 0000000..3ed132d
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright © 2016 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 lombok.Data;
+import lombok.ToString;
+import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+
+@Data
+public class RuleMetaData extends SearchTextBased<RuleId> {
+
+ private static final long serialVersionUID = -5656679015122935465L;
+
+ private TenantId tenantId;
+ private String name;
+ private ComponentLifecycleState state;
+ private int weight;
+ private String pluginToken;
+ private JsonNode filters;
+ private JsonNode processor;
+ private JsonNode action;
+ private JsonNode additionalInfo;
+
+ public RuleMetaData() {
+ super();
+ }
+
+ public RuleMetaData(RuleId id) {
+ super(id);
+ }
+
+ public RuleMetaData(RuleMetaData rule) {
+ super(rule);
+ this.tenantId = rule.getTenantId();
+ this.name = rule.getName();
+ this.state = rule.getState();
+ this.weight = rule.getWeight();
+ this.pluginToken = rule.getPluginToken();
+ this.filters = rule.getFilters();
+ this.processor = rule.getProcessor();
+ this.action = rule.getAction();
+ this.additionalInfo = rule.getAdditionalInfo();
+ }
+
+ @Override
+ public String getSearchText() {
+ return name;
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleType.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleType.java
new file mode 100644
index 0000000..f5b0700
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleType.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 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;
+
+/**
+ * Defines origin of the rule.
+ *
+ * @author ashvayka
+ *
+ */
+public enum RuleType {
+
+ SYSTEM, USER;
+
+}
\ No newline at end of file
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/Scope.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/Scope.java
new file mode 100644
index 0000000..e8a9870
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/Scope.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 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;
+
+/**
+ * Defines scope of the rule execution in the actor system
+ *
+ * @author ashvayka
+ *
+ */
+public enum Scope {
+
+ SYSTEM, TENANT, CUSTOMER, DEVICE, RULE;
+
+}
\ No newline at end of file
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBased.java
new file mode 100644
index 0000000..c63d8ca
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBased.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016 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;
+
+import org.thingsboard.server.common.data.id.UUIDBased;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+public abstract class SearchTextBased<I extends UUIDBased> extends BaseData<I> {
+
+ private static final long serialVersionUID = -539812997348227609L;
+
+ public SearchTextBased() {
+ super();
+ }
+
+ public SearchTextBased(I id) {
+ super(id);
+ }
+
+ public SearchTextBased(SearchTextBased<I> searchTextBased) {
+ super(searchTextBased);
+ }
+
+ @JsonIgnore
+ public abstract String getSearchText();
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java
new file mode 100644
index 0000000..568d07b
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright © 2016 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.security;
+
+public enum Authority {
+
+ SYS_ADMIN(0),
+ TENANT_ADMIN(1),
+ CUSTOMER_USER(2),
+ REFRESH_TOKEN(10);
+
+ private int code;
+
+ Authority(int code) {
+ this.code = code;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public static Authority parse(String value) {
+ Authority authority = null;
+ if (value != null && value.length() != 0) {
+ for (Authority current : Authority.values()) {
+ if (current.name().equalsIgnoreCase(value)) {
+ authority = current;
+ break;
+ }
+ }
+ }
+ return authority;
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java
new file mode 100644
index 0000000..a64d324
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java
@@ -0,0 +1,128 @@
+/**
+ * Copyright © 2016 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.security;
+
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.id.DeviceCredentialsId;
+import org.thingsboard.server.common.data.id.DeviceId;
+
+public class DeviceCredentials extends BaseData<DeviceCredentialsId> implements DeviceCredentialsFilter {
+
+ private static final long serialVersionUID = -7869261127032877765L;
+
+ private DeviceId deviceId;
+ private DeviceCredentialsType credentialsType;
+ private String credentialsId;
+ private String credentialsValue;
+
+ public DeviceCredentials() {
+ super();
+ }
+
+ public DeviceCredentials(DeviceCredentialsId id) {
+ super(id);
+ }
+
+ public DeviceCredentials(DeviceCredentials deviceCredentials) {
+ super(deviceCredentials);
+ this.deviceId = deviceCredentials.getDeviceId();
+ this.credentialsType = deviceCredentials.getCredentialsType();
+ this.credentialsId = deviceCredentials.getCredentialsId();
+ this.credentialsValue = deviceCredentials.getCredentialsValue();
+ }
+
+ public DeviceId getDeviceId() {
+ return deviceId;
+ }
+
+ public void setDeviceId(DeviceId deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ @Override
+ public DeviceCredentialsType getCredentialsType() {
+ return credentialsType;
+ }
+
+ public void setCredentialsType(DeviceCredentialsType credentialsType) {
+ this.credentialsType = credentialsType;
+ }
+
+ @Override
+ public String getCredentialsId() {
+ return credentialsId;
+ }
+
+ public void setCredentialsId(String credentialsId) {
+ this.credentialsId = credentialsId;
+ }
+
+ public String getCredentialsValue() {
+ return credentialsValue;
+ }
+
+ public void setCredentialsValue(String credentialsValue) {
+ this.credentialsValue = credentialsValue;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + ((credentialsId == null) ? 0 : credentialsId.hashCode());
+ result = prime * result + ((credentialsType == null) ? 0 : credentialsType.hashCode());
+ result = prime * result + ((credentialsValue == null) ? 0 : credentialsValue.hashCode());
+ result = prime * result + ((deviceId == null) ? 0 : deviceId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ DeviceCredentials other = (DeviceCredentials) obj;
+ if (credentialsId == null) {
+ if (other.credentialsId != null)
+ return false;
+ } else if (!credentialsId.equals(other.credentialsId))
+ return false;
+ if (credentialsType != other.credentialsType)
+ return false;
+ if (credentialsValue == null) {
+ if (other.credentialsValue != null)
+ return false;
+ } else if (!credentialsValue.equals(other.credentialsValue))
+ return false;
+ if (deviceId == null) {
+ if (other.deviceId != null)
+ return false;
+ } else if (!deviceId.equals(other.deviceId))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "DeviceCredentials [deviceId=" + deviceId + ", credentialsType=" + credentialsType + ", credentialsId="
+ + credentialsId + ", credentialsValue=" + credentialsValue + ", createdTime=" + createdTime + ", id="
+ + id + "]";
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsFilter.java
new file mode 100644
index 0000000..c4b4dcc
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsFilter.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 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.security;
+
+/**
+ * TODO: This is a temporary name. DeviceCredentialsId is resereved in dao layer
+ */
+public interface DeviceCredentialsFilter {
+
+ String getCredentialsId();
+
+ DeviceCredentialsType getCredentialsType();
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java
new file mode 100644
index 0000000..3daa1e4
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 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.security;
+
+public enum DeviceCredentialsType {
+
+ ACCESS_TOKEN
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceTokenCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceTokenCredentials.java
new file mode 100644
index 0000000..8ce9f00
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceTokenCredentials.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016 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.security;
+
+public class DeviceTokenCredentials implements DeviceCredentialsFilter {
+
+ private final String token;
+
+ public DeviceTokenCredentials(String token) {
+ super();
+ this.token = token;
+ }
+
+ @Override
+ public DeviceCredentialsType getCredentialsType() {
+ return DeviceCredentialsType.ACCESS_TOKEN;
+ }
+
+ @Override
+ public String getCredentialsId() {
+ return token;
+ }
+
+ @Override
+ public String toString() {
+ return "DeviceTokenCredentials [token=" + token + "]";
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java
new file mode 100644
index 0000000..42d90bb
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java
@@ -0,0 +1,156 @@
+/**
+ * Copyright © 2016 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.security;
+
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.id.UserCredentialsId;
+import org.thingsboard.server.common.data.id.UserId;
+
+public class UserCredentials extends BaseData<UserCredentialsId> {
+
+ private static final long serialVersionUID = -2108436378880529163L;
+
+ private UserId userId;
+ private boolean enabled;
+ private String password;
+ private String activateToken;
+ private String resetToken;
+
+ public UserCredentials() {
+ super();
+ }
+
+ public UserCredentials(UserCredentialsId id) {
+ super(id);
+ }
+
+ public UserCredentials(UserCredentials userCredentials) {
+ super(userCredentials);
+ this.userId = userCredentials.getUserId();
+ this.password = userCredentials.getPassword();
+ this.enabled = userCredentials.isEnabled();
+ this.activateToken = userCredentials.getActivateToken();
+ this.resetToken = userCredentials.getResetToken();
+ }
+
+ public UserId getUserId() {
+ return userId;
+ }
+
+ public void setUserId(UserId userId) {
+ this.userId = userId;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getActivateToken() {
+ return activateToken;
+ }
+
+ public void setActivateToken(String activateToken) {
+ this.activateToken = activateToken;
+ }
+
+ public String getResetToken() {
+ return resetToken;
+ }
+
+ public void setResetToken(String resetToken) {
+ this.resetToken = resetToken;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + ((activateToken == null) ? 0 : activateToken.hashCode());
+ result = prime * result + (enabled ? 1231 : 1237);
+ result = prime * result + ((password == null) ? 0 : password.hashCode());
+ result = prime * result + ((resetToken == null) ? 0 : resetToken.hashCode());
+ result = prime * result + ((userId == null) ? 0 : userId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ UserCredentials other = (UserCredentials) obj;
+ if (activateToken == null) {
+ if (other.activateToken != null)
+ return false;
+ } else if (!activateToken.equals(other.activateToken))
+ return false;
+ if (enabled != other.enabled)
+ return false;
+ if (password == null) {
+ if (other.password != null)
+ return false;
+ } else if (!password.equals(other.password))
+ return false;
+ if (resetToken == null) {
+ if (other.resetToken != null)
+ return false;
+ } else if (!resetToken.equals(other.resetToken))
+ return false;
+ if (userId == null) {
+ if (other.userId != null)
+ return false;
+ } else if (!userId.equals(other.userId))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("UserCredentials [userId=");
+ builder.append(userId);
+ builder.append(", enabled=");
+ builder.append(enabled);
+ builder.append(", password=");
+ builder.append(password);
+ builder.append(", activateToken=");
+ builder.append(activateToken);
+ builder.append(", resetToken=");
+ builder.append(resetToken);
+ builder.append(", createdTime=");
+ builder.append(createdTime);
+ builder.append(", id=");
+ builder.append(id);
+ builder.append("]");
+ return builder.toString();
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
new file mode 100644
index 0000000..2c22967
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
@@ -0,0 +1,144 @@
+/**
+ * Copyright © 2016 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;
+
+import org.thingsboard.server.common.data.id.TenantId;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class Tenant extends ContactBased<TenantId>{
+
+ private static final long serialVersionUID = 8057243243859922101L;
+
+ private String title;
+ private String region;
+ private JsonNode additionalInfo;
+
+ public Tenant() {
+ super();
+ }
+
+ public Tenant(TenantId id) {
+ super(id);
+ }
+
+ public Tenant(Tenant tenant) {
+ super(tenant);
+ this.title = tenant.getTitle();
+ this.region = tenant.getRegion();
+ this.additionalInfo = tenant.getAdditionalInfo();
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getRegion() {
+ return region;
+ }
+
+ public void setRegion(String region) {
+ this.region = region;
+ }
+
+ public JsonNode getAdditionalInfo() {
+ return additionalInfo;
+ }
+
+ public void setAdditionalInfo(JsonNode additionalInfo) {
+ this.additionalInfo = additionalInfo;
+ }
+
+ @Override
+ public String getSearchText() {
+ return title;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
+ result = prime * result + ((region == null) ? 0 : region.hashCode());
+ result = prime * result + ((title == null) ? 0 : title.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Tenant other = (Tenant) obj;
+ if (additionalInfo == null) {
+ if (other.additionalInfo != null)
+ return false;
+ } else if (!additionalInfo.equals(other.additionalInfo))
+ return false;
+ if (region == null) {
+ if (other.region != null)
+ return false;
+ } else if (!region.equals(other.region))
+ return false;
+ if (title == null) {
+ if (other.title != null)
+ return false;
+ } else if (!title.equals(other.title))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Tenant [title=");
+ builder.append(title);
+ builder.append(", region=");
+ builder.append(region);
+ builder.append(", additionalInfo=");
+ builder.append(additionalInfo);
+ builder.append(", country=");
+ builder.append(country);
+ builder.append(", state=");
+ builder.append(state);
+ builder.append(", city=");
+ builder.append(city);
+ builder.append(", address=");
+ builder.append(address);
+ builder.append(", address2=");
+ builder.append(address2);
+ builder.append(", zip=");
+ builder.append(zip);
+ builder.append(", phone=");
+ builder.append(phone);
+ builder.append(", email=");
+ builder.append(email);
+ builder.append(", createdTime=");
+ builder.append(createdTime);
+ builder.append(", id=");
+ builder.append(id);
+ builder.append("]");
+ return builder.toString();
+ }
+
+}
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
new file mode 100644
index 0000000..2124454
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/User.java
@@ -0,0 +1,200 @@
+/**
+ * Copyright © 2016 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;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.security.Authority;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class User extends SearchTextBased<UserId> {
+
+ private static final long serialVersionUID = 8250339805336035966L;
+
+ private TenantId tenantId;
+ private CustomerId customerId;
+ private String email;
+ private Authority authority;
+ private String firstName;
+ private String lastName;
+ private JsonNode additionalInfo;
+
+ public User() {
+ super();
+ }
+
+ public User(UserId id) {
+ super(id);
+ }
+
+ public User(User user) {
+ super(user);
+ this.tenantId = user.getTenantId();
+ this.customerId = user.getCustomerId();
+ this.email = user.getEmail();
+ this.authority = user.getAuthority();
+ this.firstName = user.getFirstName();
+ this.lastName = user.getLastName();
+ this.additionalInfo = user.getAdditionalInfo();
+ }
+
+ public TenantId getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(TenantId tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public CustomerId getCustomerId() {
+ return customerId;
+ }
+
+ public void setCustomerId(CustomerId customerId) {
+ this.customerId = customerId;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public Authority getAuthority() {
+ return authority;
+ }
+
+ public void setAuthority(Authority authority) {
+ this.authority = authority;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public JsonNode getAdditionalInfo() {
+ return additionalInfo;
+ }
+
+ public void setAdditionalInfo(JsonNode additionalInfo) {
+ this.additionalInfo = additionalInfo;
+ }
+
+ @Override
+ public String getSearchText() {
+ return email;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
+ result = prime * result + ((authority == null) ? 0 : authority.hashCode());
+ result = prime * result + ((customerId == null) ? 0 : customerId.hashCode());
+ result = prime * result + ((email == null) ? 0 : email.hashCode());
+ result = prime * result + ((firstName == null) ? 0 : firstName.hashCode());
+ result = prime * result + ((lastName == null) ? 0 : lastName.hashCode());
+ result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ User other = (User) obj;
+ if (additionalInfo == null) {
+ if (other.additionalInfo != null)
+ return false;
+ } else if (!additionalInfo.equals(other.additionalInfo))
+ return false;
+ if (authority != other.authority)
+ return false;
+ if (customerId == null) {
+ if (other.customerId != null)
+ return false;
+ } else if (!customerId.equals(other.customerId))
+ return false;
+ if (email == null) {
+ if (other.email != null)
+ return false;
+ } else if (!email.equals(other.email))
+ return false;
+ if (firstName == null) {
+ if (other.firstName != null)
+ return false;
+ } else if (!firstName.equals(other.firstName))
+ return false;
+ if (lastName == null) {
+ if (other.lastName != null)
+ return false;
+ } else if (!lastName.equals(other.lastName))
+ return false;
+ if (tenantId == null) {
+ if (other.tenantId != null)
+ return false;
+ } else if (!tenantId.equals(other.tenantId))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("User [tenantId=");
+ builder.append(tenantId);
+ builder.append(", customerId=");
+ builder.append(customerId);
+ builder.append(", email=");
+ builder.append(email);
+ builder.append(", authority=");
+ builder.append(authority);
+ builder.append(", firstName=");
+ builder.append(firstName);
+ builder.append(", lastName=");
+ builder.append(lastName);
+ builder.append(", additionalInfo=");
+ builder.append(additionalInfo);
+ builder.append(", createdTime=");
+ builder.append(createdTime);
+ builder.append(", id=");
+ builder.append(id);
+ builder.append("]");
+ return builder.toString();
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java
new file mode 100644
index 0000000..65a7f72
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java
@@ -0,0 +1,121 @@
+/**
+ * Copyright © 2016 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.widget;
+
+import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.WidgetsBundleId;
+
+import java.util.Arrays;
+
+public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> {
+
+ private static final long serialVersionUID = -7627368878362410489L;
+
+ private TenantId tenantId;
+ private String alias;
+ private String title;
+ private byte[] image;
+
+ public WidgetsBundle() {
+ super();
+ }
+
+ public WidgetsBundle(WidgetsBundleId id) {
+ super(id);
+ }
+
+ public WidgetsBundle(WidgetsBundle widgetsBundle) {
+ super(widgetsBundle);
+ this.tenantId = widgetsBundle.getTenantId();
+ this.alias = widgetsBundle.getAlias();
+ this.title = widgetsBundle.getTitle();
+ this.image = widgetsBundle.getImage();
+ }
+
+ public TenantId getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(TenantId tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public String getAlias() {
+ return alias;
+ }
+
+ public void setAlias(String alias) {
+ this.alias = alias;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public byte[] getImage() {
+ return image;
+ }
+
+ public void setImage(byte[] image) {
+ this.image = image;
+ }
+
+ @Override
+ public String getSearchText() {
+ return title;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0);
+ result = 31 * result + (alias != null ? alias.hashCode() : 0);
+ result = 31 * result + (title != null ? title.hashCode() : 0);
+ result = 31 * result + Arrays.hashCode(image);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+
+ WidgetsBundle that = (WidgetsBundle) o;
+
+ if (tenantId != null ? !tenantId.equals(that.tenantId) : that.tenantId != null) return false;
+ if (alias != null ? !alias.equals(that.alias) : that.alias != null) return false;
+ if (title != null ? !title.equals(that.title) : that.title != null) return false;
+ return Arrays.equals(image, that.image);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("WidgetsBundle{");
+ sb.append("tenantId=").append(tenantId);
+ sb.append(", alias='").append(alias).append('\'');
+ sb.append(", title='").append(title).append('\'');
+ sb.append(", image=").append(Arrays.toString(image));
+ sb.append('}');
+ return sb.toString();
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java
new file mode 100644
index 0000000..2f2851c
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java
@@ -0,0 +1,129 @@
+/**
+ * Copyright © 2016 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.widget;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.WidgetTypeId;
+
+public class WidgetType extends BaseData<WidgetTypeId> {
+
+ private static final long serialVersionUID = 8388684344603660756L;
+
+ private TenantId tenantId;
+ private String bundleAlias;
+ private String alias;
+ private String name;
+ private JsonNode descriptor;
+
+ public WidgetType() {
+ super();
+ }
+
+ public WidgetType(WidgetTypeId id) {
+ super(id);
+ }
+
+ public WidgetType(WidgetType widgetType) {
+ super(widgetType);
+ this.tenantId = widgetType.getTenantId();
+ this.bundleAlias = widgetType.getBundleAlias();
+ this.alias = widgetType.getAlias();
+ this.name = widgetType.getName();
+ this.descriptor = widgetType.getDescriptor();
+ }
+
+ public TenantId getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(TenantId tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public String getBundleAlias() {
+ return bundleAlias;
+ }
+
+ public void setBundleAlias(String bundleAlias) {
+ this.bundleAlias = bundleAlias;
+ }
+
+ public String getAlias() {
+ return alias;
+ }
+
+ public void setAlias(String alias) {
+ this.alias = alias;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public JsonNode getDescriptor() {
+ return descriptor;
+ }
+
+ public void setDescriptor(JsonNode descriptor) {
+ this.descriptor = descriptor;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0);
+ result = 31 * result + (bundleAlias != null ? bundleAlias.hashCode() : 0);
+ result = 31 * result + (alias != null ? alias.hashCode() : 0);
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ result = 31 * result + (descriptor != null ? descriptor.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+
+ WidgetType that = (WidgetType) o;
+
+ if (tenantId != null ? !tenantId.equals(that.tenantId) : that.tenantId != null) return false;
+ if (bundleAlias != null ? !bundleAlias.equals(that.bundleAlias) : that.bundleAlias != null) return false;
+ if (alias != null ? !alias.equals(that.alias) : that.alias != null) return false;
+ if (name != null ? !name.equals(that.name) : that.name != null) return false;
+ return descriptor != null ? descriptor.equals(that.descriptor) : that.descriptor == null;
+
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("WidgetType{");
+ sb.append("tenantId=").append(tenantId);
+ sb.append(", bundleAlias='").append(bundleAlias).append('\'');
+ sb.append(", alias='").append(alias).append('\'');
+ sb.append(", name='").append(name).append('\'');
+ sb.append(", descriptor=").append(descriptor);
+ sb.append('}');
+ return sb.toString();
+ }
+
+}
common/message/pom.xml 76(+76 -0)
diff --git a/common/message/pom.xml b/common/message/pom.xml
new file mode 100644
index 0000000..be14928
--- /dev/null
+++ b/common/message/pom.xml
@@ -0,0 +1,76 @@
+<!--
+
+ Copyright © 2016 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.server</groupId>
+ <version>0.0.1-SNAPSHOT</version>
+ <artifactId>common</artifactId>
+ </parent>
+ <groupId>org.thingsboard.server.common</groupId>
+ <artifactId>message</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Thingsboard Server Common Messages</name>
+ <url>http://thingsboard.org</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <main.dir>${basedir}/../..</main.dir>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.thingsboard.server.common</groupId>
+ <artifactId>data</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>log4j-over-slf4j</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ </plugins>
+ </build>
+
+</project>
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/CustomerAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/CustomerAwareMsg.java
new file mode 100644
index 0000000..d3911be
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/CustomerAwareMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 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.aware;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+
+public interface CustomerAwareMsg {
+
+ CustomerId getCustomerId();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/DeviceAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/DeviceAwareMsg.java
new file mode 100644
index 0000000..2d1c85b
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/DeviceAwareMsg.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016 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.aware;
+
+import org.thingsboard.server.common.data.id.DeviceId;
+
+public interface DeviceAwareMsg {
+
+ DeviceId getDeviceId();
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/NodeAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/NodeAwareMsg.java
new file mode 100644
index 0000000..64ea606
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/NodeAwareMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 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.aware;
+
+import org.thingsboard.server.common.data.id.NodeId;
+
+public interface NodeAwareMsg {
+
+ NodeId getNodeId();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/PluginAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/PluginAwareMsg.java
new file mode 100644
index 0000000..c0b4a26
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/PluginAwareMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 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.aware;
+
+import org.thingsboard.server.common.data.id.PluginId;
+
+public interface PluginAwareMsg {
+
+ PluginId getPluginId();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleAwareMsg.java
new file mode 100644
index 0000000..43631d9
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleAwareMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 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.aware;
+
+import org.thingsboard.server.common.data.id.RuleId;
+
+public interface RuleAwareMsg {
+
+ RuleId getRuleId();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/SessionAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/SessionAwareMsg.java
new file mode 100644
index 0000000..c4b7856
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/SessionAwareMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 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.aware;
+
+import org.thingsboard.server.common.data.id.SessionId;
+
+public interface SessionAwareMsg {
+
+ SessionId getSessionId();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java
new file mode 100644
index 0000000..5a4cc97
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 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.aware;
+
+import org.thingsboard.server.common.data.id.TenantId;
+
+public interface TenantAwareMsg {
+
+ TenantId getTenantId();
+
+}
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
new file mode 100644
index 0000000..dba41f7
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ClusterEventMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.cluster;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public final class ClusterEventMsg {
+
+ private final ServerAddress serverAddress;
+ private final boolean added;
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ServerAddress.java b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ServerAddress.java
new file mode 100644
index 0000000..d574bac
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ServerAddress.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016 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.cluster;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+@EqualsAndHashCode
+public class ServerAddress implements Comparable<ServerAddress>, Serializable {
+
+ private final String host;
+ private final int port;
+
+ @Override
+ public int compareTo(ServerAddress o) {
+ int result = this.host.compareTo(o.host);
+ if (result == 0) {
+ result = this.port - o.port;
+ }
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return '[' + host + ':' + port + ']';
+ }
+}
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
new file mode 100644
index 0000000..9aae56b
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ToAllNodesMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 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.cluster;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ToAllNodesMsg extends Serializable {
+}
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
new file mode 100644
index 0000000..f782873
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesSubscribeMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.core;
+
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class AttributesSubscribeMsg implements FromDeviceMsg {
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.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
new file mode 100644
index 0000000..a38a4cb
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUnsubscribeMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.core;
+
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class AttributesUnsubscribeMsg implements FromDeviceMsg {
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.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
new file mode 100644
index 0000000..e3a92ca
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUpdateNotification.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright © 2016 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.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.ToDeviceMsg;
+
+@ToString
+public class AttributesUpdateNotification implements ToDeviceMsg {
+
+ private static final long serialVersionUID = 1L;
+
+ private AttributesKVMsg data;
+
+ public AttributesUpdateNotification(AttributesKVMsg data) {
+ this.data = data;
+ }
+
+ @Override
+ public boolean isSuccess() {
+ return true;
+ }
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.ATTRIBUTES_UPDATE_NOTIFICATION;
+ }
+
+ public AttributesKVMsg getData() {
+ return data;
+ }
+}
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
new file mode 100644
index 0000000..82e4377
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicCommandAckResponse.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016 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.core;
+
+import org.thingsboard.server.common.msg.session.MsgType;
+
+public class BasicCommandAckResponse extends BasicResponseMsg<Integer> implements StatusCodeResponse {
+
+ private static final long serialVersionUID = 1L;
+
+ public static BasicCommandAckResponse onSuccess(MsgType requestMsgType, Integer requestId) {
+ return BasicCommandAckResponse.onSuccess(requestMsgType, requestId, 200);
+ }
+
+ public static BasicCommandAckResponse onSuccess(MsgType requestMsgType, Integer requestId, Integer code) {
+ return new BasicCommandAckResponse(requestMsgType, requestId, true, null, code);
+ }
+
+ public static BasicCommandAckResponse onError(MsgType 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);
+ }
+
+ @Override
+ public String toString() {
+ return "BasicStatusCodeResponse []";
+ }
+}
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
new file mode 100644
index 0000000..a8fdc86
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright © 2016 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.core;
+
+import lombok.ToString;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+import java.util.Set;
+
+@ToString
+public class BasicGetAttributesRequest extends BasicRequest implements GetAttributesRequest {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Set<String> clientKeys;
+ private final Set<String> sharedKeys;
+
+ public BasicGetAttributesRequest(Integer requestId, Set<String> clientKeys, Set<String> sharedKeys) {
+ super(requestId);
+ this.clientKeys = clientKeys;
+ this.sharedKeys = sharedKeys;
+ }
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.GET_ATTRIBUTES_REQUEST;
+ }
+
+ @Override
+ public Set<String> getClientAttributeNames() {
+ return clientKeys;
+ }
+
+ @Override
+ public Set<String> getSharedAttributeNames() {
+ return sharedKeys;
+ }
+
+}
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
new file mode 100644
index 0000000..ed786bc
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesResponse.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 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.core;
+
+import lombok.ToString;
+import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+@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) {
+ return new BasicGetAttributesResponse(requestMsgType, requestId, true, null, code);
+ }
+
+ public static BasicGetAttributesResponse onError(MsgType 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);
+ }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicRequest.java
new file mode 100644
index 0000000..26401d1
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicRequest.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 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.core;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class BasicRequest implements Serializable {
+
+ public static final Integer DEFAULT_REQUEST_ID = 0;
+
+ private final Integer requestId;
+
+ public BasicRequest(Integer requestId) {
+ this.requestId = requestId;
+ }
+
+ public Integer getRequestId() {
+ return requestId;
+ }
+}
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
new file mode 100644
index 0000000..2492192
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicResponseMsg.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright © 2016 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.core;
+
+import java.io.Serializable;
+import java.util.Optional;
+
+import org.thingsboard.server.common.msg.session.MsgType;
+
+
+public class BasicResponseMsg<T extends Serializable> implements ResponseMsg<T> {
+
+ private static final long serialVersionUID = 1L;
+
+ private final MsgType requestMsgType;
+ private final Integer requestId;
+ private final MsgType msgType;
+ 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) {
+ super();
+ this.requestMsgType = requestMsgType;
+ this.requestId = requestId;
+ this.msgType = msgType;
+ this.success = success;
+ this.error = error;
+ this.data = data;
+ }
+
+ @Override
+ public MsgType getRequestMsgType() {
+ return requestMsgType;
+ }
+
+ @Override
+ public Integer getRequestId() {
+ return requestId;
+ }
+
+ @Override
+ public boolean isSuccess() {
+ return success;
+ }
+
+ @Override
+ public Optional<Exception> getError() {
+ return Optional.ofNullable(error);
+ }
+
+ @Override
+ public Optional<T> getData() {
+ return Optional.ofNullable(data);
+ }
+
+ @Override
+ public String toString() {
+ return "BasicResponseMsg [success=" + success + ", data=" + data + ", error=" + error + "]";
+ }
+
+ @Override
+ public MsgType getMsgType() {
+ return msgType;
+ }
+}
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
new file mode 100644
index 0000000..1c2bf41
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicStatusCodeResponse.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016 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.core;
+
+import lombok.ToString;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+@ToString
+public class BasicStatusCodeResponse extends BasicResponseMsg<Integer> implements StatusCodeResponse {
+
+ private static final long serialVersionUID = 1L;
+
+ public static BasicStatusCodeResponse onSuccess(MsgType requestMsgType, Integer requestId) {
+ return BasicStatusCodeResponse.onSuccess(requestMsgType, requestId, 0);
+ }
+
+ public static BasicStatusCodeResponse onSuccess(MsgType requestMsgType, Integer requestId, Integer code) {
+ return new BasicStatusCodeResponse(requestMsgType, requestId, true, null, code);
+ }
+
+ public static BasicStatusCodeResponse onError(MsgType 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);
+ }
+}
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
new file mode 100644
index 0000000..d984e7f
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicTelemetryUploadRequest.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright © 2016 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.core;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+public class BasicTelemetryUploadRequest extends BasicRequest implements TelemetryUploadRequest {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Map<Long, List<KvEntry>> data;
+
+ public BasicTelemetryUploadRequest() {
+ this(DEFAULT_REQUEST_ID);
+ }
+
+ public BasicTelemetryUploadRequest(Integer requestId) {
+ super(requestId);
+ this.data = new HashMap<>();
+ }
+
+ public void add(long ts, KvEntry entry) {
+ List<KvEntry> tsEntries = data.get(ts);
+ if (tsEntries == null) {
+ tsEntries = new ArrayList<>();
+ data.put(ts, tsEntries);
+ }
+ tsEntries.add(entry);
+ }
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.POST_TELEMETRY_REQUEST;
+ }
+
+ @Override
+ public Map<Long, List<KvEntry>> getData() {
+ return data;
+ }
+
+ @Override
+ public String toString() {
+ return "BasicTelemetryUploadRequest [data=" + data + "]";
+ }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicToDeviceSessionActorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicToDeviceSessionActorMsg.java
new file mode 100644
index 0000000..538959d
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicToDeviceSessionActorMsg.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright © 2016 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.core;
+
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+
+public class BasicToDeviceSessionActorMsg implements ToDeviceSessionActorMsg {
+
+ private final ToDeviceMsg msg;
+ private final SessionId sessionId;
+
+ public BasicToDeviceSessionActorMsg(ToDeviceMsg msg, SessionId sessionId) {
+ super();
+ this.msg = msg;
+ this.sessionId = sessionId;
+ }
+
+ @Override
+ public SessionId getSessionId() {
+ return sessionId;
+ }
+
+ @Override
+ public ToDeviceMsg getMsg() {
+ return msg;
+ }
+
+ @Override
+ public String toString() {
+ return "BasicToSessionResponseMsg [msg=" + msg + ", sessionId=" + sessionId + "]";
+ }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicUpdateAttributesRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicUpdateAttributesRequest.java
new file mode 100644
index 0000000..c968f04
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicUpdateAttributesRequest.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 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.core;
+
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+public class BasicUpdateAttributesRequest extends BasicRequest implements UpdateAttributesRequest {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Set<AttributeKvEntry> data;
+
+ public BasicUpdateAttributesRequest() {
+ this(DEFAULT_REQUEST_ID);
+ }
+
+ public BasicUpdateAttributesRequest(Integer requestId) {
+ super(requestId);
+ this.data = new LinkedHashSet<>();
+ }
+
+ public void add(AttributeKvEntry entry) {
+ this.data.add(entry);
+ }
+
+ public void add(Collection<AttributeKvEntry> entries) {
+ this.data.addAll(entries);
+ }
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.POST_ATTRIBUTES_REQUEST;
+ }
+
+ @Override
+ public Set<AttributeKvEntry> getAttributes() {
+ return data;
+ }
+
+ @Override
+ public String toString() {
+ return "BasicUpdateAttributesRequest [data=" + data + "]";
+ }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesRequest.java
new file mode 100644
index 0000000..49bca53
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesRequest.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 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.core;
+
+import java.util.Set;
+
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+
+public interface GetAttributesRequest extends FromDeviceRequestMsg {
+
+ Set<String> getClientAttributeNames();
+ Set<String> getSharedAttributeNames();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesResponse.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesResponse.java
new file mode 100644
index 0000000..ec3f074
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesResponse.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 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.core;
+
+import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
+
+public interface GetAttributesResponse extends ResponseMsg<AttributesKVMsg> {
+
+}
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
new file mode 100644
index 0000000..861eab8
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ResponseMsg.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016 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.core;
+
+import java.io.Serializable;
+import java.util.Optional;
+
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+
+public interface ResponseMsg<T extends Serializable> extends ToDeviceMsg {
+
+ MsgType getRequestMsgType();
+
+ Integer getRequestId();
+
+ Optional<Exception> getError();
+
+ Optional<T> getData();
+}
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
new file mode 100644
index 0000000..b9ea578
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcSubscribeMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.core;
+
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RpcSubscribeMsg implements FromDeviceMsg {
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.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
new file mode 100644
index 0000000..6934a95
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcUnsubscribeMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.core;
+
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RpcUnsubscribeMsg implements FromDeviceMsg {
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.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
new file mode 100644
index 0000000..1c235a0
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineError.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016 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.core;
+
+/**
+ * @author Andrew Shvayka
+ */
+
+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);
+
+ private final boolean critical;
+
+ RuleEngineError() {
+ this(false);
+ }
+
+ RuleEngineError(boolean critical) {
+ this.critical = critical;
+ }
+
+ public boolean isCritical() {
+ return critical;
+ }
+
+ public int getPriority() {
+ return ordinal();
+ }
+}
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
new file mode 100644
index 0000000..c9ea00a
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineErrorMsg.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright © 2016 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.core;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class RuleEngineErrorMsg implements ToDeviceMsg {
+
+ private final MsgType inMsgType;
+ private final RuleEngineError error;
+
+ @Override
+ public boolean isSuccess() {
+ return false;
+ }
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.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!";
+ 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
new file mode 100644
index 0000000..61bc094
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.core;
+
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class SessionCloseMsg implements FromDeviceMsg {
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.SESSION_CLOSE;
+ }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/StatusCodeResponse.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/StatusCodeResponse.java
new file mode 100644
index 0000000..81daf89
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/StatusCodeResponse.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright © 2016 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.core;
+
+public interface StatusCodeResponse extends ResponseMsg<Integer>{
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/TelemetryUploadRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/TelemetryUploadRequest.java
new file mode 100644
index 0000000..0682d3b
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/TelemetryUploadRequest.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.core;
+
+import java.util.List;
+import java.util.Map;
+
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+
+public interface TelemetryUploadRequest extends FromDeviceRequestMsg {
+
+ Map<Long, List<KvEntry>> getData();
+
+}
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
new file mode 100644
index 0000000..c376ea0
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcRequestMsg.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016 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.core;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class ToDeviceRpcRequestMsg implements ToDeviceMsg {
+
+ private final int requestId;
+ private final String method;
+ private final String params;
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.TO_DEVICE_RPC_REQUEST;
+ }
+
+ @Override
+ public boolean isSuccess() {
+ return true;
+ }
+}
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
new file mode 100644
index 0000000..5ee764a
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcResponseMsg.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 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.core;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class ToDeviceRpcResponseMsg implements FromDeviceMsg {
+
+ private final int requestId;
+ private final String data;
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.TO_DEVICE_RPC_RESPONSE;
+ }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceSessionActorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceSessionActorMsg.java
new file mode 100644
index 0000000..97ab6d9
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceSessionActorMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.core;
+
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ToDeviceSessionActorMsg extends SessionAwareMsg, Serializable {
+
+ ToDeviceMsg getMsg();
+}
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
new file mode 100644
index 0000000..a684681
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 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.core;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class ToServerRpcRequestMsg implements FromDeviceMsg {
+
+ private final int requestId;
+ private final String method;
+ private final String params;
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.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
new file mode 100644
index 0000000..ccce825
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcResponseMsg.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016 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.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.ToDeviceMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class ToServerRpcResponseMsg implements ToDeviceMsg {
+
+ private final int requestId;
+ private final String data;
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.TO_SERVER_RPC_RESPONSE;
+ }
+
+ @Override
+ public boolean isSuccess() {
+ return true;
+ }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/UpdateAttributesRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/UpdateAttributesRequest.java
new file mode 100644
index 0000000..4f5c936
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/UpdateAttributesRequest.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 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.core;
+
+import java.util.Set;
+
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+
+public interface UpdateAttributesRequest extends FromDeviceRequestMsg {
+
+ Set<AttributeKvEntry> getAttributes();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/device/BasicToDeviceActorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/device/BasicToDeviceActorMsg.java
new file mode 100644
index 0000000..c559111
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/device/BasicToDeviceActorMsg.java
@@ -0,0 +1,101 @@
+/**
+ * Copyright © 2016 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.device;
+
+import lombok.ToString;
+import org.thingsboard.server.common.data.id.CustomerId;
+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.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ToDeviceActorSessionMsg;
+
+import java.util.Optional;
+
+@ToString
+public class BasicToDeviceActorMsg implements ToDeviceActorMsg {
+
+ private static final long serialVersionUID = -1866795134993115408L;
+
+ private final TenantId tenantId;
+ private final CustomerId customerId;
+ private final DeviceId deviceId;
+ private final SessionId sessionId;
+ private final SessionType sessionType;
+ private final ServerAddress serverAddress;
+ private final FromDeviceMsg msg;
+
+ public BasicToDeviceActorMsg(ToDeviceActorMsg other, FromDeviceMsg msg) {
+ this(null, other.getTenantId(), other.getCustomerId(), other.getDeviceId(), other.getSessionId(), other.getSessionType(), msg);
+ }
+
+ public BasicToDeviceActorMsg(ToDeviceActorSessionMsg msg, SessionType sessionType) {
+ this(null, msg.getTenantId(), msg.getCustomerId(), msg.getDeviceId(), msg.getSessionId(), sessionType, msg.getSessionMsg().getMsg());
+ }
+
+ private BasicToDeviceActorMsg(ServerAddress serverAddress, TenantId tenantId, CustomerId customerId, DeviceId deviceId, SessionId sessionId, SessionType sessionType,
+ FromDeviceMsg msg) {
+ super();
+ this.serverAddress = serverAddress;
+ this.tenantId = tenantId;
+ this.customerId = customerId;
+ this.deviceId = deviceId;
+ this.sessionId = sessionId;
+ this.sessionType = sessionType;
+ this.msg = msg;
+ }
+
+ @Override
+ public DeviceId getDeviceId() {
+ return deviceId;
+ }
+
+ @Override
+ public CustomerId getCustomerId() {
+ return customerId;
+ }
+
+ public TenantId getTenantId() {
+ return tenantId;
+ }
+
+ @Override
+ public SessionId getSessionId() {
+ return sessionId;
+ }
+
+ @Override
+ public SessionType getSessionType() {
+ return sessionType;
+ }
+
+ @Override
+ public Optional<ServerAddress> getServerAddress() {
+ return Optional.ofNullable(serverAddress);
+ }
+
+ @Override
+ public FromDeviceMsg getPayload() {
+ return msg;
+ }
+
+ @Override
+ public ToDeviceActorMsg toOtherAddress(ServerAddress otherAddress) {
+ return new BasicToDeviceActorMsg(otherAddress, tenantId, customerId, deviceId, sessionId, sessionType, msg);
+ }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/device/ToDeviceActorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/device/ToDeviceActorMsg.java
new file mode 100644
index 0000000..c02ac03
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/device/ToDeviceActorMsg.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016 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.device;
+
+import java.io.Serializable;
+import java.util.Optional;
+
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.aware.CustomerAwareMsg;
+import org.thingsboard.server.common.msg.aware.DeviceAwareMsg;
+import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.SessionType;
+
+public interface ToDeviceActorMsg extends DeviceAwareMsg, CustomerAwareMsg, TenantAwareMsg, Serializable {
+
+ SessionId getSessionId();
+
+ SessionType getSessionType();
+
+ Optional<ServerAddress> getServerAddress();
+
+ FromDeviceMsg getPayload();
+
+ ToDeviceActorMsg toOtherAddress(ServerAddress otherAddress);
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/kv/AttributesKVMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/kv/AttributesKVMsg.java
new file mode 100644
index 0000000..73b8f00
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/kv/AttributesKVMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.kv;
+
+import java.io.Serializable;
+import java.util.List;
+
+import org.thingsboard.server.common.data.kv.AttributeKey;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+
+public interface AttributesKVMsg extends Serializable {
+
+ List<AttributeKvEntry> getClientAttributes();
+ List<AttributeKvEntry> getSharedAttributes();
+ List<AttributeKey> getDeletedAttributes();
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/kv/BasicAttributeKVMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/kv/BasicAttributeKVMsg.java
new file mode 100644
index 0000000..32cdcaf
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/kv/BasicAttributeKVMsg.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright © 2016 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.kv;
+
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import org.thingsboard.server.common.data.kv.AttributeKey;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+
+import java.util.Collections;
+import java.util.List;
+
+@Data
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+public class BasicAttributeKVMsg implements AttributesKVMsg {
+
+ private static final long serialVersionUID = 1L;
+
+ private final List<AttributeKvEntry> clientAttributes;
+ private final List<AttributeKvEntry> sharedAttributes;
+ private final List<AttributeKey> deletedAttributes;
+
+ public static BasicAttributeKVMsg fromClient(List<AttributeKvEntry> attributes) {
+ return new BasicAttributeKVMsg(attributes, Collections.emptyList(), Collections.emptyList());
+ }
+
+ public static BasicAttributeKVMsg fromShared(List<AttributeKvEntry> attributes) {
+ return new BasicAttributeKVMsg(Collections.emptyList(), attributes, Collections.emptyList());
+ }
+
+ public static BasicAttributeKVMsg from(List<AttributeKvEntry> client, List<AttributeKvEntry> shared) {
+ return new BasicAttributeKVMsg(client, shared, Collections.emptyList());
+ }
+
+ public static AttributesKVMsg fromDeleted(List<AttributeKey> shared) {
+ return new BasicAttributeKVMsg(Collections.emptyList(), Collections.emptyList(), shared);
+ }
+}
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
new file mode 100644
index 0000000..fb56893
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright © 2016 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.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.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
+import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+@ToString
+public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg {
+ @Getter
+ private final TenantId tenantId;
+ private final PluginId pluginId;
+ private final RuleId ruleId;
+ @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) {
+ this.tenantId = tenantId;
+ this.pluginId = pluginId;
+ this.ruleId = ruleId;
+ this.event = event;
+ }
+
+ public Optional<PluginId> getPluginId() {
+ return Optional.ofNullable(pluginId);
+ }
+
+ public Optional<RuleId> getRuleId() {
+ return Optional.ofNullable(ruleId);
+ }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/RuleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/RuleMsg.java
new file mode 100644
index 0000000..1a93c25
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/RuleMsg.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016 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 org.thingsboard.server.common.data.rule.Scope;
+import org.thingsboard.server.common.data.rule.RuleType;
+import org.thingsboard.server.common.msg.aware.RuleAwareMsg;
+
+/**
+ * Message that is used to deliver some data to the rule instance.
+ * For example: aggregated statistics or command decoded from http request.
+ *
+ * @author ashvayka
+ *
+ * @param <V> - payload
+ */
+public interface RuleMsg<V> extends RuleAwareMsg {
+
+ Scope getRuleLevel();
+
+ RuleType getRuleType();
+
+ V getPayload();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/AdaptorToSessionActorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/AdaptorToSessionActorMsg.java
new file mode 100644
index 0000000..a005f1c
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/AdaptorToSessionActorMsg.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 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.session;
+
+public interface AdaptorToSessionActorMsg extends SessionMsg {
+
+ FromDeviceMsg getMsg();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicAdaptorToSessionActorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicAdaptorToSessionActorMsg.java
new file mode 100644
index 0000000..11caf25
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicAdaptorToSessionActorMsg.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 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.session;
+
+public class BasicAdaptorToSessionActorMsg extends BasicSessionMsg implements AdaptorToSessionActorMsg {
+
+ private final FromDeviceMsg msg;
+
+ public BasicAdaptorToSessionActorMsg(SessionContext ctx, FromDeviceMsg msg) {
+ super(ctx);
+ this.msg = msg;
+ }
+
+ @Override
+ public FromDeviceMsg getMsg() {
+ return msg;
+ }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicSessionActorToAdaptorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicSessionActorToAdaptorMsg.java
new file mode 100644
index 0000000..3b7d9fd
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicSessionActorToAdaptorMsg.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016 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.session;
+
+import java.util.Optional;
+
+public class BasicSessionActorToAdaptorMsg extends BasicSessionMsg implements SessionActorToAdaptorMsg {
+
+ private final ToDeviceMsg msg;
+
+ public BasicSessionActorToAdaptorMsg(SessionContext ctx, ToDeviceMsg msg) {
+ super(ctx);
+ this.msg = msg;
+ }
+
+ @Override
+ public ToDeviceMsg getMsg() {
+ return msg;
+ }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicSessionMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicSessionMsg.java
new file mode 100644
index 0000000..7bcc55e
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicSessionMsg.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016 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.session;
+
+import org.thingsboard.server.common.data.id.SessionId;
+
+public class BasicSessionMsg implements SessionMsg {
+
+ private final SessionContext ctx;
+
+ public BasicSessionMsg(SessionContext ctx) {
+ super();
+ this.ctx = ctx;
+ }
+
+ @Override
+ public SessionId getSessionId() {
+ return ctx.getSessionId();
+ }
+
+ @Override
+ public SessionContext getSessionContext() {
+ return ctx;
+ }
+
+ @Override
+ public String toString() {
+ return "BasicSessionMsg [ctx=" + ctx + "]";
+ }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicToDeviceActorSessionMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicToDeviceActorSessionMsg.java
new file mode 100644
index 0000000..ced4843
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicToDeviceActorSessionMsg.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright © 2016 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.session;
+
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+public class BasicToDeviceActorSessionMsg implements ToDeviceActorSessionMsg {
+
+ private final TenantId tenantId;
+ private final CustomerId customerId;
+ private final DeviceId deviceId;
+ private final AdaptorToSessionActorMsg msg;
+
+ public BasicToDeviceActorSessionMsg(Device device, AdaptorToSessionActorMsg msg) {
+ super();
+ this.tenantId = device.getTenantId();
+ this.customerId = device.getCustomerId();
+ this.deviceId = device.getId();
+ this.msg = msg;
+ }
+
+ public BasicToDeviceActorSessionMsg(ToDeviceActorSessionMsg deviceMsg) {
+ this.tenantId = deviceMsg.getTenantId();
+ this.customerId = deviceMsg.getCustomerId();
+ this.deviceId = deviceMsg.getDeviceId();
+ this.msg = deviceMsg.getSessionMsg();
+ }
+
+ @Override
+ public DeviceId getDeviceId() {
+ return deviceId;
+ }
+
+ @Override
+ public CustomerId getCustomerId() {
+ return customerId;
+ }
+
+ public TenantId getTenantId() {
+ return tenantId;
+ }
+
+ @Override
+ public SessionId getSessionId() {
+ return msg.getSessionId();
+ }
+
+ @Override
+ public AdaptorToSessionActorMsg getSessionMsg() {
+ return msg;
+ }
+
+ @Override
+ public String toString() {
+ return "BasicToDeviceActorSessionMsg [tenantId=" + tenantId + ", customerId=" + customerId + ", deviceId=" + deviceId + ", msg=" + 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
new file mode 100644
index 0000000..d188527
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016 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.session.ctrl;
+
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
+
+public class SessionCloseMsg implements SessionCtrlMsg {
+
+ private final SessionId sessionId;
+ private final boolean timeout;
+
+ public SessionCloseMsg(SessionId sessionId, boolean timeout) {
+ super();
+ this.sessionId = sessionId;
+ this.timeout = timeout;
+ }
+
+ @Override
+ public SessionId getSessionId() {
+ return sessionId;
+ }
+
+ public boolean isTimeout() {
+ return timeout;
+ }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/ProcessingTimeoutException.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/ProcessingTimeoutException.java
new file mode 100644
index 0000000..7323a2d
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/ProcessingTimeoutException.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 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.session.ex;
+
+public class ProcessingTimeoutException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/SessionAuthException.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/SessionAuthException.java
new file mode 100644
index 0000000..9d22cca
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/SessionAuthException.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 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.session.ex;
+
+public class SessionAuthException extends SessionException {
+
+ private static final long serialVersionUID = 1L;
+
+ public SessionAuthException(String msg) {
+ super(msg);
+ }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/SessionException.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/SessionException.java
new file mode 100644
index 0000000..444c364
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/SessionException.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 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.session.ex;
+
+public class SessionException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public SessionException(String msg){
+ super(msg);
+ }
+
+ public SessionException(Exception cause){
+ super(cause);
+ }
+
+ public SessionException(String msg, Exception cause){
+ super(msg, cause);
+ }
+
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java
new file mode 100644
index 0000000..76da405
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright © 2016 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.session;
+
+public enum FeatureType {
+ ATTRIBUTES, TELEMETRY, RPC
+}
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
new file mode 100644
index 0000000..b717fec
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 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.session;
+
+import java.io.Serializable;
+
+public interface FromDeviceMsg extends Serializable {
+
+ MsgType getMsgType();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceRequestMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceRequestMsg.java
new file mode 100644
index 0000000..7709462
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceRequestMsg.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 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.session;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface FromDeviceRequestMsg extends FromDeviceMsg {
+
+ Integer getRequestId();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java
new file mode 100644
index 0000000..1b91425
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016 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.session;
+
+public enum MsgType {
+ GET_ATTRIBUTES_REQUEST(true), POST_ATTRIBUTES_REQUEST(true), GET_ATTRIBUTES_RESPONSE,
+ SUBSCRIBE_ATTRIBUTES_REQUEST, UNSUBSCRIBE_ATTRIBUTES_REQUEST, ATTRIBUTES_UPDATE_NOTIFICATION,
+
+ POST_TELEMETRY_REQUEST(true), STATUS_CODE_RESPONSE,
+
+ SUBSCRIBE_RPC_COMMANDS_REQUEST, UNSUBSCRIBE_RPC_COMMANDS_REQUEST,
+ TO_DEVICE_RPC_REQUEST, TO_DEVICE_RPC_RESPONSE, TO_DEVICE_RPC_RESPONSE_ACK,
+
+ TO_SERVER_RPC_REQUEST(true), TO_SERVER_RPC_RESPONSE,
+
+ RULE_ENGINE_ERROR,
+
+ SESSION_CLOSE;
+
+ private final boolean requiresRulesProcessing;
+
+ MsgType() {
+ this(false);
+ }
+
+ MsgType(boolean requiresRulesProcessing) {
+ this.requiresRulesProcessing = requiresRulesProcessing;
+ }
+
+ public boolean requiresRulesProcessing() {
+ return requiresRulesProcessing;
+ }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionActorToAdaptorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionActorToAdaptorMsg.java
new file mode 100644
index 0000000..f85545e
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionActorToAdaptorMsg.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 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.session;
+
+public interface SessionActorToAdaptorMsg extends SessionMsg {
+
+ ToDeviceMsg getMsg();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionContext.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionContext.java
new file mode 100644
index 0000000..d437dda
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionContext.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 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.session;
+
+import org.thingsboard.server.common.data.security.DeviceCredentialsFilter;
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
+import org.thingsboard.server.common.msg.session.ex.SessionException;
+
+public interface SessionContext extends SessionAwareMsg {
+
+ SessionType getSessionType();
+
+ void onMsg(SessionActorToAdaptorMsg msg) throws SessionException;
+
+ void onMsg(SessionCtrlMsg msg) throws SessionException;
+
+ void onError(SessionException e);
+
+ boolean isClosed();
+
+ long getTimeout();
+
+}
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
new file mode 100644
index 0000000..68c1d8d
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionCtrlMsg.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 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.session;
+
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
+
+public interface SessionCtrlMsg extends SessionAwareMsg {
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsg.java
new file mode 100644
index 0000000..f4a51c5
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 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.session;
+
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
+
+public interface SessionMsg extends SessionAwareMsg {
+
+ SessionContext getSessionContext();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionType.java
new file mode 100644
index 0000000..1e432f9
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionType.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 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.session;
+
+public enum SessionType {
+
+ SYNC, ASYNC;
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceActorSessionMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceActorSessionMsg.java
new file mode 100644
index 0000000..aa3650e
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceActorSessionMsg.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 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.session;
+
+import org.thingsboard.server.common.msg.aware.CustomerAwareMsg;
+import org.thingsboard.server.common.msg.aware.DeviceAwareMsg;
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
+import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
+
+public interface ToDeviceActorSessionMsg extends DeviceAwareMsg, CustomerAwareMsg, TenantAwareMsg, SessionAwareMsg {
+
+ AdaptorToSessionActorMsg getSessionMsg();
+
+}
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
new file mode 100644
index 0000000..8c87f60
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceMsg.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 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.session;
+
+import java.io.Serializable;
+
+public interface ToDeviceMsg extends Serializable {
+
+ boolean isSuccess();
+
+ MsgType getMsgType();
+
+}
common/pom.xml 43(+43 -0)
diff --git a/common/pom.xml b/common/pom.xml
new file mode 100644
index 0000000..97ff8ab
--- /dev/null
+++ b/common/pom.xml
@@ -0,0 +1,43 @@
+<!--
+
+ Copyright © 2016 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>0.0.1-SNAPSHOT</version>
+ <artifactId>server</artifactId>
+ </parent>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>common</artifactId>
+ <packaging>pom</packaging>
+
+ <name>Thingsboard Server Commons</name>
+ <url>http://thingsboard.org</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <main.dir>${basedir}/..</main.dir>
+ </properties>
+ <modules>
+ <module>data</module>
+ <module>message</module>
+ <module>transport</module>
+ </modules>
+
+</project>
common/transport/pom.xml 79(+79 -0)
diff --git a/common/transport/pom.xml b/common/transport/pom.xml
new file mode 100644
index 0000000..e57d3d7
--- /dev/null
+++ b/common/transport/pom.xml
@@ -0,0 +1,79 @@
+<!--
+
+ Copyright © 2016 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.server</groupId>
+ <version>0.0.1-SNAPSHOT</version>
+ <artifactId>common</artifactId>
+ </parent>
+ <groupId>org.thingsboard.server.common</groupId>
+ <artifactId>transport</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Thingsboard Server Common Transport components</name>
+ <url>http://thingsboard.org</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <main.dir>${basedir}/../..</main.dir>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.thingsboard.server.common</groupId>
+ <artifactId>data</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server.common</groupId>
+ <artifactId>message</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>log4j-over-slf4j</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java
new file mode 100644
index 0000000..2c826ec
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016 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.adaptor;
+
+public class AdaptorException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public AdaptorException() {
+ super();
+ }
+
+ public AdaptorException(String cause) {
+ super(cause);
+ }
+
+ public AdaptorException(Exception cause) {
+ super(cause);
+ }
+
+}
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
new file mode 100644
index 0000000..27c7052
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java
@@ -0,0 +1,199 @@
+/**
+ * Copyright © 2016 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.adaptor;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import com.google.gson.*;
+import org.thingsboard.server.common.msg.core.*;
+
+import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
+
+public class JsonConverter {
+
+ private static final Gson GSON = new Gson();
+
+ public static TelemetryUploadRequest convertToTelemetry(JsonElement jsonObject) throws JsonSyntaxException {
+ return convertToTelemetry(jsonObject, BasicRequest.DEFAULT_REQUEST_ID);
+ }
+
+ public static TelemetryUploadRequest convertToTelemetry(JsonElement jsonObject, int requestId) throws JsonSyntaxException {
+ BasicTelemetryUploadRequest request = new BasicTelemetryUploadRequest(requestId);
+ long systemTs = System.currentTimeMillis();
+ if (jsonObject.isJsonObject()) {
+ parseObject(request, systemTs, jsonObject);
+ } else if (jsonObject.isJsonArray()) {
+ jsonObject.getAsJsonArray().forEach(je -> {
+ if (je.isJsonObject()) {
+ parseObject(request, systemTs, je.getAsJsonObject());
+ } else {
+ throw new JsonSyntaxException("Can't parse value: " + je);
+ }
+ });
+ } else {
+ throw new JsonSyntaxException("Can't parse value: " + jsonObject);
+ }
+ return request;
+ }
+
+ public static ToServerRpcRequestMsg convertToServerRpcRequest(JsonElement json, int requestId) throws JsonSyntaxException {
+ JsonObject object = json.getAsJsonObject();
+ return new ToServerRpcRequestMsg(requestId, object.get("method").getAsString(), GSON.toJson(object.get("params")));
+ }
+
+ private static void parseObject(BasicTelemetryUploadRequest request, long systemTs, JsonElement jsonObject) {
+ JsonObject jo = jsonObject.getAsJsonObject();
+ if (jo.has("ts") && jo.has("values")) {
+ parseWithTs(request, jo);
+ } else {
+ parseWithoutTs(request, systemTs, jo);
+ }
+ }
+
+ private static void parseWithoutTs(BasicTelemetryUploadRequest request, long systemTs, JsonObject jo) {
+ for (KvEntry entry : parseValues(jo)) {
+ request.add(systemTs, entry);
+ }
+ }
+
+ private static void parseWithTs(BasicTelemetryUploadRequest request, JsonObject jo) {
+ long ts = jo.get("ts").getAsLong();
+ JsonObject valuesObject = jo.get("values").getAsJsonObject();
+ for (KvEntry entry : parseValues(valuesObject)) {
+ request.add(ts, entry);
+ }
+ }
+
+ private static List<KvEntry> parseValues(JsonObject valuesObject) {
+ List<KvEntry> result = new ArrayList<>();
+ for (Entry<String, JsonElement> valueEntry : valuesObject.entrySet()) {
+ JsonElement element = valueEntry.getValue();
+ if (element.isJsonPrimitive()) {
+ JsonPrimitive value = element.getAsJsonPrimitive();
+ if (value.isString()) {
+ result.add(new StringDataEntry(valueEntry.getKey(), value.getAsString()));
+ } else if (value.isBoolean()) {
+ result.add(new BooleanDataEntry(valueEntry.getKey(), value.getAsBoolean()));
+ } else if (value.isNumber()) {
+ if (value.getAsString().contains(".")) {
+ result.add(new DoubleDataEntry(valueEntry.getKey(), value.getAsDouble()));
+ } else {
+ result.add(new LongDataEntry(valueEntry.getKey(), value.getAsLong()));
+ }
+ } else {
+ throw new JsonSyntaxException("Can't parse value: " + value);
+ }
+ } else {
+ throw new JsonSyntaxException("Can't parse value: " + element);
+ }
+ }
+ return result;
+ }
+
+ public static UpdateAttributesRequest convertToAttributes(JsonElement element) {
+ return convertToAttributes(element, BasicRequest.DEFAULT_REQUEST_ID);
+ }
+
+ public static UpdateAttributesRequest convertToAttributes(JsonElement element, int requestId) {
+ if (element.isJsonObject()) {
+ BasicUpdateAttributesRequest request = new BasicUpdateAttributesRequest(requestId);
+ long ts = System.currentTimeMillis();
+ request.add(parseValues(element.getAsJsonObject()).stream().map(kv -> new BaseAttributeKvEntry(kv, ts)).collect(Collectors.toList()));
+ return request;
+ } else {
+ throw new JsonSyntaxException("Can't parse value: " + element);
+ }
+ }
+
+ public static JsonObject toJson(AttributesKVMsg payload, boolean asMap) {
+ JsonObject result = new JsonObject();
+ if (asMap) {
+ if (!payload.getClientAttributes().isEmpty()) {
+ JsonObject attrObject = new JsonObject();
+ payload.getClientAttributes().forEach(addToObject(attrObject));
+ result.add("client", attrObject);
+ }
+ if (!payload.getSharedAttributes().isEmpty()) {
+ JsonObject attrObject = new JsonObject();
+ payload.getSharedAttributes().forEach(addToObject(attrObject));
+ result.add("shared", attrObject);
+ }
+ } else {
+ payload.getClientAttributes().forEach(addToObject(result));
+ payload.getSharedAttributes().forEach(addToObject(result));
+ }
+ if (!payload.getDeletedAttributes().isEmpty()) {
+ JsonArray attrObject = new JsonArray();
+ payload.getDeletedAttributes().forEach(addToObject(attrObject));
+ result.add("deleted", attrObject);
+ }
+ return result;
+ }
+
+ private static Consumer<AttributeKey> addToObject(JsonArray result) {
+ return key -> {
+ result.add(key.getAttributeKey());
+ };
+ }
+
+ private static Consumer<AttributeKvEntry> addToObject(JsonObject result) {
+ return de -> {
+ JsonPrimitive value;
+ switch (de.getDataType()) {
+ case BOOLEAN:
+ value = new JsonPrimitive(de.getBooleanValue().get());
+ break;
+ case DOUBLE:
+ value = new JsonPrimitive(de.getDoubleValue().get());
+ break;
+ case LONG:
+ value = new JsonPrimitive(de.getLongValue().get());
+ break;
+ case STRING:
+ value = new JsonPrimitive(de.getStrValue().get());
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported data type: " + de.getDataType());
+ }
+ result.add(de.getKey(), value);
+ };
+ }
+
+ public static JsonObject toJson(ToDeviceRpcRequestMsg msg, boolean includeRequestId) {
+ JsonObject result = new JsonObject();
+ if (includeRequestId) {
+ result.addProperty("id", msg.getRequestId());
+ }
+ result.addProperty("method", msg.getMethod());
+ result.add("params", new JsonParser().parse(msg.getParams()));
+ return result;
+ }
+
+ public static JsonElement toJson(ToServerRpcResponseMsg msg) {
+ return new JsonParser().parse(msg.getData());
+ }
+
+ public static JsonElement toErrorJson(String errorMsg) {
+ JsonObject error = new JsonObject();
+ error.addProperty("error", errorMsg);
+ return error;
+ }
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/auth/DeviceAuthResult.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/auth/DeviceAuthResult.java
new file mode 100644
index 0000000..40d4be9
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/auth/DeviceAuthResult.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright © 2016 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.auth;
+
+import org.thingsboard.server.common.data.id.DeviceId;
+
+public class DeviceAuthResult {
+
+ private final boolean success;
+ private final DeviceId deviceId;
+ private final String errorMsg;
+
+ public static DeviceAuthResult of(DeviceId deviceId) {
+ return new DeviceAuthResult(true, deviceId, null);
+ }
+
+ public static DeviceAuthResult of(String errorMsg) {
+ return new DeviceAuthResult(false, null, errorMsg);
+ }
+
+ private DeviceAuthResult(boolean success, DeviceId deviceId, String errorMsg) {
+ super();
+ this.success = success;
+ this.deviceId = deviceId;
+ this.errorMsg = errorMsg;
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public DeviceId getDeviceId() {
+ return deviceId;
+ }
+
+ public String getErrorMsg() {
+ return errorMsg;
+ }
+
+ @Override
+ public String toString() {
+ return "DeviceAuthResult [success=" + success + ", deviceId=" + deviceId + ", errorMsg=" + errorMsg + "]";
+ }
+
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/auth/DeviceAuthService.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/auth/DeviceAuthService.java
new file mode 100644
index 0000000..67cdb51
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/auth/DeviceAuthService.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 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.auth;
+
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.security.DeviceCredentialsFilter;
+
+import java.util.Optional;
+
+public interface DeviceAuthService {
+
+ DeviceAuthResult process(DeviceCredentialsFilter credentials);
+
+ Optional<Device> findDeviceById(DeviceId deviceId);
+
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java
new file mode 100644
index 0000000..b762ed9
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright © 2016 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.session;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.security.DeviceCredentialsFilter;
+import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
+import org.thingsboard.server.common.msg.session.SessionContext;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.common.transport.auth.DeviceAuthResult;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public abstract class DeviceAwareSessionContext implements SessionContext {
+
+ protected final DeviceAuthService authService;
+ protected final SessionMsgProcessor processor;
+
+ protected volatile Device device;
+
+ public DeviceAwareSessionContext(SessionMsgProcessor processor, DeviceAuthService authService) {
+ this.processor = processor;
+ this.authService = authService;
+ }
+
+ public boolean login(DeviceCredentialsFilter credentials) {
+ DeviceAuthResult result = authService.process(credentials);
+ if (result.isSuccess()) {
+ Optional<Device> deviceOpt = authService.findDeviceById(result.getDeviceId());
+ if (deviceOpt.isPresent()) {
+ device = deviceOpt.get();
+ }
+ return true;
+ } else {
+ log.debug("Can't find device using credentials [{}] due to {}", credentials, result.getErrorMsg());
+ return false;
+ }
+ }
+
+ public Device getDevice() {
+ return device;
+ }
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/SessionMsgProcessor.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/SessionMsgProcessor.java
new file mode 100644
index 0000000..a9760aa
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/SessionMsgProcessor.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 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;
+
+import org.thingsboard.server.common.data.security.DeviceCredentialsFilter;
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
+
+public interface SessionMsgProcessor {
+
+ void process(SessionAwareMsg msg);
+
+}
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
new file mode 100644
index 0000000..c2b1fe0
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/TransportAdaptor.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 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;
+
+import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionActorToAdaptorMsg;
+import org.thingsboard.server.common.msg.session.SessionContext;
+import org.thingsboard.server.common.transport.adaptor.AdaptorException;
+
+import java.util.Optional;
+
+public interface TransportAdaptor<C extends SessionContext, T, V> {
+
+ AdaptorToSessionActorMsg convertToActorMsg(C ctx, MsgType type, T inbound) throws AdaptorException;
+
+ Optional<V> convertToAdaptorMsg(C ctx, SessionActorToAdaptorMsg msg) throws AdaptorException;
+
+}
extensions-api/pom.xml 95(+95 -0)
diff --git a/extensions-api/pom.xml b/extensions-api/pom.xml
new file mode 100644
index 0000000..4af3de3
--- /dev/null
+++ b/extensions-api/pom.xml
@@ -0,0 +1,95 @@
+<!--
+
+ Copyright © 2016 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>0.0.1-SNAPSHOT</version>
+ <artifactId>server</artifactId>
+ </parent>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>extensions-api</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Thingsboard Server Extensions API</name>
+ <url>http://thingsboard.org</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <main.dir>${basedir}/..</main.dir>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.thingsboard.server.common</groupId>
+ <artifactId>data</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server.common</groupId>
+ <artifactId>message</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.protobuf</groupId>
+ <artifactId>protobuf-java</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>log4j-over-slf4j</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+
+</project>
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Action.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Action.java
new file mode 100644
index 0000000..6dd4327
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Action.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016 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.extensions.api.component;
+
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Action {
+
+ String name();
+
+ ComponentScope scope() default ComponentScope.TENANT;
+
+ String descriptor() default "EmptyJsonDescriptor.json";
+
+ Class<?> configuration() default EmptyComponentConfiguration.class;
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/ConfigurableComponent.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/ConfigurableComponent.java
new file mode 100644
index 0000000..79f728f
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/ConfigurableComponent.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 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.extensions.api.component;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ConfigurableComponent<T> {
+
+ void init(T configuration);
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/EmptyComponentConfiguration.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/EmptyComponentConfiguration.java
new file mode 100644
index 0000000..f99272e
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/EmptyComponentConfiguration.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 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.extensions.api.component;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class EmptyComponentConfiguration {
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Filter.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Filter.java
new file mode 100644
index 0000000..6c20ae9
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Filter.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016 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.extensions.api.component;
+
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Filter {
+
+ String name();
+
+ ComponentScope scope() default ComponentScope.TENANT;
+
+ String descriptor() default "EmptyJsonDescriptor.json";
+
+ Class<?> configuration() default EmptyComponentConfiguration.class;
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Plugin.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Plugin.java
new file mode 100644
index 0000000..5c4c632
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Plugin.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016 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.extensions.api.component;
+
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Plugin {
+
+ String name();
+
+ ComponentScope scope() default ComponentScope.TENANT;
+
+ String descriptor() default "EmptyJsonDescriptor.json";
+
+ Class<?> configuration() default EmptyComponentConfiguration.class;
+
+ Class<?>[] actions();
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Processor.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Processor.java
new file mode 100644
index 0000000..d6b1bce
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Processor.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016 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.extensions.api.component;
+
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Processor {
+
+ String name();
+
+ ComponentScope scope() default ComponentScope.TENANT;
+
+ String descriptor() default "EmptyJsonDescriptor.json";
+
+ Class<?> configuration() default EmptyComponentConfiguration.class;
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/Configurable.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/Configurable.java
new file mode 100644
index 0000000..242becc
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/Configurable.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 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.extensions.api.configuration;
+
+public interface Configurable<C extends Configuration> {
+
+ Class<C> getConfigurationClass();
+
+ void validate(C configuration) throws ConfigurationValidationException;
+
+ void configure(C configuration);
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/Configuration.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/Configuration.java
new file mode 100644
index 0000000..3c2ba30
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/Configuration.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 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.extensions.api.configuration;
+
+import java.io.Serializable;
+
+public interface Configuration extends Serializable {
+
+ // TODO: Figure out how to define and visualize configurations
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/ConfigurationValidationException.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/ConfigurationValidationException.java
new file mode 100644
index 0000000..416668c
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/ConfigurationValidationException.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 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.extensions.api.configuration;
+
+public class ConfigurationValidationException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public ConfigurationValidationException(String msg, Exception cause){
+ super(msg, cause);
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributes.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributes.java
new file mode 100644
index 0000000..4f2d760
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributes.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright © 2016 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.extensions.api.device;
+
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+
+import java.util.*;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class DeviceAttributes {
+
+ private final Map<String, AttributeKvEntry> clientSideAttributesMap;
+ private final Map<String, AttributeKvEntry> serverPrivateAttributesMap;
+ private final Map<String, AttributeKvEntry> serverPublicAttributesMap;
+
+ public DeviceAttributes(List<AttributeKvEntry> clientSideAttributes, List<AttributeKvEntry> serverPrivateAttributes, List<AttributeKvEntry> serverPublicAttributes) {
+ this.clientSideAttributesMap = mapAttributes(clientSideAttributes);
+ this.serverPrivateAttributesMap = mapAttributes(serverPrivateAttributes);
+ this.serverPublicAttributesMap = mapAttributes(serverPublicAttributes);
+ }
+
+ private static Map<String, AttributeKvEntry> mapAttributes(List<AttributeKvEntry> attributes) {
+ Map<String, AttributeKvEntry> result = new HashMap<>();
+ for (AttributeKvEntry attribute : attributes) {
+ result.put(attribute.getKey(), attribute);
+ }
+ return result;
+ }
+
+ public Collection<AttributeKvEntry> getClientSideAttributes() {
+ return clientSideAttributesMap.values();
+ }
+
+ public Collection<AttributeKvEntry> getServerSideAttributes() {
+ return serverPrivateAttributesMap.values();
+ }
+
+ public Collection<AttributeKvEntry> getServerSidePublicAttributes() {
+ return serverPublicAttributesMap.values();
+ }
+
+ public Optional<AttributeKvEntry> getClientSideAttribute(String attribute) {
+ return Optional.ofNullable(clientSideAttributesMap.get(attribute));
+ }
+
+ public Optional<AttributeKvEntry> getServerPrivateAttribute(String attribute) {
+ return Optional.ofNullable(serverPrivateAttributesMap.get(attribute));
+ }
+
+ public Optional<AttributeKvEntry> getServerPublicAttribute(String attribute) {
+ return Optional.ofNullable(serverPublicAttributesMap.get(attribute));
+ }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributesEventNotificationMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributesEventNotificationMsg.java
new file mode 100644
index 0000000..ebeb58f
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributesEventNotificationMsg.java
@@ -0,0 +1,51 @@
+/**
+ * Copyright © 2016 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.extensions.api.device;
+
+import lombok.Getter;
+import lombok.ToString;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.AttributeKey;
+
+import java.util.Set;
+
+/**
+ * @author Andrew Shvayka
+ */
+@ToString
+public class DeviceAttributesEventNotificationMsg implements ToDeviceActorNotificationMsg {
+
+ @Getter private final TenantId tenantId;
+ @Getter private final DeviceId deviceId;
+ @Getter private final Set<AttributeKey> keys;
+ @Getter private final boolean deleted;
+
+ public static DeviceAttributesEventNotificationMsg onUpdate(TenantId tenantId, DeviceId deviceId, Set<AttributeKey> keys) {
+ return new DeviceAttributesEventNotificationMsg(tenantId, deviceId, keys, false);
+ }
+
+ public static DeviceAttributesEventNotificationMsg onDelete(TenantId tenantId, DeviceId deviceId, Set<AttributeKey> keys) {
+ return new DeviceAttributesEventNotificationMsg(tenantId, deviceId, keys, true);
+ }
+
+ private DeviceAttributesEventNotificationMsg(TenantId tenantId, DeviceId deviceId, Set<AttributeKey> keys, boolean deleted) {
+ this.tenantId = tenantId;
+ this.deviceId = deviceId;
+ this.keys = keys;
+ this.deleted = deleted;
+ }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/ToDeviceActorNotificationMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/ToDeviceActorNotificationMsg.java
new file mode 100644
index 0000000..2236f14
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/ToDeviceActorNotificationMsg.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 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.extensions.api.device;
+
+import org.thingsboard.server.common.msg.aware.DeviceAwareMsg;
+import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ToDeviceActorNotificationMsg extends TenantAwareMsg, DeviceAwareMsg, Serializable {
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/AbstractPlugin.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/AbstractPlugin.java
new file mode 100644
index 0000000..c20b91d
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/AbstractPlugin.java
@@ -0,0 +1,88 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.api.plugins.handlers.*;
+import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+
+/**
+ * @author Andrew Shvayka
+ */
+public abstract class AbstractPlugin<T> implements Plugin<T> {
+
+ @Override
+ public void process(PluginContext ctx, PluginWebsocketMsg<?> wsMsg) {
+ getWebsocketMsgHandler().process(ctx, wsMsg);
+ }
+
+ @Override
+ public void process(PluginContext ctx, PluginRestMsg msg) {
+ getRestMsgHandler().process(ctx, msg);
+ }
+
+ @Override
+ public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
+ getRuleMsgHandler().process(ctx, tenantId, ruleId, msg);
+ }
+
+ @Override
+ public void process(PluginContext ctx, RpcMsg msg) {
+ getRpcMsgHandler().process(ctx, msg);
+ }
+
+ @Override
+ public void process(PluginContext ctx, FromDeviceRpcResponse msg) {
+ throw new IllegalStateException("Device RPC messages is not handled in current plugin!");
+ }
+
+ @Override
+ public void process(PluginContext ctx, TimeoutMsg<?> msg) {
+ throw new IllegalStateException("Timeouts is not handled in current plugin!");
+ }
+
+ @Override
+ public void onServerAdded(PluginContext ctx, ServerAddress server) {
+ }
+
+ @Override
+ public void onServerRemoved(PluginContext ctx, ServerAddress server) {
+ }
+
+ protected RuleMsgHandler getRuleMsgHandler() {
+ return new DefaultRuleMsgHandler();
+ }
+
+ protected RestMsgHandler getRestMsgHandler() {
+ return new DefaultRestMsgHandler();
+ }
+
+ protected WebsocketMsgHandler getWebsocketMsgHandler() {
+ return new DefaultWebsocketMsgHandler();
+ }
+
+ protected RpcMsgHandler getRpcMsgHandler() {
+ return new DefaultRpcMsgHandler();
+ }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRestMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRestMsgHandler.java
new file mode 100644
index 0000000..c9b27f7
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRestMsgHandler.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.handlers;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+
+import javax.servlet.ServletException;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class DefaultRestMsgHandler implements RestMsgHandler {
+
+ protected final ObjectMapper jsonMapper = new ObjectMapper();
+
+ @Override
+ public void process(PluginContext ctx, PluginRestMsg msg) {
+ try {
+ log.debug("[{}] Processing REST msg: {}", ctx.getPluginId(), msg);
+ HttpMethod method = msg.getRequest().getMethod();
+ switch (method) {
+ case GET:
+ handleHttpGetRequest(ctx, msg);
+ break;
+ case POST:
+ handleHttpPostRequest(ctx, msg);
+ break;
+ case DELETE:
+ handleHttpDeleteRequest(ctx, msg);
+ break;
+ default:
+ msg.getResponseHolder().setErrorResult(new HttpRequestMethodNotSupportedException(method.name()));
+ }
+ log.debug("[{}] Processed REST msg.", ctx.getPluginId());
+ } catch (Exception e) {
+ log.warn("[{}] Exception during REST msg processing: {}", ctx.getPluginId(), e.getMessage(), e);
+ msg.getResponseHolder().setErrorResult(e);
+ }
+ }
+
+ protected void handleHttpGetRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
+ msg.getResponseHolder().setErrorResult(new HttpRequestMethodNotSupportedException(HttpMethod.GET.name()));
+ }
+
+ protected void handleHttpPostRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
+ msg.getResponseHolder().setErrorResult(new HttpRequestMethodNotSupportedException(HttpMethod.POST.name()));
+ }
+
+ protected void handleHttpDeleteRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
+ msg.getResponseHolder().setErrorResult(new HttpRequestMethodNotSupportedException(HttpMethod.DELETE.name()));
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRpcMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRpcMsgHandler.java
new file mode 100644
index 0000000..c428cdb
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRpcMsgHandler.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.handlers;
+
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class DefaultRpcMsgHandler implements RpcMsgHandler {
+
+ @Override
+ public void process(PluginContext ctx, RpcMsg msg) {
+ throw new RuntimeException("Not registered msg type: " + msg.getMsgClazz() + "!");
+ }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRuleMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRuleMsgHandler.java
new file mode 100644
index 0000000..6550f87
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRuleMsgHandler.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.handlers;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.msg.GetAttributesRequestRuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.TelemetryUploadRequestRuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.UpdateAttributesRequestRuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class DefaultRuleMsgHandler implements RuleMsgHandler {
+
+ @Override
+ public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
+ if (msg instanceof TelemetryUploadRequestRuleToPluginMsg) {
+ handleTelemetryUploadRequest(ctx, tenantId, ruleId, (TelemetryUploadRequestRuleToPluginMsg) msg);
+ } else if (msg instanceof UpdateAttributesRequestRuleToPluginMsg) {
+ handleUpdateAttributesRequest(ctx, tenantId, ruleId, (UpdateAttributesRequestRuleToPluginMsg) msg);
+ } else if (msg instanceof GetAttributesRequestRuleToPluginMsg) {
+ handleGetAttributesRequest(ctx, tenantId, ruleId, (GetAttributesRequestRuleToPluginMsg) msg);
+ }
+ //TODO: handle subscriptions to attribute updates.
+ }
+
+ protected void handleGetAttributesRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, GetAttributesRequestRuleToPluginMsg msg) {
+ msgTypeNotSupported(msg.getPayload().getMsgType());
+ }
+
+ protected void handleUpdateAttributesRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, UpdateAttributesRequestRuleToPluginMsg msg) {
+ msgTypeNotSupported(msg.getPayload().getMsgType());
+ }
+
+ protected void handleTelemetryUploadRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, TelemetryUploadRequestRuleToPluginMsg msg) {
+ msgTypeNotSupported(msg.getPayload().getMsgType());
+ }
+
+ private void msgTypeNotSupported(MsgType msgType) {
+ throw new RuntimeException("Not supported msg type: " + msgType + "!");
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java
new file mode 100644
index 0000000..fab11bb
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java
@@ -0,0 +1,103 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.handlers;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+import org.thingsboard.server.extensions.api.plugins.ws.SessionEvent;
+import org.thingsboard.server.extensions.api.plugins.ws.WsSessionMetaData;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.*;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class DefaultWebsocketMsgHandler implements WebsocketMsgHandler {
+
+ protected final ObjectMapper jsonMapper = new ObjectMapper();
+
+ protected final Map<String, WsSessionMetaData> wsSessionsMap = new HashMap<>();
+
+ @Override
+ public void process(PluginContext ctx, PluginWebsocketMsg<?> wsMsg) {
+ PluginWebsocketSessionRef sessionRef = wsMsg.getSessionRef();
+ if (log.isTraceEnabled()) {
+ log.trace("[{}] Processing: {}", sessionRef.getSessionId(), wsMsg);
+ } else {
+ log.debug("[{}] Processing: {}", sessionRef.getSessionId(), wsMsg.getClass().getSimpleName());
+ }
+ if (wsMsg instanceof SessionEventPluginWebSocketMsg) {
+ handleWebSocketSessionEvent(ctx, sessionRef, (SessionEventPluginWebSocketMsg) wsMsg);
+ } else if (wsMsg instanceof TextPluginWebSocketMsg || wsMsg instanceof BinaryPluginWebSocketMsg) {
+ handleWebSocketMsg(ctx, sessionRef, wsMsg);
+ } else if (wsMsg instanceof PongPluginWebsocketMsg) {
+ handleWebSocketPongEvent(ctx, sessionRef);
+ }
+ }
+
+ protected void handleWebSocketMsg(PluginContext ctx, PluginWebsocketSessionRef sessionRef, PluginWebsocketMsg<?> wsMsg) {
+ throw new RuntimeException("Web-sockets are not supported by current plugin!");
+ }
+
+ protected void cleanupWebSocketSession(PluginContext ctx, String sessionId) {
+
+ }
+
+ protected void handleWebSocketSessionEvent(PluginContext ctx, PluginWebsocketSessionRef sessionRef, SessionEventPluginWebSocketMsg wsMsg) {
+ String sessionId = sessionRef.getSessionId();
+ SessionEvent event = wsMsg.getPayload();
+ log.debug("[{}] Processing: {}", 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().get());
+ break;
+ case CLOSED:
+ wsSessionsMap.remove(sessionId);
+ cleanupWebSocketSession(ctx, sessionId);
+ break;
+ }
+ }
+
+ protected void handleWebSocketPongEvent(PluginContext ctx, PluginWebsocketSessionRef sessionRef) {
+ String sessionId = sessionRef.getSessionId();
+ WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
+ if (sessionMD != null) {
+ log.debug("[{}] Updating session metadata: {}", sessionId, sessionRef);
+ sessionMD.setSessionRef(sessionRef);
+ sessionMD.setLastActivityTime(System.currentTimeMillis());
+ }
+ }
+
+ public void clear(PluginContext ctx) {
+ wsSessionsMap.values().stream().forEach(v -> {
+ try {
+ ctx.close(v.getSessionRef());
+ } catch (IOException e) {
+ log.debug("[{}] Failed to close session: {}", v.getSessionRef().getSessionId(), e.getMessage(), e);
+ }
+ });
+ wsSessionsMap.clear();
+ }
+}
\ No newline at end of file
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RestMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RestMsgHandler.java
new file mode 100644
index 0000000..d91ba7a
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RestMsgHandler.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.handlers;
+
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface RestMsgHandler {
+
+ void process(PluginContext ctx, PluginRestMsg msg);
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RpcMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RpcMsgHandler.java
new file mode 100644
index 0000000..42aeb81
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RpcMsgHandler.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.handlers;
+
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface RpcMsgHandler {
+
+ void process(PluginContext ctx, RpcMsg msg);
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RuleMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RuleMsgHandler.java
new file mode 100644
index 0000000..c356575
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RuleMsgHandler.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.handlers;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface RuleMsgHandler {
+
+ void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException;
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/WebsocketMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/WebsocketMsgHandler.java
new file mode 100644
index 0000000..8526ec5
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/WebsocketMsgHandler.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.handlers;
+
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface WebsocketMsgHandler {
+
+ void process(PluginContext ctx, PluginWebsocketMsg<?> wsMsg);
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractPluginToRuleMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractPluginToRuleMsg.java
new file mode 100644
index 0000000..398f5a3
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractPluginToRuleMsg.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+public class AbstractPluginToRuleMsg<T extends Serializable> implements PluginToRuleMsg<T> {
+
+ private static final long serialVersionUID = 1L;
+
+ private final UUID uid;
+ private final TenantId tenantId;
+ private final RuleId ruleId;
+ private final T payload;
+
+ public AbstractPluginToRuleMsg(UUID uid, TenantId tenantId, RuleId ruleId, T payload) {
+ super();
+ this.uid = uid;
+ this.tenantId = tenantId;
+ this.ruleId = ruleId;
+ this.payload = payload;
+ }
+
+ @Override
+ public UUID getUid() {
+ return uid;
+ }
+
+ @Override
+ public TenantId getTenantId() {
+ return tenantId;
+ }
+
+ @Override
+ public T getPayload() {
+ return payload;
+ }
+
+ @Override
+ public RuleId getRuleId() {
+ return ruleId;
+ }
+
+
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractRuleToPluginMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractRuleToPluginMsg.java
new file mode 100644
index 0000000..ead9e9d
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractRuleToPluginMsg.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+public abstract class AbstractRuleToPluginMsg<T extends Serializable> implements RuleToPluginMsg<T> {
+
+ private static final long serialVersionUID = 1L;
+
+ private final UUID uid;
+ private final TenantId tenantId;
+ private final CustomerId customerId;
+ private final DeviceId deviceId;
+ private final T payload;
+
+ public AbstractRuleToPluginMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, T payload) {
+ super();
+ this.uid = UUID.randomUUID();
+ this.tenantId = tenantId;
+ this.customerId = customerId;
+ this.deviceId = deviceId;
+ this.payload = payload;
+ }
+
+ @Override
+ public UUID getUid() {
+ return uid;
+ }
+
+ public TenantId getTenantId() {
+ return tenantId;
+ }
+
+ public CustomerId getCustomerId() {
+ return customerId;
+ }
+
+ @Override
+ public DeviceId getDeviceId() {
+ return deviceId;
+ }
+
+ public T getPayload() {
+ return payload;
+ }
+
+ @Override
+ public String toString() {
+ return "AbstractRuleToPluginMsg [uid=" + uid + ", tenantId=" + tenantId + ", customerId=" + customerId
+ + ", deviceId=" + deviceId + ", payload=" + payload + "]";
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/FromDeviceRpcResponse.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/FromDeviceRpcResponse.java
new file mode 100644
index 0000000..f9e1883
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/FromDeviceRpcResponse.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@RequiredArgsConstructor
+@ToString
+public class FromDeviceRpcResponse {
+ @Getter
+ private final UUID id;
+ private final String response;
+ private final RpcError error;
+
+ public Optional<String> getResponse() {
+ return Optional.ofNullable(response);
+ }
+
+ public Optional<RpcError> getError() {
+ return Optional.ofNullable(error);
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/GetAttributesRequestRuleToPluginMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/GetAttributesRequestRuleToPluginMsg.java
new file mode 100644
index 0000000..4c821f6
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/GetAttributesRequestRuleToPluginMsg.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.GetAttributesRequest;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class GetAttributesRequestRuleToPluginMsg extends AbstractRuleToPluginMsg<GetAttributesRequest> {
+
+ private static final long serialVersionUID = 1L;
+
+ public GetAttributesRequestRuleToPluginMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, GetAttributesRequest payload) {
+ super(tenantId, customerId, deviceId, payload);
+ }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/GetRequestRuleToPluginMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/GetRequestRuleToPluginMsg.java
new file mode 100644
index 0000000..c824a3f
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/GetRequestRuleToPluginMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+public class GetRequestRuleToPluginMsg extends AbstractRuleToPluginMsg<String[]>{
+
+ private static final long serialVersionUID = 1L;
+
+ public GetRequestRuleToPluginMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId,
+ String[] payload) {
+ super(tenantId, customerId, deviceId, payload);
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/PluginToRuleMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/PluginToRuleMsg.java
new file mode 100644
index 0000000..a6bd6e5
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/PluginToRuleMsg.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.rules.ToRuleActorMsg;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+/**
+ * The basic interface for messages that are sent from particular plugin to rule
+ * instance
+ *
+ * @author ashvayka
+ * @see RuleToPluginMsg
+ *
+ */
+public interface PluginToRuleMsg<V extends Serializable> extends ToRuleActorMsg, Serializable {
+
+ /**
+ * Returns the unique identifier of the message
+ *
+ * @return unique identifier of the message.
+ */
+ UUID getUid();
+
+ /**
+ * Returns the unique identifier of the tenant that owns the rule
+ *
+ * @return unique identifier of the tenant that owns the rule.
+ *
+ */
+ TenantId getTenantId();
+
+ /**
+ * Returns the unique identifier of the rule
+ *
+ * @return unique identifier of the rule.
+ */
+ RuleId getRuleId();
+
+ /**
+ * Returns the serializable message payload.
+ *
+ * @return the serializable message payload.
+ */
+ V getPayload();
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ResponsePluginToRuleMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ResponsePluginToRuleMsg.java
new file mode 100644
index 0000000..6768042
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ResponsePluginToRuleMsg.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+
+import java.util.UUID;
+
+public class ResponsePluginToRuleMsg extends AbstractPluginToRuleMsg<ToDeviceMsg>{
+
+ private static final long serialVersionUID = 1L;
+
+ public ResponsePluginToRuleMsg(UUID uid, TenantId tenantId, RuleId ruleId, ToDeviceMsg payload) {
+ super(uid, tenantId, ruleId, payload);
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcError.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcError.java
new file mode 100644
index 0000000..1f3768e
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcError.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+/**
+ * @author Andrew Shvayka
+ */
+public enum RpcError {
+ NOT_FOUND, FORBIDDEN, NO_ACTIVE_CONNECTION, TIMEOUT, INTERNAL;
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcRequestRuleToPluginMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcRequestRuleToPluginMsg.java
new file mode 100644
index 0000000..8826979
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcRequestRuleToPluginMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.ToServerRpcRequestMsg;
+
+public class RpcRequestRuleToPluginMsg extends AbstractRuleToPluginMsg<ToServerRpcRequestMsg> {
+
+ private static final long serialVersionUID = 1L;
+
+ public RpcRequestRuleToPluginMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, ToServerRpcRequestMsg payload) {
+ super(tenantId, customerId, deviceId, payload);
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcResponsePluginToRuleMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcResponsePluginToRuleMsg.java
new file mode 100644
index 0000000..6ddd56a
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcResponsePluginToRuleMsg.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.ToServerRpcResponseMsg;
+
+import java.util.UUID;
+
+public class RpcResponsePluginToRuleMsg extends AbstractPluginToRuleMsg<ToServerRpcResponseMsg> {
+
+ private static final long serialVersionUID = 1L;
+
+ public RpcResponsePluginToRuleMsg(UUID uid, TenantId tenantId, RuleId ruleId, ToServerRpcResponseMsg payload) {
+ super(uid, tenantId, ruleId, payload);
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RuleToPluginMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RuleToPluginMsg.java
new file mode 100644
index 0000000..548bf8e
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RuleToPluginMsg.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+/**
+ * The basic interface for messages that are sent from particular rule to plugin
+ * instance
+ *
+ * @author ashvayka
+ *
+ */
+public interface RuleToPluginMsg<V extends Serializable> extends Serializable {
+
+ /**
+ * Returns the unique identifier of the message
+ *
+ * @return unique identifier of the message.
+ */
+ UUID getUid();
+
+
+ /**
+ * Returns the unique identifier of the device that send the message
+ *
+ * @return unique identifier of the device.
+ */
+ DeviceId getDeviceId();
+
+ /**
+ * Returns the unique identifier of the customer that owns the device
+ *
+ * @return unique identifier of the device.
+ */
+ CustomerId getCustomerId();
+
+ /**
+ * Returns the serializable message payload.
+ *
+ * @return the serializable message payload.
+ */
+ V getPayload();
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TelemetryUploadRequestRuleToPluginMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TelemetryUploadRequestRuleToPluginMsg.java
new file mode 100644
index 0000000..c72cdb3
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TelemetryUploadRequestRuleToPluginMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+
+public class TelemetryUploadRequestRuleToPluginMsg extends AbstractRuleToPluginMsg<TelemetryUploadRequest> {
+
+ private static final long serialVersionUID = 1L;
+
+ public TelemetryUploadRequestRuleToPluginMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, TelemetryUploadRequest payload) {
+ super(tenantId, customerId, deviceId, payload);
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutIntMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutIntMsg.java
new file mode 100644
index 0000000..6d96ae7
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutIntMsg.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+/**
+ * @author Andrew Shvayka
+ */
+public final class TimeoutIntMsg extends TimeoutMsg<Integer> {
+
+ public TimeoutIntMsg(Integer id, long timeout) {
+ super(id, timeout);
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutMsg.java
new file mode 100644
index 0000000..8932c33
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutMsg.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class TimeoutMsg<T> {
+ private final T id;
+ private final long timeout;
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutUUIDMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutUUIDMsg.java
new file mode 100644
index 0000000..47b00a2
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutUUIDMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+public final class TimeoutUUIDMsg extends TimeoutMsg<UUID> {
+
+ public TimeoutUUIDMsg(UUID id, long timeout) {
+ super(id, timeout);
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java
new file mode 100644
index 0000000..3fdc076
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class ToDeviceRpcRequest implements Serializable {
+ private final UUID id;
+ private final TenantId tenantId;
+ private final DeviceId deviceId;
+ private final boolean oneway;
+ private final long expirationTime;
+ private final ToDeviceRpcRequestBody body;
+}
+
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequestBody.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequestBody.java
new file mode 100644
index 0000000..c37e576
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequestBody.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class ToDeviceRpcRequestBody implements Serializable {
+ private final String method;
+ private final String params;
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequestPluginMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequestPluginMsg.java
new file mode 100644
index 0000000..c19c9dc
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequestPluginMsg.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+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.extensions.api.device.ToDeviceActorNotificationMsg;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+@ToString
+@RequiredArgsConstructor
+public class ToDeviceRpcRequestPluginMsg implements ToDeviceActorNotificationMsg {
+
+ private final ServerAddress serverAddress;
+ @Getter
+ private final PluginId pluginId;
+ @Getter
+ private final TenantId pluginTenantId;
+ @Getter
+ private final ToDeviceRpcRequest msg;
+
+ public ToDeviceRpcRequestPluginMsg(PluginId pluginId, TenantId pluginTenantId, ToDeviceRpcRequest msg) {
+ this(null, pluginId, pluginTenantId, msg);
+ }
+
+ public Optional<ServerAddress> getServerAddress() {
+ return Optional.ofNullable(serverAddress);
+ }
+
+ @Override
+ public DeviceId getDeviceId() {
+ return msg.getDeviceId();
+ }
+
+ @Override
+ public TenantId getTenantId() {
+ return msg.getTenantId();
+ }
+}
+
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToPluginActorMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToPluginActorMsg.java
new file mode 100644
index 0000000..22a4faf
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToPluginActorMsg.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.aware.PluginAwareMsg;
+
+public interface ToPluginActorMsg extends PluginAwareMsg {
+
+ TenantId getPluginTenantId();
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToPluginRpcResponseDeviceMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToPluginRpcResponseDeviceMsg.java
new file mode 100644
index 0000000..f761850
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToPluginRpcResponseDeviceMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class ToPluginRpcResponseDeviceMsg implements ToPluginActorMsg {
+ private final PluginId pluginId;
+ private final TenantId pluginTenantId;
+ private final FromDeviceRpcResponse response;
+}
+
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/UpdateAttributesRequestRuleToPluginMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/UpdateAttributesRequestRuleToPluginMsg.java
new file mode 100644
index 0000000..a427d65
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/UpdateAttributesRequestRuleToPluginMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.UpdateAttributesRequest;
+
+public class UpdateAttributesRequestRuleToPluginMsg extends AbstractRuleToPluginMsg<UpdateAttributesRequest> {
+
+ private static final long serialVersionUID = 1L;
+
+ public UpdateAttributesRequestRuleToPluginMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, UpdateAttributesRequest payload) {
+ super(tenantId, customerId, deviceId, payload);
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/Plugin.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/Plugin.java
new file mode 100644
index 0000000..15c12d4
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/Plugin.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.api.component.ConfigurableComponent;
+import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+
+public interface Plugin<T> extends ConfigurableComponent<T> {
+
+ void process(PluginContext ctx, PluginWebsocketMsg<?> wsMsg);
+
+ void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException;
+
+ void process(PluginContext ctx, PluginRestMsg msg);
+
+ void process(PluginContext ctx, RpcMsg msg);
+
+ void process(PluginContext ctx, FromDeviceRpcResponse msg);
+
+ void process(PluginContext ctx, TimeoutMsg<?> msg);
+
+ void onServerAdded(PluginContext ctx, ServerAddress server);
+
+ void onServerRemoved(PluginContext ctx, ServerAddress server);
+
+ void resume(PluginContext ctx);
+
+ void suspend(PluginContext ctx);
+
+ void stop(PluginContext ctx);
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginAction.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginAction.java
new file mode 100644
index 0000000..2d71f1a
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginAction.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins;
+
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.extensions.api.component.ConfigurableComponent;
+import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.api.rules.RuleLifecycleComponent;
+import org.thingsboard.server.extensions.api.rules.RuleProcessingMetaData;
+
+import java.util.Optional;
+
+public interface PluginAction<T> extends ConfigurableComponent<T>, RuleLifecycleComponent {
+
+ Optional<RuleToPluginMsg<?>> convert(RuleContext ctx, ToDeviceActorMsg toDeviceActorMsg, RuleProcessingMetaData deviceMsgMd);
+
+ Optional<ToDeviceMsg> convert(PluginToRuleMsg<?> response);
+
+ boolean isOneWayAction();
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java
new file mode 100644
index 0000000..ff02567
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import java.io.Serializable;
+
+public final class PluginApiCallSecurityContext implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private final TenantId pluginTenantId;
+ private final PluginId pluginId;
+ private final TenantId tenantId;
+ private final CustomerId customerId;
+
+ public PluginApiCallSecurityContext(TenantId pluginTenantId, PluginId pluginId, TenantId tenantId, CustomerId customerId) {
+ super();
+ this.pluginTenantId = pluginTenantId;
+ this.pluginId = pluginId;
+ this.tenantId = tenantId;
+ this.customerId = customerId;
+ }
+
+ public TenantId getPluginTenantId(){
+ return pluginTenantId;
+ }
+
+ public PluginId getPluginId() {
+ return pluginId;
+ }
+
+ public boolean isSystemAdmin() {
+ return tenantId == null || EntityId.NULL_UUID.equals(tenantId.getId());
+ }
+
+ public boolean isTenantAdmin() {
+ return !isSystemAdmin() && (customerId == null || EntityId.NULL_UUID.equals(customerId.getId()));
+ }
+
+ public boolean isCustomerUser() {
+ return !isSystemAdmin() && !isTenantAdmin();
+ }
+
+ public TenantId getTenantId() {
+ return tenantId;
+ }
+
+ public CustomerId getCustomerId() {
+ return customerId;
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginCallback.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginCallback.java
new file mode 100644
index 0000000..281a3af
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginCallback.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface PluginCallback<T> {
+
+ void onSuccess(PluginContext ctx, T value);
+
+ void onFailure(PluginContext ctx, Exception e);
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginConstants.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginConstants.java
new file mode 100644
index 0000000..13a9561
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginConstants.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class PluginConstants {
+ public static final String PLUGIN_URL_PREFIX = "/api/plugins";
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java
new file mode 100644
index 0000000..834ec3a
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins;
+
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.CustomerId;
+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.data.kv.AttributeKvEntry;
+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.extensions.api.plugins.msg.PluginToRuleMsg;
+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.rpc.RpcMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+public interface PluginContext {
+
+ PluginId getPluginId();
+
+ void reply(PluginToRuleMsg<?> msg);
+
+ boolean checkAccess(DeviceId deviceId);
+
+ Optional<PluginApiCallSecurityContext> getSecurityCtx();
+
+ /*
+ Device RPC API
+ */
+
+ Optional<ServerAddress> resolve(DeviceId deviceId);
+
+ void getDevice(DeviceId deviceId, PluginCallback<Device> pluginCallback);
+
+ void sendRpcRequest(ToDeviceRpcRequest msg);
+
+ void scheduleTimeoutMsg(TimeoutMsg<?> timeoutMsg);
+
+
+ /*
+ Websocket API
+ */
+
+ void send(PluginWebsocketMsg<?> wsMsg) throws IOException;
+
+ void close(PluginWebsocketSessionRef sessionRef) throws IOException;
+
+ /*
+ Plugin RPC API
+ */
+
+ void sendPluginRpcMsg(RpcMsg msg);
+
+ /*
+ Timeseries API
+ */
+
+
+ void saveTsData(DeviceId deviceId, TsKvEntry entry, PluginCallback<Void> callback);
+
+ void saveTsData(DeviceId deviceId, List<TsKvEntry> entry, PluginCallback<Void> callback);
+
+ List<TsKvEntry> loadTimeseries(DeviceId deviceId, TsKvQuery query);
+
+ void loadLatestTimeseries(DeviceId deviceId, Collection<String> keys, PluginCallback<List<TsKvEntry>> callback);
+
+ void loadLatestTimeseries(DeviceId deviceId, PluginCallback<List<TsKvEntry>> callback);
+
+ /*
+ Attributes API
+ */
+
+ void saveAttributes(DeviceId deviceId, String attributeType, List<AttributeKvEntry> attributes, PluginCallback<Void> callback);
+
+ Optional<AttributeKvEntry> loadAttribute(DeviceId deviceId, String attributeType, String attributeKey);
+
+ List<AttributeKvEntry> loadAttributes(DeviceId deviceId, String attributeType, List<String> attributeKeys);
+
+ List<AttributeKvEntry> loadAttributes(DeviceId deviceId, String attributeType);
+
+ void removeAttributes(DeviceId deviceId, String scope, List<String> attributeKeys);
+
+ void getCustomerDevices(TenantId tenantId, CustomerId customerId, int limit, PluginCallback<List<Device>> callback);
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginException.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginException.java
new file mode 100644
index 0000000..5f39ef9
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginException.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins;
+
+public class PluginException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ public PluginException(String msg, Exception e) {
+ super(msg, e);
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginInitializationException.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginInitializationException.java
new file mode 100644
index 0000000..e4eb432
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginInitializationException.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins;
+
+public class PluginInitializationException extends PluginException {
+
+ private static final long serialVersionUID = 1L;
+
+ public PluginInitializationException(String msg, Exception e) {
+ super(msg, e);
+ }
+
+ public PluginInitializationException(String msg) {
+ super(msg, null);
+ }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/BasicPluginRestMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/BasicPluginRestMsg.java
new file mode 100644
index 0000000..3a17638
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/BasicPluginRestMsg.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.rest;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
+
+@SuppressWarnings("rawtypes")
+public class BasicPluginRestMsg implements PluginRestMsg {
+
+ private final PluginApiCallSecurityContext securityCtx;
+ private final RestRequest request;
+ private final DeferredResult<ResponseEntity> responseHolder;
+
+ public BasicPluginRestMsg(PluginApiCallSecurityContext securityCtx, RestRequest request,
+ DeferredResult<ResponseEntity> responseHolder) {
+ super();
+ this.securityCtx = securityCtx;
+ this.request = request;
+ this.responseHolder = responseHolder;
+ }
+
+ @Override
+ public PluginApiCallSecurityContext getSecurityCtx() {
+ return securityCtx;
+ }
+
+ @Override
+ public RestRequest getRequest() {
+ return request;
+ }
+
+ @Override
+ public DeferredResult<ResponseEntity> getResponseHolder() {
+ return responseHolder;
+ }
+
+ @Override
+ public PluginId getPluginId() {
+ return securityCtx.getPluginId();
+ }
+
+ @Override
+ public TenantId getPluginTenantId() {
+ return securityCtx.getPluginTenantId();
+ }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/PluginRestMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/PluginRestMsg.java
new file mode 100644
index 0000000..b2ba689
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/PluginRestMsg.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.rest;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
+import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
+
+@SuppressWarnings("rawtypes")
+public interface PluginRestMsg extends ToPluginActorMsg {
+
+ RestRequest getRequest();
+
+ DeferredResult<ResponseEntity> getResponseHolder();
+
+ PluginApiCallSecurityContext getSecurityCtx();
+
+ @Override
+ default PluginId getPluginId() {
+ return getSecurityCtx().getPluginId();
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/RestRequest.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/RestRequest.java
new file mode 100644
index 0000000..c9aaefd
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/RestRequest.java
@@ -0,0 +1,87 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.rest;
+
+import lombok.Data;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.RequestEntity;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.thingsboard.server.extensions.api.plugins.PluginConstants;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import java.nio.charset.Charset;
+import java.util.Optional;
+import java.util.function.Function;
+
+@Data
+public class RestRequest {
+ private static final Charset UTF8 = Charset.forName("UTF-8");
+ private final RequestEntity<byte[]> requestEntity;
+ private final HttpServletRequest request;
+
+ public HttpMethod getMethod() {
+ return requestEntity.getMethod();
+ }
+
+ public String getRequestBody() {
+ return new String(requestEntity.getBody(), UTF8);
+ }
+
+ public String[] getPathParams() {
+ String requestUrl = request.getRequestURL().toString();
+ int index = requestUrl.indexOf(PluginConstants.PLUGIN_URL_PREFIX);
+ String[] pathParams = requestUrl.substring(index + PluginConstants.PLUGIN_URL_PREFIX.length()).split("/");
+ String[] result = new String[pathParams.length - 2];
+ System.arraycopy(pathParams, 2, result, 0, result.length);
+ return result;
+ }
+
+ public String getParameter(String paramName) throws ServletException {
+ return getParameter(paramName, null);
+ }
+
+ public String getParameter(String paramName, String defaultValue) throws ServletException {
+ String paramValue = request.getParameter(paramName);
+ if (StringUtils.isEmpty(paramValue)) {
+ if (defaultValue == null) {
+ throw new MissingServletRequestParameterException(paramName, "String");
+ } else {
+ return defaultValue;
+ }
+ } else {
+ return paramValue;
+ }
+ }
+
+ public Optional<Long> getLongParamValue(String paramName) {
+ return getParamValue(paramName, s -> Long.valueOf(s));
+ }
+
+ public Optional<Integer> getIntParamValue(String paramName) {
+ return getParamValue(paramName, s -> Integer.valueOf(s));
+ }
+
+ public <T> Optional<T> getParamValue(String paramName, Function<String, T> function) {
+ String paramValue = request.getParameter(paramName);
+ if (paramValue != null) {
+ return Optional.of(function.apply(paramValue));
+ } else {
+ return Optional.empty();
+ }
+ }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rpc/PluginRpcMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rpc/PluginRpcMsg.java
new file mode 100644
index 0000000..7aaaf01
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rpc/PluginRpcMsg.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.rpc;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
+
+@ToString
+@RequiredArgsConstructor
+public class PluginRpcMsg implements ToPluginActorMsg {
+
+ private final TenantId tenantId;
+ private final PluginId pluginId;
+ @Getter
+ private final RpcMsg rpcMsg;
+
+ @Override
+ public TenantId getPluginTenantId() {
+ return tenantId;
+ }
+
+ @Override
+ public PluginId getPluginId() {
+ return pluginId;
+ }
+
+
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rpc/RpcMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rpc/RpcMsg.java
new file mode 100644
index 0000000..71a8534
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rpc/RpcMsg.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.rpc;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+
+/**
+ * @author Andrew Shvayka
+ */
+@ToString
+@RequiredArgsConstructor
+public class RpcMsg {
+ @Getter
+ private final ServerAddress serverAddress;
+ @Getter
+ private final int msgClazz;
+ @Getter
+ private final byte[] msgData;
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/BasicPluginWebsocketSessionRef.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/BasicPluginWebsocketSessionRef.java
new file mode 100644
index 0000000..2f4c582
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/BasicPluginWebsocketSessionRef.java
@@ -0,0 +1,111 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.ws;
+
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
+
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.util.Map;
+
+public class BasicPluginWebsocketSessionRef implements PluginWebsocketSessionRef {
+
+ private static final long serialVersionUID = 1L;
+
+ private final String sessionId;
+ private final PluginApiCallSecurityContext securityCtx;
+ private final URI uri;
+ private final Map<String, Object> attributes;
+ private final InetSocketAddress localAddress;
+ private final InetSocketAddress remoteAddress;
+
+ public BasicPluginWebsocketSessionRef(String sessionId, PluginApiCallSecurityContext securityCtx, URI uri, Map<String, Object> attributes,
+ InetSocketAddress localAddress, InetSocketAddress remoteAddress) {
+ super();
+ this.sessionId = sessionId;
+ this.securityCtx = securityCtx;
+ this.uri = uri;
+ this.attributes = attributes;
+ this.localAddress = localAddress;
+ this.remoteAddress = remoteAddress;
+ }
+
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ public TenantId getPluginTenantId() {
+ return securityCtx.getPluginTenantId();
+ }
+
+ public PluginId getPluginId() {
+ return securityCtx.getPluginId();
+ }
+
+ public URI getUri() {
+ return uri;
+ }
+
+ public Map<String, Object> getAttributes() {
+ return attributes;
+ }
+
+ public InetSocketAddress getLocalAddress() {
+ return localAddress;
+ }
+
+ public InetSocketAddress getRemoteAddress() {
+ return remoteAddress;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((sessionId == null) ? 0 : sessionId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ BasicPluginWebsocketSessionRef other = (BasicPluginWebsocketSessionRef) obj;
+ if (sessionId == null) {
+ if (other.sessionId != null)
+ return false;
+ } else if (!sessionId.equals(other.sessionId))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "BasicPluginWebsocketSessionRef [sessionId=" + sessionId + ", pluginId=" + getPluginId() + "]";
+ }
+
+ @Override
+ public PluginApiCallSecurityContext getSecurityCtx() {
+ return securityCtx;
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/AbstractPluginWebSocketMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/AbstractPluginWebSocketMsg.java
new file mode 100644
index 0000000..ef70edc
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/AbstractPluginWebSocketMsg.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.ws.msg;
+
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+
+public abstract class AbstractPluginWebSocketMsg<T> implements PluginWebsocketMsg<T> {
+
+ private static final long serialVersionUID = 1L;
+
+ private final PluginWebsocketSessionRef sessionRef;
+ private final T payload;
+
+ AbstractPluginWebSocketMsg(PluginWebsocketSessionRef sessionRef, T payload) {
+ this.sessionRef = sessionRef;
+ this.payload = payload;
+ }
+
+ public PluginWebsocketSessionRef getSessionRef() {
+ return sessionRef;
+ }
+
+
+ @Override
+ public TenantId getPluginTenantId(){
+ return sessionRef.getPluginTenantId();
+ }
+
+ @Override
+ public PluginId getPluginId() {
+ return sessionRef.getPluginId();
+ }
+
+ @Override
+ public PluginApiCallSecurityContext getSecurityCtx() {
+ return sessionRef.getSecurityCtx();
+ }
+
+ public T getPayload() {
+ return payload;
+ }
+
+ @Override
+ public String toString() {
+ return "AbstractPluginWebSocketMsg [sessionRef=" + sessionRef + ", payload=" + payload + "]";
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/BinaryPluginWebSocketMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/BinaryPluginWebSocketMsg.java
new file mode 100644
index 0000000..8afe0ef
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/BinaryPluginWebSocketMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.ws.msg;
+
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+
+import java.nio.ByteBuffer;
+
+public class BinaryPluginWebSocketMsg extends AbstractPluginWebSocketMsg<ByteBuffer> {
+
+ private static final long serialVersionUID = 1L;
+
+ public BinaryPluginWebSocketMsg(PluginWebsocketSessionRef sessionRef, ByteBuffer payload) {
+ super(sessionRef, payload);
+ }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/EmptyPluginWebsocketMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/EmptyPluginWebsocketMsg.java
new file mode 100644
index 0000000..a02c997
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/EmptyPluginWebsocketMsg.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.ws.msg;
+
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+
+import java.nio.ByteBuffer;
+
+public class EmptyPluginWebsocketMsg extends AbstractPluginWebSocketMsg<ByteBuffer> {
+
+ private static final long serialVersionUID = 1L;
+ private static ByteBuffer EMPTY = ByteBuffer.wrap(new byte[0]);
+
+ protected EmptyPluginWebsocketMsg(PluginWebsocketSessionRef sessionRef) {
+ super(sessionRef, EMPTY);
+ }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PingPluginWebsocketMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PingPluginWebsocketMsg.java
new file mode 100644
index 0000000..7d6d682
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PingPluginWebsocketMsg.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.ws.msg;
+
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+
+public class PingPluginWebsocketMsg extends EmptyPluginWebsocketMsg {
+
+ private static final long serialVersionUID = 1L;
+
+ public PingPluginWebsocketMsg(PluginWebsocketSessionRef sessionRef) {
+ super(sessionRef);
+ }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PluginWebsocketMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PluginWebsocketMsg.java
new file mode 100644
index 0000000..8cc4232
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PluginWebsocketMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.ws.msg;
+
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
+import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+
+import java.io.Serializable;
+
+public interface PluginWebsocketMsg<T> extends ToPluginActorMsg, Serializable {
+
+ PluginWebsocketSessionRef getSessionRef();
+
+ T getPayload();
+
+ PluginApiCallSecurityContext getSecurityCtx();
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PongPluginWebsocketMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PongPluginWebsocketMsg.java
new file mode 100644
index 0000000..ab5a238
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PongPluginWebsocketMsg.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.ws.msg;
+
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+
+public class PongPluginWebsocketMsg extends EmptyPluginWebsocketMsg {
+
+ private static final long serialVersionUID = 1L;
+
+ public PongPluginWebsocketMsg(PluginWebsocketSessionRef sessionRef) {
+ super(sessionRef);
+ }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/SessionEventPluginWebSocketMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/SessionEventPluginWebSocketMsg.java
new file mode 100644
index 0000000..a365498
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/SessionEventPluginWebSocketMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.ws.msg;
+
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+import org.thingsboard.server.extensions.api.plugins.ws.SessionEvent;
+
+public class SessionEventPluginWebSocketMsg extends AbstractPluginWebSocketMsg<SessionEvent> {
+
+ private static final long serialVersionUID = 1L;
+
+ public SessionEventPluginWebSocketMsg(PluginWebsocketSessionRef sessionRef, SessionEvent payload) {
+ super(sessionRef, payload);
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/TextPluginWebSocketMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/TextPluginWebSocketMsg.java
new file mode 100644
index 0000000..d144b6c
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/TextPluginWebSocketMsg.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.ws.msg;
+
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+
+public class TextPluginWebSocketMsg extends AbstractPluginWebSocketMsg<String> {
+
+ private static final long serialVersionUID = 1L;
+
+ public TextPluginWebSocketMsg(PluginWebsocketSessionRef sessionRef, String payload) {
+ super(sessionRef, payload);
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/PluginWebsocketSessionRef.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/PluginWebsocketSessionRef.java
new file mode 100644
index 0000000..7079c82
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/PluginWebsocketSessionRef.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.ws;
+
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
+
+import java.io.Serializable;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.util.Map;
+
+public interface PluginWebsocketSessionRef extends Serializable {
+
+ String getSessionId();
+
+ TenantId getPluginTenantId();
+
+ PluginId getPluginId();
+
+ URI getUri();
+
+ Map<String, Object> getAttributes();
+
+ InetSocketAddress getLocalAddress();
+
+ InetSocketAddress getRemoteAddress();
+
+ PluginApiCallSecurityContext getSecurityCtx();
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/SessionEvent.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/SessionEvent.java
new file mode 100644
index 0000000..66d3f27
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/SessionEvent.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.ws;
+
+import lombok.Getter;
+import lombok.ToString;
+
+import java.util.Optional;
+
+@ToString
+public class SessionEvent {
+
+ public enum SessionEventType {
+ ESTABLISHED, CLOSED, ERROR
+ };
+
+ @Getter
+ private final SessionEventType eventType;
+ @Getter
+ private final Optional<Throwable> error;
+
+ private SessionEvent(SessionEventType eventType, Throwable error) {
+ super();
+ this.eventType = eventType;
+ this.error = Optional.ofNullable(error);
+ }
+
+ public static SessionEvent onEstablished() {
+ return new SessionEvent(SessionEventType.ESTABLISHED, null);
+ }
+
+ public static SessionEvent onClosed() {
+ return new SessionEvent(SessionEventType.CLOSED, null);
+ }
+
+ public static SessionEvent onError(Throwable t) {
+ return new SessionEvent(SessionEventType.ERROR, t);
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/WsSessionMetaData.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/WsSessionMetaData.java
new file mode 100644
index 0000000..0ee8830
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/WsSessionMetaData.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016 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.extensions.api.plugins.ws;
+
+public class WsSessionMetaData {
+
+ private PluginWebsocketSessionRef sessionRef;
+ private long lastActivityTime;
+
+ public WsSessionMetaData(PluginWebsocketSessionRef sessionRef) {
+ super();
+ this.sessionRef = sessionRef;
+ this.lastActivityTime = System.currentTimeMillis();
+ }
+
+ public PluginWebsocketSessionRef getSessionRef() {
+ return sessionRef;
+ }
+
+ public void setSessionRef(PluginWebsocketSessionRef sessionRef) {
+ this.sessionRef = sessionRef;
+ }
+
+ public long getLastActivityTime() {
+ return lastActivityTime;
+ }
+
+ public void setLastActivityTime(long lastActivityTime) {
+ this.lastActivityTime = lastActivityTime;
+ }
+
+ @Override
+ public String toString() {
+ return "WsSessionMetaData [sessionRef=" + sessionRef + ", lastActivityTime=" + lastActivityTime + "]";
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleContext.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleContext.java
new file mode 100644
index 0000000..337028f
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleContext.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 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.extensions.api.rules;
+
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.extensions.api.device.DeviceAttributes;
+
+import java.util.Optional;
+
+public interface RuleContext {
+
+ RuleId getRuleId();
+
+ DeviceAttributes getDeviceAttributes();
+
+ Event save(Event event);
+
+ Optional<Event> saveIfNotExists(Event event);
+
+ Optional<Event> findEvent(String eventType, String eventUid);
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleException.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleException.java
new file mode 100644
index 0000000..7cc3bd4
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleException.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 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.extensions.api.rules;
+
+public class RuleException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public RuleException(String msg) {
+ super(msg);
+ }
+
+ public RuleException(String msg, Exception e) {
+ super(msg, e);
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleFilter.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleFilter.java
new file mode 100644
index 0000000..0f39f92
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleFilter.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 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.extensions.api.rules;
+
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.extensions.api.component.ConfigurableComponent;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface RuleFilter<T> extends ConfigurableComponent<T>, RuleLifecycleComponent {
+
+ boolean filter(RuleContext ctx, ToDeviceActorMsg msg);
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleInitializationException.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleInitializationException.java
new file mode 100644
index 0000000..8cf19e5
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleInitializationException.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.extensions.api.rules;
+
+public class RuleInitializationException extends RuleException {
+
+ private static final long serialVersionUID = 1L;
+
+ public RuleInitializationException(String msg, Exception e) {
+ super(msg, e);
+ }
+
+ public RuleInitializationException(String msg) {
+ super(msg, null);
+ }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleLifecycleComponent.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleLifecycleComponent.java
new file mode 100644
index 0000000..4eb38d2
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleLifecycleComponent.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.extensions.api.rules;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface RuleLifecycleComponent {
+
+ void resume();
+
+ void suspend();
+
+ void stop();
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessingMetaData.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessingMetaData.java
new file mode 100644
index 0000000..8f528a9
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessingMetaData.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016 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.extensions.api.rules;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+public class RuleProcessingMetaData {
+
+ private final Map<String, Object> md;
+
+ public RuleProcessingMetaData() {
+ super();
+ this.md = new HashMap<>();
+ }
+
+ public <T> void put(String key, T value) {
+ md.put(key, value);
+ }
+
+ public <T> Optional<T> get(String key) {
+ return Optional.ofNullable((T) md.get(key));
+ }
+
+ public Map<String, Object> getValues() {
+ return Collections.unmodifiableMap(md);
+ }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessor.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessor.java
new file mode 100644
index 0000000..179204f
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessor.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 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.extensions.api.rules;
+
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.extensions.api.component.ConfigurableComponent;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface RuleProcessor<T> extends ConfigurableComponent<T>, RuleLifecycleComponent {
+
+ RuleProcessingMetaData process(RuleContext ctx, ToDeviceActorMsg msg) throws RuleException;
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/SimpleRuleLifecycleComponent.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/SimpleRuleLifecycleComponent.java
new file mode 100644
index 0000000..7da902d
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/SimpleRuleLifecycleComponent.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016 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.extensions.api.rules;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public abstract class SimpleRuleLifecycleComponent implements RuleLifecycleComponent {
+
+ @Override
+ public void resume() {
+ log.debug("Resume method was called, but no impl provided!");
+ }
+
+ @Override
+ public void suspend() {
+ log.debug("Suspend method was called, but no impl provided!");
+ }
+
+ @Override
+ public void stop() {
+ log.debug("Stop method was called, but no impl provided!");
+ }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/ToRuleActorMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/ToRuleActorMsg.java
new file mode 100644
index 0000000..57ac998
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/ToRuleActorMsg.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 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.extensions.api.rules;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
+
+public interface ToRuleActorMsg extends TenantAwareMsg {
+
+ RuleId getRuleId();
+
+}
diff --git a/extensions-api/src/main/resources/EmptyJsonDescriptor.json b/extensions-api/src/main/resources/EmptyJsonDescriptor.json
new file mode 100644
index 0000000..03bf145
--- /dev/null
+++ b/extensions-api/src/main/resources/EmptyJsonDescriptor.json
@@ -0,0 +1,12 @@
+{
+ "schema": {
+ "description": "Empty Schema",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ }
+ },
+ "form": [
+ "*"
+ ]
+}
\ No newline at end of file
extensions-core/pom.xml 131(+131 -0)
diff --git a/extensions-core/pom.xml b/extensions-core/pom.xml
new file mode 100644
index 0000000..b2e2d18
--- /dev/null
+++ b/extensions-core/pom.xml
@@ -0,0 +1,131 @@
+<!--
+
+ Copyright © 2016 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>0.0.1-SNAPSHOT</version>
+ <artifactId>server</artifactId>
+ </parent>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>extensions-core</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Thingsboard Server Core Extensions</name>
+ <url>http://thingsboard.org</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <main.dir>${basedir}/..</main.dir>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>extensions-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.velocity</groupId>
+ <artifactId>velocity</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.velocity</groupId>
+ <artifactId>velocity-tools</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-context-support</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>javax.mail</groupId>
+ <artifactId>mail</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.protobuf</groupId>
+ <artifactId>protobuf-java</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>log4j-over-slf4j</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-dependency-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.xolstice.maven.plugins</groupId>
+ <artifactId>protobuf-maven-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>build-helper-maven-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailAction.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailAction.java
new file mode 100644
index 0000000..33b1333
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailAction.java
@@ -0,0 +1,109 @@
+/**
+ * Copyright © 2016 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.extensions.core.action.mail;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.velocity.Template;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.extensions.api.component.Action;
+import org.thingsboard.server.extensions.api.plugins.PluginAction;
+import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.api.rules.RuleProcessingMetaData;
+import org.thingsboard.server.extensions.api.rules.SimpleRuleLifecycleComponent;
+import org.thingsboard.server.extensions.core.utils.VelocityUtils;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Action(name = "Send Mail Action", descriptor = "SendMailActionDescriptor.json", configuration = SendMailActionConfiguration.class)
+@Slf4j
+public class SendMailAction extends SimpleRuleLifecycleComponent implements PluginAction<SendMailActionConfiguration> {
+
+ private SendMailActionConfiguration configuration;
+ private Optional<Template> fromTemplate;
+ private Optional<Template> toTemplate;
+ private Optional<Template> ccTemplate;
+ private Optional<Template> bccTemplate;
+ private Optional<Template> subjectTemplate;
+ private Optional<Template> bodyTemplate;
+
+ @Override
+ public void init(SendMailActionConfiguration configuration) {
+ this.configuration = configuration;
+ try {
+ fromTemplate = toTemplate(configuration.getFromTemplate(), "From Template");
+ toTemplate = toTemplate(configuration.getToTemplate(), "To Template");
+ ccTemplate = toTemplate(configuration.getCcTemplate(), "Cc Template");
+ bccTemplate = toTemplate(configuration.getBccTemplate(), "Bcc Template");
+ subjectTemplate = toTemplate(configuration.getSubjectTemplate(), "Subject Template");
+ bodyTemplate = toTemplate(configuration.getBodyTemplate(), "Body Template");
+ } catch (ParseException e) {
+ log.error("Failed to create templates based on provided configuration!", e);
+ throw new RuntimeException("Failed to create templates based on provided configuration!", e);
+ }
+ }
+
+ private Optional<Template> toTemplate(String source, String name) throws ParseException {
+ if (!StringUtils.isEmpty(source)) {
+ return Optional.of(VelocityUtils.create(source, name));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public Optional<RuleToPluginMsg<?>> convert(RuleContext ctx, ToDeviceActorMsg toDeviceActorMsg, RuleProcessingMetaData metadata) {
+ String sendFlag = configuration.getSendFlag();
+ if (StringUtils.isEmpty(sendFlag) || (Boolean) metadata.get(sendFlag).orElse(Boolean.FALSE)) {
+ VelocityContext context = VelocityUtils.createContext(metadata);
+
+ SendMailActionMsg.SendMailActionMsgBuilder builder = SendMailActionMsg.builder();
+ fromTemplate.ifPresent(t -> builder.from(VelocityUtils.merge(t, context)));
+ toTemplate.ifPresent(t -> builder.to(VelocityUtils.merge(t, context)));
+ ccTemplate.ifPresent(t -> builder.cc(VelocityUtils.merge(t, context)));
+ bccTemplate.ifPresent(t -> builder.bcc(VelocityUtils.merge(t, context)));
+ subjectTemplate.ifPresent(t -> builder.subject(VelocityUtils.merge(t, context)));
+ bodyTemplate.ifPresent(t -> builder.body(VelocityUtils.merge(t, context)));
+ return Optional.of(new SendMailRuleToPluginActionMsg(toDeviceActorMsg.getTenantId(), toDeviceActorMsg.getCustomerId(), toDeviceActorMsg.getDeviceId(),
+ builder.build()));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public Optional<ToDeviceMsg> convert(PluginToRuleMsg<?> response) {
+ if (response instanceof ResponsePluginToRuleMsg) {
+ return Optional.of(((ResponsePluginToRuleMsg) response).getPayload());
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public boolean isOneWayAction() {
+ return true;
+ }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailActionConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailActionConfiguration.java
new file mode 100644
index 0000000..bec33b9
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailActionConfiguration.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016 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.extensions.core.action.mail;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class SendMailActionConfiguration {
+
+ private String sendFlag;
+
+ private String fromTemplate;
+ private String toTemplate;
+ private String ccTemplate;
+ private String bccTemplate;
+ private String subjectTemplate;
+ private String bodyTemplate;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailActionMsg.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailActionMsg.java
new file mode 100644
index 0000000..979dfe0
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailActionMsg.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 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.extensions.core.action.mail;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+@Builder
+public class SendMailActionMsg implements Serializable {
+
+ private final String from;
+ private final String to;
+ private final String cc;
+ private final String bcc;
+ private final String subject;
+ private final String body;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailRuleToPluginActionMsg.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailRuleToPluginActionMsg.java
new file mode 100644
index 0000000..ab120ad
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailRuleToPluginActionMsg.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 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.extensions.core.action.mail;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class SendMailRuleToPluginActionMsg extends AbstractRuleToPluginMsg<SendMailActionMsg> {
+
+ public SendMailRuleToPluginActionMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId,
+ SendMailActionMsg payload) {
+ super(tenantId, customerId, deviceId, payload);
+ }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/rpc/RpcPluginAction.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/rpc/RpcPluginAction.java
new file mode 100644
index 0000000..e23e61e
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/rpc/RpcPluginAction.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright © 2016 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.extensions.core.action.rpc;
+
+import org.thingsboard.server.common.msg.core.ToServerRpcRequestMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.extensions.api.component.Action;
+import org.thingsboard.server.extensions.api.component.EmptyComponentConfiguration;
+import org.thingsboard.server.extensions.api.plugins.PluginAction;
+import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RpcRequestRuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RpcResponsePluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.api.rules.RuleProcessingMetaData;
+import org.thingsboard.server.extensions.api.rules.SimpleRuleLifecycleComponent;
+
+import java.util.Optional;
+
+@Action(name = "RPC Plugin Action")
+public class RpcPluginAction extends SimpleRuleLifecycleComponent implements PluginAction<EmptyComponentConfiguration> {
+
+ public void init(EmptyComponentConfiguration configuration) {
+ }
+
+ @Override
+ public Optional<RuleToPluginMsg<?>> convert(RuleContext ctx, ToDeviceActorMsg toDeviceActorMsg, RuleProcessingMetaData deviceMsgMd) {
+ FromDeviceMsg msg = toDeviceActorMsg.getPayload();
+ if (msg.getMsgType() == MsgType.TO_SERVER_RPC_REQUEST) {
+ ToServerRpcRequestMsg payload = (ToServerRpcRequestMsg) msg;
+ return Optional.of(new RpcRequestRuleToPluginMsg(toDeviceActorMsg.getTenantId(), toDeviceActorMsg.getCustomerId(),
+ toDeviceActorMsg.getDeviceId(), payload));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public Optional<ToDeviceMsg> convert(PluginToRuleMsg<?> response) {
+ if (response instanceof RpcResponsePluginToRuleMsg) {
+ return Optional.of(((RpcResponsePluginToRuleMsg) response).getPayload());
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public boolean isOneWayAction() {
+ return false;
+ }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/telemetry/TelemetryPluginAction.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/telemetry/TelemetryPluginAction.java
new file mode 100644
index 0000000..cfbccd9
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/telemetry/TelemetryPluginAction.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright © 2016 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.extensions.core.action.telemetry;
+
+import org.thingsboard.server.common.msg.core.GetAttributesRequest;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.msg.core.UpdateAttributesRequest;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.extensions.api.component.Action;
+import org.thingsboard.server.extensions.api.component.EmptyComponentConfiguration;
+import org.thingsboard.server.extensions.api.plugins.PluginAction;
+import org.thingsboard.server.extensions.api.plugins.msg.*;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.api.rules.RuleProcessingMetaData;
+import org.thingsboard.server.extensions.api.rules.SimpleRuleLifecycleComponent;
+
+import java.util.Optional;
+
+@Action(name = "Telemetry Plugin Action")
+public class TelemetryPluginAction extends SimpleRuleLifecycleComponent implements PluginAction<EmptyComponentConfiguration> {
+
+ public void init(EmptyComponentConfiguration configuration) {
+ }
+
+ @Override
+ public Optional<RuleToPluginMsg<?>> convert(RuleContext ctx, ToDeviceActorMsg toDeviceActorMsg, RuleProcessingMetaData deviceMsgMd) {
+ FromDeviceMsg msg = toDeviceActorMsg.getPayload();
+ if (msg.getMsgType() == MsgType.POST_TELEMETRY_REQUEST) {
+ TelemetryUploadRequest payload = (TelemetryUploadRequest) msg;
+ return Optional.of(new TelemetryUploadRequestRuleToPluginMsg(toDeviceActorMsg.getTenantId(), toDeviceActorMsg.getCustomerId(),
+ toDeviceActorMsg.getDeviceId(), payload));
+ } else if (msg.getMsgType() == MsgType.POST_ATTRIBUTES_REQUEST) {
+ UpdateAttributesRequest payload = (UpdateAttributesRequest) msg;
+ return Optional.of(new UpdateAttributesRequestRuleToPluginMsg(toDeviceActorMsg.getTenantId(), toDeviceActorMsg.getCustomerId(),
+ toDeviceActorMsg.getDeviceId(), payload));
+ } else if (msg.getMsgType() == MsgType.GET_ATTRIBUTES_REQUEST) {
+ GetAttributesRequest payload = (GetAttributesRequest) msg;
+ return Optional.of(new GetAttributesRequestRuleToPluginMsg(toDeviceActorMsg.getTenantId(), toDeviceActorMsg.getCustomerId(),
+ toDeviceActorMsg.getDeviceId(), payload));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public Optional<ToDeviceMsg> convert(PluginToRuleMsg<?> response) {
+ if (response instanceof ResponsePluginToRuleMsg) {
+ return Optional.of(((ResponsePluginToRuleMsg) response).getPayload());
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public boolean isOneWayAction() {
+ return false;
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/template/AbstractTemplatePluginAction.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/template/AbstractTemplatePluginAction.java
new file mode 100644
index 0000000..d427170
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/template/AbstractTemplatePluginAction.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright © 2016 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.extensions.core.action.template;
+
+import org.apache.velocity.Template;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.extensions.api.plugins.PluginAction;
+import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.api.rules.RuleProcessingMetaData;
+import org.thingsboard.server.extensions.api.rules.SimpleRuleLifecycleComponent;
+import org.thingsboard.server.extensions.core.utils.VelocityUtils;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+public abstract class AbstractTemplatePluginAction<T extends TemplateActionConfiguration> extends SimpleRuleLifecycleComponent implements PluginAction<T> {
+ protected T configuration;
+ protected Template template;
+
+ @Override
+ public void init(T configuration) {
+ this.configuration = configuration;
+ try {
+ this.template = VelocityUtils.create(configuration.getTemplate(), "Template");
+ } catch (ParseException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public Optional<RuleToPluginMsg<?>> convert(RuleContext ctx, ToDeviceActorMsg msg, RuleProcessingMetaData deviceMsgMd) {
+ FromDeviceRequestMsg payload;
+ if (msg.getPayload() instanceof FromDeviceRequestMsg) {
+ payload = (FromDeviceRequestMsg) msg.getPayload();
+ } else {
+ throw new IllegalArgumentException("Action does not support messages of type: " + msg.getPayload().getMsgType());
+ }
+ return buildRuleToPluginMsg(ctx, msg, payload);
+ }
+
+ @Override
+ public Optional<ToDeviceMsg> convert(PluginToRuleMsg<?> response) {
+ if (response instanceof ResponsePluginToRuleMsg) {
+ return Optional.of(((ResponsePluginToRuleMsg) response).getPayload());
+ }
+ return Optional.empty();
+ }
+
+ protected String getMsgBody(RuleContext ctx, ToDeviceActorMsg msg) {
+ VelocityContext context = VelocityUtils.createContext(ctx.getDeviceAttributes(), msg.getPayload());
+ return VelocityUtils.merge(template, context);
+ }
+
+ abstract protected Optional<RuleToPluginMsg<?>> buildRuleToPluginMsg(RuleContext ctx,
+ ToDeviceActorMsg msg,
+ FromDeviceRequestMsg payload);
+
+ @Override
+ public boolean isOneWayAction() {
+ return !configuration.isSync();
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/template/TemplateActionConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/template/TemplateActionConfiguration.java
new file mode 100644
index 0000000..d9caa97
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/template/TemplateActionConfiguration.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 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.extensions.core.action.template;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface TemplateActionConfiguration {
+
+ boolean isSync();
+ String getTemplate();
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/BasicJsFilter.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/BasicJsFilter.java
new file mode 100644
index 0000000..5abfe92
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/BasicJsFilter.java
@@ -0,0 +1,92 @@
+/**
+ * Copyright © 2016 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.extensions.core.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.api.rules.RuleFilter;
+
+import javax.script.ScriptException;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public abstract class BasicJsFilter implements RuleFilter<JsFilterConfiguration> {
+
+ protected JsFilterConfiguration configuration;
+ protected NashornJsEvaluator evaluator;
+
+ @Override
+ public void init(JsFilterConfiguration configuration) {
+ this.configuration = configuration;
+ initEvaluator(configuration);
+ }
+
+ @Override
+ public boolean filter(RuleContext ctx, ToDeviceActorMsg msg) {
+ try {
+ return doFilter(ctx, msg);
+ } catch (ScriptException e) {
+ log.warn("RuleFilter evaluation exception: {}", e.getMessage(), e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected abstract boolean doFilter(RuleContext ctx, ToDeviceActorMsg msg) throws ScriptException;
+
+ @Override
+ public void resume() {
+ initEvaluator(configuration);
+ }
+
+ @Override
+ public void suspend() {
+ destroyEvaluator();
+ }
+
+ @Override
+ public void stop() {
+ destroyEvaluator();
+ }
+
+ private void initEvaluator(JsFilterConfiguration configuration) {
+ evaluator = new NashornJsEvaluator(configuration.getFilter());
+ }
+
+ private void destroyEvaluator() {
+ if (evaluator != null) {
+ evaluator.destroy();
+ }
+ }
+
+ protected static Object getValue(KvEntry attr) {
+ switch (attr.getDataType()) {
+ case STRING:
+ return attr.getStrValue().get();
+ case LONG:
+ return attr.getLongValue().get();
+ case DOUBLE:
+ return attr.getDoubleValue().get();
+ case BOOLEAN:
+ return attr.getBooleanValue().get();
+ }
+ return null;
+ }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilter.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilter.java
new file mode 100644
index 0000000..35ba20e
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilter.java
@@ -0,0 +1,93 @@
+/**
+ * Copyright © 2016 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.extensions.core.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.msg.core.UpdateAttributesRequest;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.extensions.api.component.Filter;
+import org.thingsboard.server.extensions.api.device.DeviceAttributes;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+
+import javax.script.Bindings;
+import javax.script.ScriptException;
+import javax.script.SimpleBindings;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Filter(name = "Device Attributes Filter", descriptor = "JsFilterDescriptor.json", configuration = JsFilterConfiguration.class)
+@Slf4j
+public class DeviceAttributesFilter extends BasicJsFilter {
+
+ public static final String CLIENT_SIDE = "cs";
+ public static final String SERVER_SIDE = "ss";
+ public static final String SHARED = "shared";
+
+ @Override
+ protected boolean doFilter(RuleContext ctx, ToDeviceActorMsg msg) throws ScriptException {
+ return evaluator.execute(toBindings(ctx.getDeviceAttributes(), msg != null ? msg.getPayload() : null));
+ }
+
+ private Bindings toBindings(DeviceAttributes attributes, FromDeviceMsg msg) {
+ Bindings bindings = new SimpleBindings();
+ convertListEntries(bindings, CLIENT_SIDE, attributes.getClientSideAttributes());
+ convertListEntries(bindings, SERVER_SIDE, attributes.getServerSideAttributes());
+ convertListEntries(bindings, SHARED, attributes.getServerSidePublicAttributes());
+
+ if (msg != null) {
+ switch (msg.getMsgType()) {
+ case POST_ATTRIBUTES_REQUEST:
+ updateBindings(bindings, (UpdateAttributesRequest) msg);
+ break;
+ }
+ }
+
+ return bindings;
+ }
+
+ private void updateBindings(Bindings bindings, UpdateAttributesRequest msg) {
+ Map<String, Object> attrMap = (Map<String, Object>) bindings.get(CLIENT_SIDE);
+ for (AttributeKvEntry attr : msg.getAttributes()) {
+ if (!CLIENT_SIDE.equalsIgnoreCase(attr.getKey()) && !SERVER_SIDE.equalsIgnoreCase(attr.getKey())
+ && !SHARED.equalsIgnoreCase(attr.getKey())) {
+ bindings.put(attr.getKey(), getValue(attr));
+ }
+ attrMap.put(attr.getKey(), getValue(attr));
+ }
+ bindings.put(CLIENT_SIDE, attrMap);
+ }
+
+ public static Bindings convertListEntries(Bindings bindings, String attributesVarName, Collection<AttributeKvEntry> attributes) {
+ Map<String, Object> attrMap = new HashMap<>();
+ for (AttributeKvEntry attr : attributes) {
+ if (!CLIENT_SIDE.equalsIgnoreCase(attr.getKey()) && !SERVER_SIDE.equalsIgnoreCase(attr.getKey())
+ && !SHARED.equalsIgnoreCase(attr.getKey())) {
+ bindings.put(attr.getKey(), getValue(attr));
+ }
+ attrMap.put(attr.getKey(), getValue(attr));
+ }
+ bindings.put(attributesVarName, attrMap);
+ return bindings;
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterConfiguration.java
new file mode 100644
index 0000000..10c0044
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterConfiguration.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 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.extensions.core.filter;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class DeviceAttributesFilterConfiguration {
+
+ private String filterBody;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceTelemetryFilter.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceTelemetryFilter.java
new file mode 100644
index 0000000..ea6df9d
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceTelemetryFilter.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright © 2016 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.extensions.core.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.extensions.api.component.Filter;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+
+import javax.script.Bindings;
+import javax.script.ScriptException;
+import javax.script.SimpleBindings;
+import java.util.List;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Filter(name = "Device Telemetry Filter", descriptor = "JsFilterDescriptor.json", configuration = JsFilterConfiguration.class)
+@Slf4j
+public class DeviceTelemetryFilter extends BasicJsFilter {
+
+ @Override
+ protected boolean doFilter(RuleContext ctx, ToDeviceActorMsg msg) throws ScriptException {
+ FromDeviceMsg deviceMsg = msg.getPayload();
+ if (deviceMsg instanceof TelemetryUploadRequest) {
+ TelemetryUploadRequest telemetryMsg = (TelemetryUploadRequest) deviceMsg;
+ for (List<KvEntry> entries : telemetryMsg.getData().values()) {
+ if (evaluator.execute(toBindings(entries))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private Bindings toBindings(List<KvEntry> entries) {
+ Bindings bindings = new SimpleBindings();
+ for (KvEntry entry : entries) {
+ bindings.put(entry.getKey(), getValue(entry));
+ }
+ return bindings;
+ }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/JsFilterConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/JsFilterConfiguration.java
new file mode 100644
index 0000000..3b64a80
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/JsFilterConfiguration.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 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.extensions.core.filter;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class JsFilterConfiguration {
+
+ private final String filter;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java
new file mode 100644
index 0000000..21180d7
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright © 2016 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.extensions.core.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.msg.core.ToServerRpcRequestMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.extensions.api.component.Filter;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.api.rules.RuleFilter;
+import org.thingsboard.server.extensions.api.rules.SimpleRuleLifecycleComponent;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.thingsboard.server.common.msg.session.MsgType.TO_SERVER_RPC_REQUEST;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Filter(name = "Method Name Filter", descriptor = "MethodNameFilterDescriptor.json", configuration = MethodNameFilterConfiguration.class)
+@Slf4j
+public class MethodNameFilter extends SimpleRuleLifecycleComponent implements RuleFilter<MethodNameFilterConfiguration> {
+
+ private Set<String> methods;
+
+ @Override
+ public void init(MethodNameFilterConfiguration configuration) {
+ methods = Arrays.asList(configuration.getMethodNames()).stream().map(m -> m.getName()).collect(Collectors.toSet());
+ }
+
+ @Override
+ public boolean filter(RuleContext ctx, ToDeviceActorMsg msg) {
+ if (msg.getPayload().getMsgType() == TO_SERVER_RPC_REQUEST) {
+ return methods.contains(((ToServerRpcRequestMsg) msg.getPayload()).getMethod());
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilterConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilterConfiguration.java
new file mode 100644
index 0000000..8352e9e
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilterConfiguration.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016 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.extensions.core.filter;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class MethodNameFilterConfiguration {
+
+ private MethodName[] methodNames;
+
+ @Data
+ public static class MethodName {
+ private String name;
+ }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java
new file mode 100644
index 0000000..84deea5
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright © 2016 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.extensions.core.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.extensions.api.component.Filter;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.api.rules.RuleFilter;
+import org.thingsboard.server.extensions.api.rules.SimpleRuleLifecycleComponent;
+
+import java.security.InvalidParameterException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Filter(name = "Message Type Filter", descriptor = "MsgTypeFilterDescriptor.json", configuration = MsgTypeFilterConfiguration.class)
+@Slf4j
+public class MsgTypeFilter extends SimpleRuleLifecycleComponent implements RuleFilter<MsgTypeFilterConfiguration> {
+
+ private List<MsgType> msgTypes;
+
+ @Override
+ public void init(MsgTypeFilterConfiguration configuration) {
+ msgTypes = Arrays.asList(configuration.getMessageTypes()).stream().map(type -> {
+ switch (type) {
+ case "GET_ATTRIBUTES":
+ return MsgType.GET_ATTRIBUTES_REQUEST;
+ case "POST_ATTRIBUTES":
+ return MsgType.POST_ATTRIBUTES_REQUEST;
+ case "POST_TELEMETRY":
+ return MsgType.POST_TELEMETRY_REQUEST;
+ case "RPC_REQUEST":
+ return MsgType.TO_SERVER_RPC_REQUEST;
+ default:
+ throw new InvalidParameterException("Can't map " + type + " to " + MsgType.class.getName() + "!");
+ }
+ }).collect(Collectors.toList());
+ }
+
+ @Override
+ public boolean filter(RuleContext ctx, ToDeviceActorMsg msg) {
+ for (MsgType msgType : msgTypes) {
+ if (msgType == msg.getPayload().getMsgType()) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilterConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilterConfiguration.java
new file mode 100644
index 0000000..3053188
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilterConfiguration.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 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.extensions.core.filter;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class MsgTypeFilterConfiguration {
+
+ private String[] messageTypes;
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/NashornJsEvaluator.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/NashornJsEvaluator.java
new file mode 100644
index 0000000..365aa4c
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/NashornJsEvaluator.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 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.extensions.core.filter;
+
+import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.script.*;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class NashornJsEvaluator {
+
+ private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
+
+ private CompiledScript engine;
+
+ public NashornJsEvaluator(String script) {
+ engine = compileScript(script);
+ }
+
+ private static CompiledScript compileScript(String script) {
+ ScriptEngine engine = factory.getScriptEngine(new String[]{"--no-java"});
+ Compilable compEngine = (Compilable) engine;
+ try {
+ return compEngine.compile(script);
+ } catch (ScriptException e) {
+ log.warn("Failed to compile filter script: {}", e.getMessage(), e);
+ throw new IllegalArgumentException("Can't compile script: " + e.getMessage());
+ }
+ }
+
+ public Boolean execute(Bindings bindings) throws ScriptException {
+ Object eval = engine.eval(bindings);
+ if (eval instanceof Boolean) {
+ return (Boolean) eval;
+ } else {
+ log.warn("Wrong result type: {}", eval);
+ throw new ScriptException("Wrong result type: " + eval);
+ }
+ }
+
+ public void destroy() {
+ engine = null;
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/KeyValuePluginProperties.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/KeyValuePluginProperties.java
new file mode 100644
index 0000000..f0ceefc
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/KeyValuePluginProperties.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class KeyValuePluginProperties {
+ private String key;
+ private String value;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java
new file mode 100644
index 0000000..52fd2e9
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java
@@ -0,0 +1,121 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.mail;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.mail.javamail.JavaMailSenderImpl;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.component.Plugin;
+import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+import org.thingsboard.server.extensions.core.action.mail.SendMailAction;
+import org.thingsboard.server.extensions.core.action.mail.SendMailActionMsg;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+import java.util.Properties;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Plugin(name = "Mail Plugin", actions = {SendMailAction.class}, descriptor = "MailPluginDescriptor.json", configuration = MailPluginConfiguration.class)
+@Slf4j
+public class MailPlugin extends AbstractPlugin<MailPluginConfiguration> implements RuleMsgHandler {
+
+ private MailPluginConfiguration configuration;
+ private JavaMailSenderImpl mailSender;
+
+ @Override
+ public void init(MailPluginConfiguration configuration) {
+ log.info("Initializing plugin using configuration {}", configuration);
+ this.configuration = configuration;
+ initMailSender(configuration);
+ }
+
+ @Override
+ public void resume(PluginContext ctx) {
+ initMailSender(configuration);
+ }
+
+ @Override
+ public void suspend(PluginContext ctx) {
+ mailSender = null;
+ }
+
+ @Override
+ public void stop(PluginContext ctx) {
+ mailSender = null;
+ }
+
+ private void initMailSender(MailPluginConfiguration configuration) {
+ JavaMailSenderImpl mail = new JavaMailSenderImpl();
+ mail.setHost(configuration.getHost());
+ mail.setPort(configuration.getPort());
+ mail.setUsername(configuration.getUsername());
+ mail.setPassword(configuration.getPassword());
+ if (configuration.getOtherProperties() != null) {
+ Properties mailProperties = new Properties();
+ configuration.getOtherProperties()
+ .stream().forEach(p -> mailProperties.put(p.getKey(), p.getValue()));
+ mail.setJavaMailProperties(mailProperties);
+ }
+ mailSender = mail;
+ }
+
+ @Override
+ public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
+ if (msg.getPayload() instanceof SendMailActionMsg) {
+ try {
+ sendMail((SendMailActionMsg) msg.getPayload());
+ } catch (Exception e) {
+ log.warn("Failed to send email", e);
+ throw new RuleException("Failed to send email", e);
+ }
+ } else {
+ throw new RuntimeException("Not supported msg type: " + msg.getPayload().getClass() + "!");
+ }
+ }
+
+ private void sendMail(SendMailActionMsg msg) throws MessagingException {
+ log.debug("Sending mail {}", msg);
+ MimeMessage mailMsg = mailSender.createMimeMessage();
+ MimeMessageHelper helper = new MimeMessageHelper(mailMsg, "UTF-8");
+ helper.setFrom(msg.getFrom());
+ helper.setTo(msg.getTo());
+ if (!StringUtils.isEmpty(msg.getCc())) {
+ helper.setCc(msg.getCc());
+ }
+ if (!StringUtils.isEmpty(msg.getBcc())) {
+ helper.setBcc(msg.getBcc());
+ }
+ helper.setSubject(msg.getSubject());
+ helper.setText(msg.getBody());
+ mailSender.send(helper.getMimeMessage());
+ log.debug("Mail sent {}", msg);
+ }
+
+ @Override
+ protected RuleMsgHandler getRuleMsgHandler() {
+ return this;
+ }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPluginConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPluginConfiguration.java
new file mode 100644
index 0000000..38eb4fc
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPluginConfiguration.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.mail;
+
+import lombok.Data;
+import org.thingsboard.server.extensions.core.plugin.KeyValuePluginProperties;
+
+import java.util.List;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class MailPluginConfiguration {
+ private String host;
+ private Integer port;
+ private String username;
+ private String password;
+ private List<KeyValuePluginProperties> otherProperties;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPlugin.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPlugin.java
new file mode 100644
index 0000000..ef7d612
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPlugin.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.messaging;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.extensions.api.component.Plugin;
+import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
+import org.thingsboard.server.extensions.core.action.rpc.RpcPluginAction;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Plugin(name = "Device Messaging Plugin", actions = {RpcPluginAction.class},
+ descriptor = "DeviceMessagingPluginDescriptor.json", configuration = DeviceMessagingPluginConfiguration.class)
+@Slf4j
+public class DeviceMessagingPlugin extends AbstractPlugin<DeviceMessagingPluginConfiguration> {
+
+ private DeviceMessagingRuleMsgHandler ruleHandler;
+
+ public DeviceMessagingPlugin() {
+ ruleHandler = new DeviceMessagingRuleMsgHandler();
+ }
+
+ @Override
+ public void init(DeviceMessagingPluginConfiguration configuration) {
+ ruleHandler.setConfiguration(configuration);
+ }
+
+ @Override
+ public void process(PluginContext ctx, FromDeviceRpcResponse msg) {
+ ruleHandler.process(ctx, msg);
+ }
+
+ @Override
+ protected RuleMsgHandler getRuleMsgHandler() {
+ return ruleHandler;
+ }
+
+ @Override
+ public void resume(PluginContext ctx) {
+
+ }
+
+ @Override
+ public void suspend(PluginContext ctx) {
+
+ }
+
+ @Override
+ public void stop(PluginContext ctx) {
+
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPluginConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPluginConfiguration.java
new file mode 100644
index 0000000..62580ed
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPluginConfiguration.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.messaging;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class DeviceMessagingPluginConfiguration {
+
+ private int maxDeviceCountPerCustomer;
+ private long defaultTimeout;
+ private long maxTimeout;
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java
new file mode 100644
index 0000000..d029170
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java
@@ -0,0 +1,228 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.messaging;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.ToServerRpcRequestMsg;
+import org.thingsboard.server.common.msg.core.ToServerRpcResponseMsg;
+import org.thingsboard.server.extensions.api.plugins.PluginCallback;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.msg.*;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class DeviceMessagingRuleMsgHandler implements RuleMsgHandler {
+
+ private static final Gson GSON = new Gson();
+
+ private static final String GET_DEVICE_LIST_METHOD_NAME = "getDevices";
+ private static final String SEND_MSG_METHOD_NAME = "sendMsg";
+ private static final String ON_MSG_METHOD_NAME = "onMsg";
+ private static final String ONEWAY = "oneway";
+ private static final String TIMEOUT = "timeout";
+ private static final String DEVICE_ID = "deviceId";
+
+ private Map<UUID, PendingRpcRequestMetadata> pendingMsgs = new HashMap<>();
+
+ @Setter
+ private DeviceMessagingPluginConfiguration configuration;
+
+ @Override
+ public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
+ if (msg.getPayload() instanceof ToServerRpcRequestMsg) {
+ ToServerRpcRequestMsg request = (ToServerRpcRequestMsg) msg.getPayload();
+ try {
+ PendingRpcRequestMetadata md = new PendingRpcRequestMetadata(msg.getUid(),
+ request.getRequestId(), tenantId, ruleId, msg.getCustomerId(), msg.getDeviceId());
+ switch (request.getMethod()) {
+ case GET_DEVICE_LIST_METHOD_NAME:
+ processGetDeviceList(ctx, md);
+ case SEND_MSG_METHOD_NAME:
+ processSendMsg(ctx, md, request);
+ break;
+ default:
+ throw new RuleException("Method " + request.getMethod() + " not supported!");
+ }
+ } catch (RuleException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new RuleException(e.getMessage(), e);
+ }
+ }
+ }
+
+ public void process(PluginContext ctx, FromDeviceRpcResponse msg) {
+ UUID requestId = msg.getId();
+ PendingRpcRequestMetadata pendindMsg = pendingMsgs.remove(requestId);
+ if (pendindMsg != null) {
+ log.trace("[{}] Received response: {}", requestId, msg);
+ ToServerRpcResponseMsg response;
+ if (msg.getError().isPresent()) {
+ response = new ToServerRpcResponseMsg(pendindMsg.getRequestId(), toJsonString(msg.getError().get()));
+ } else {
+ response = new ToServerRpcResponseMsg(pendindMsg.getRequestId(), msg.getResponse().orElse(""));
+ }
+ ctx.reply(new RpcResponsePluginToRuleMsg(
+ pendindMsg.getUid(), pendindMsg.getTenantId(), pendindMsg.getRuleId(), response));
+ } else {
+ log.trace("[{}] Received stale response: {}", requestId, msg);
+ }
+ }
+
+ private void processGetDeviceList(PluginContext ctx, PendingRpcRequestMetadata requestMd) {
+ CustomerId customerId = requestMd.getCustomerId();
+ if (!customerId.isNullUid()) {
+ ctx.getCustomerDevices(requestMd.getTenantId(), customerId, configuration.getMaxDeviceCountPerCustomer(), new PluginCallback<List<Device>>() {
+ @Override
+ public void onSuccess(PluginContext ctx, List<Device> devices) {
+ JsonArray deviceList = new JsonArray();
+ devices.stream().filter(device -> !requestMd.getDeviceId().equals(device.getId())).forEach(device -> {
+ JsonObject deviceJson = new JsonObject();
+ deviceJson.addProperty("id", device.getId().toString());
+ deviceJson.addProperty("name", device.getName());
+ deviceList.add(deviceJson);
+ });
+ ToServerRpcResponseMsg response = new ToServerRpcResponseMsg(requestMd.getRequestId(), GSON.toJson(deviceList));
+ ctx.reply(new RpcResponsePluginToRuleMsg(
+ requestMd.getUid(), requestMd.getTenantId(), requestMd.getRuleId(), response));
+ }
+
+ @Override
+ public void onFailure(PluginContext ctx, Exception e) {
+ replyWithError(ctx, requestMd, RpcError.INTERNAL);
+ }
+ });
+ } else {
+ replyWithError(ctx, requestMd, "Device is unassigned!");
+ }
+ }
+
+ private void processSendMsg(PluginContext ctx, PendingRpcRequestMetadata requestMd, ToServerRpcRequestMsg request) {
+ JsonObject params = new JsonParser().parse(request.getParams()).getAsJsonObject();
+ String targetDeviceIdStr = params.get(DEVICE_ID).getAsString();
+ DeviceId targetDeviceId = DeviceId.fromString(targetDeviceIdStr);
+ boolean oneWay = isOneWay(params);
+ long timeout = getTimeout(params);
+ if (timeout <= 0) {
+ replyWithError(ctx, requestMd, "Timeout can't be negative!");
+ } else if (timeout > configuration.getMaxTimeout()) {
+ replyWithError(ctx, requestMd, "Timeout is too large!");
+ } else {
+ ctx.getDevice(targetDeviceId, new PluginCallback<Device>() {
+ @Override
+ public void onSuccess(PluginContext ctx, Device targetDevice) {
+ UUID uid = UUID.randomUUID();
+ if (targetDevice == null) {
+ replyWithError(ctx, requestMd, RpcError.NOT_FOUND);
+ } else if (!requestMd.getCustomerId().isNullUid() &&
+ requestMd.getTenantId().equals(targetDevice.getTenantId())
+ && requestMd.getCustomerId().equals(targetDevice.getCustomerId())) {
+ pendingMsgs.put(uid, requestMd);
+ log.trace("[{}] Forwarding {} to [{}]", uid, params, targetDeviceId);
+ ToDeviceRpcRequestBody requestBody = new ToDeviceRpcRequestBody(ON_MSG_METHOD_NAME, GSON.toJson(params.get("body")));
+ ctx.sendRpcRequest(new ToDeviceRpcRequest(uid, targetDevice.getTenantId(), targetDeviceId, oneWay, System.currentTimeMillis() + timeout, requestBody));
+ } else {
+ replyWithError(ctx, requestMd, RpcError.FORBIDDEN);
+ }
+ }
+
+ @Override
+ public void onFailure(PluginContext ctx, Exception e) {
+ replyWithError(ctx, requestMd, RpcError.INTERNAL);
+ }
+ });
+ }
+ }
+
+ private boolean isOneWay(JsonObject params) {
+ boolean oneWay = false;
+ if (params.has(ONEWAY)) {
+ oneWay = params.get(ONEWAY).getAsBoolean();
+ }
+ return oneWay;
+ }
+
+ private long getTimeout(JsonObject params) {
+ long timeout;
+ if (params.has(TIMEOUT)) {
+ timeout = params.get(TIMEOUT).getAsLong();
+ } else {
+ timeout = configuration.getDefaultTimeout();
+ }
+ return timeout;
+ }
+
+ private void replyWithError(PluginContext ctx, PendingRpcRequestMetadata requestMd, RpcError error) {
+ replyWithErrorJson(ctx, requestMd, toJsonString(error));
+ }
+
+ private void replyWithError(PluginContext ctx, PendingRpcRequestMetadata requestMd, String error) {
+ replyWithErrorJson(ctx, requestMd, toJsonString(error));
+ }
+
+ private void replyWithErrorJson(PluginContext ctx, PendingRpcRequestMetadata requestMd, String error) {
+ ToServerRpcResponseMsg response = new ToServerRpcResponseMsg(requestMd.getRequestId(), error);
+ ctx.reply(new RpcResponsePluginToRuleMsg(
+ requestMd.getUid(), requestMd.getTenantId(), requestMd.getRuleId(), response));
+ }
+
+ private String toJsonString(String error) {
+ JsonObject errorObj = new JsonObject();
+ errorObj.addProperty("error", error);
+ return GSON.toJson(errorObj);
+ }
+
+ private String toJsonString(RpcError error) {
+ JsonObject errorObj = new JsonObject();
+ switch (error) {
+ case NOT_FOUND:
+ errorObj.addProperty("error", "Target device not found!");
+ break;
+ case NO_ACTIVE_CONNECTION:
+ errorObj.addProperty("error", "No active connection to remote device!");
+ break;
+ case TIMEOUT:
+ errorObj.addProperty("error", "Timeout while waiting response from device!");
+ break;
+ case FORBIDDEN:
+ errorObj.addProperty("error", "This action is not allowed! Devices are unassigned or assigned to different customers!");
+ break;
+ case INTERNAL:
+ errorObj.addProperty("error", "Internal server error!");
+ break;
+ }
+ return GSON.toJson(errorObj);
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/PendingRpcRequestMetadata.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/PendingRpcRequestMetadata.java
new file mode 100644
index 0000000..fef27c1
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/PendingRpcRequestMetadata.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.messaging;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+class PendingRpcRequestMetadata {
+ private final UUID uid;
+ private final int requestId;
+ private final TenantId tenantId;
+ private final RuleId ruleId;
+ private final CustomerId customerId;
+ private final DeviceId deviceId;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/cmd/RpcRequest.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/cmd/RpcRequest.java
new file mode 100644
index 0000000..7996823
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/cmd/RpcRequest.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.rpc.cmd;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class RpcRequest {
+ private final String methodName;
+ private final String requestData;
+ private Long timeout;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java
new file mode 100644
index 0000000..ba50c32
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java
@@ -0,0 +1,127 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.rpc.handlers;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.DefaultRestMsgHandler;
+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.ToDeviceRpcRequest;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestBody;
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+import org.thingsboard.server.extensions.api.plugins.rest.RestRequest;
+import org.thingsboard.server.extensions.core.plugin.rpc.LocalRequestMetaData;
+import org.thingsboard.server.extensions.core.plugin.rpc.RpcManager;
+import org.thingsboard.server.extensions.core.plugin.rpc.cmd.RpcRequest;
+
+import javax.servlet.ServletException;
+import java.io.IOException;
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class RpcRestMsgHandler extends DefaultRestMsgHandler {
+
+ private final RpcManager rpcManager;
+ @Setter
+ private long defaultTimeout;
+
+ @Override
+ public void handleHttpPostRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
+ boolean valid = false;
+ RestRequest request = msg.getRequest();
+ try {
+ String[] pathParams = request.getPathParams();
+ if (pathParams.length == 2) {
+ String method = pathParams[0].toUpperCase();
+ if (DataConstants.ONEWAY.equals(method) || DataConstants.TWOWAY.equals(method)) {
+ DeviceId deviceId = DeviceId.fromString(pathParams[1]);
+ if (!ctx.checkAccess(deviceId)) {
+ msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
+ return;
+ }
+ JsonNode rpcRequestBody = jsonMapper.readTree(request.getRequestBody());
+
+ RpcRequest cmd = new RpcRequest(rpcRequestBody.get("method").asText(),
+ jsonMapper.writeValueAsString(rpcRequestBody.get("params")));
+ if (rpcRequestBody.has("timeout")) {
+ cmd.setTimeout(rpcRequestBody.get("timeout").asLong());
+ }
+ long timeout = cmd.getTimeout() != null ? cmd.getTimeout() : defaultTimeout;
+ ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(cmd.getMethodName(), cmd.getRequestData());
+ ToDeviceRpcRequest rpcRequest = new ToDeviceRpcRequest(UUID.randomUUID(),
+ ctx.getSecurityCtx().orElseThrow(() -> new IllegalStateException("Security context is empty!")).getTenantId(),
+ deviceId,
+ DataConstants.ONEWAY.equals(method),
+ System.currentTimeMillis() + timeout,
+ body
+ );
+ rpcManager.process(ctx, new LocalRequestMetaData(rpcRequest, msg.getResponseHolder()));
+ valid = true;
+ }
+ }
+ } catch (IOException e) {
+ log.debug("Failed to process POST request due to IO exception", e);
+ } catch (RuntimeException e) {
+ log.debug("Failed to process POST request due to Runtime exception", e);
+ }
+ if (!valid) {
+ msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+ }
+ }
+
+ public void reply(PluginContext ctx, DeferredResult<ResponseEntity> responseWriter, FromDeviceRpcResponse response) {
+ if (response.getError().isPresent()) {
+ RpcError error = response.getError().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 {
+ if (response.getResponse().isPresent() && !StringUtils.isEmpty(response.getResponse().get())) {
+ String data = response.getResponse().get();
+ try {
+ responseWriter.setResult(new ResponseEntity<>(jsonMapper.readTree(data), HttpStatus.OK));
+ } catch (IOException e) {
+ log.debug("Failed to decode device response: {}", data, e);
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE));
+ }
+ } else {
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.OK));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/LocalRequestMetaData.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/LocalRequestMetaData.java
new file mode 100644
index 0000000..96ec7a2
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/LocalRequestMetaData.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.rpc;
+
+import lombok.Data;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class LocalRequestMetaData {
+ private final ToDeviceRpcRequest request;
+ private final DeferredResult<ResponseEntity> responseWriter;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java
new file mode 100644
index 0000000..ccbe16c
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.rpc;
+
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.msg.*;
+import org.thingsboard.server.extensions.core.plugin.rpc.handlers.RpcRestMsgHandler;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class RpcManager {
+
+ @Setter
+ private RpcRestMsgHandler restHandler;
+
+ private Map<UUID, LocalRequestMetaData> localRpcRequests = new HashMap<>();
+
+ public void process(PluginContext ctx, LocalRequestMetaData requestMd) {
+ ToDeviceRpcRequest request = requestMd.getRequest();
+ log.trace("[{}] Processing local rpc call for device [{}]", request.getId(), request.getDeviceId());
+ ctx.sendRpcRequest(request);
+ localRpcRequests.put(request.getId(), requestMd);
+ ctx.scheduleTimeoutMsg(new TimeoutUUIDMsg(request.getId(), request.getExpirationTime() - System.currentTimeMillis()));
+ }
+
+ public void process(PluginContext ctx, FromDeviceRpcResponse response) {
+ UUID requestId = response.getId();
+ LocalRequestMetaData md = localRpcRequests.remove(requestId);
+ if (md != null) {
+ log.trace("[{}] Processing local rpc response from device [{}]", requestId, md.getRequest().getDeviceId());
+ restHandler.reply(ctx, md.getResponseWriter(), response);
+ } else {
+ log.trace("[{}] Unknown or stale rpc response received [{}]", requestId, response);
+ }
+ }
+
+ public void process(PluginContext ctx, TimeoutMsg msg) {
+ if (msg instanceof TimeoutUUIDMsg) {
+ UUID requestId = ((TimeoutUUIDMsg) msg).getId();
+ FromDeviceRpcResponse timeoutReponse = new FromDeviceRpcResponse(requestId, null, RpcError.TIMEOUT);
+ LocalRequestMetaData md = localRpcRequests.remove(requestId);
+ if (md != null) {
+ log.trace("[{}] Processing rpc timeout for local device [{}]", requestId, md.getRequest().getDeviceId());
+ restHandler.reply(ctx, md.getResponseWriter(), timeoutReponse);
+ }
+ }
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPlugin.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPlugin.java
new file mode 100644
index 0000000..ad273d2
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPlugin.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.rpc;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.extensions.api.component.Plugin;
+import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RestMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
+import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
+import org.thingsboard.server.extensions.core.plugin.rpc.handlers.RpcRestMsgHandler;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Plugin(name = "RPC Plugin", actions = {}, descriptor = "RpcPluginDescriptor.json", configuration = RpcPluginConfiguration.class)
+@Slf4j
+public class RpcPlugin extends AbstractPlugin<RpcPluginConfiguration> {
+
+ private final RpcManager rpcManager;
+ private final RpcRestMsgHandler restMsgHandler;
+
+ public RpcPlugin() {
+ this.rpcManager = new RpcManager();
+ this.restMsgHandler = new RpcRestMsgHandler(rpcManager);
+ this.rpcManager.setRestHandler(restMsgHandler);
+ }
+
+ @Override
+ public void process(PluginContext ctx, FromDeviceRpcResponse msg) {
+ rpcManager.process(ctx, msg);
+ }
+
+ @Override
+ public void process(PluginContext ctx, TimeoutMsg<?> msg) {
+ rpcManager.process(ctx, msg);
+ }
+
+ @Override
+ protected RestMsgHandler getRestMsgHandler() {
+ return restMsgHandler;
+ }
+
+ @Override
+ public void init(RpcPluginConfiguration configuration) {
+ restMsgHandler.setDefaultTimeout(configuration.getDefaultTimeout());
+ }
+
+ @Override
+ public void resume(PluginContext ctx) {
+
+ }
+
+ @Override
+ public void suspend(PluginContext ctx) {
+
+ }
+
+ @Override
+ public void stop(PluginContext ctx) {
+
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPluginConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPluginConfiguration.java
new file mode 100644
index 0000000..13db417
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPluginConfiguration.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.rpc;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class RpcPluginConfiguration {
+ private long defaultTimeout;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/AttributeData.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/AttributeData.java
new file mode 100644
index 0000000..1c77ffb
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/AttributeData.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.telemetry;
+
+public class AttributeData implements Comparable<AttributeData>{
+
+ private final long lastUpdateTs;
+ private final String key;
+ private final Object value;
+
+ public AttributeData(long lastUpdateTs, String key, Object value) {
+ super();
+ this.lastUpdateTs = lastUpdateTs;
+ this.key = key;
+ this.value = value;
+ }
+
+ public long getLastUpdateTs() {
+ return lastUpdateTs;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public Object getValue() {
+ return value;
+ }
+
+ @Override
+ public int compareTo(AttributeData o) {
+ return Long.compare(lastUpdateTs, o.lastUpdateTs);
+ }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java
new file mode 100644
index 0000000..e7bc414
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.telemetry.cmd;
+
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+
+/**
+ * @author Andrew Shvayka
+ */
+@NoArgsConstructor
+public class AttributesSubscriptionCmd extends SubscriptionCmd {
+
+ public AttributesSubscriptionCmd(int cmdId, String deviceId, String keys, boolean unsubscribe) {
+ super(cmdId, deviceId, keys, unsubscribe);
+ }
+
+ @Override
+ public SubscriptionType getType() {
+ return SubscriptionType.ATTRIBUTES;
+ }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java
new file mode 100644
index 0000000..5c9ccc3
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.telemetry.cmd;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class GetHistoryCmd implements TelemetryPluginCmd {
+
+ private int cmdId;
+ private String deviceId;
+ private String keys;
+ private long startTs;
+ private long endTs;
+
+ @Override
+ public int getCmdId() {
+ return cmdId;
+ }
+
+ @Override
+ public void setCmdId(int cmdId) {
+ this.cmdId = cmdId;
+ }
+
+ public String getDeviceId() {
+ return deviceId;
+ }
+
+ public void setDeviceId(String deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ public String getKeys() {
+ return keys;
+ }
+
+ public void setKeys(String keys) {
+ this.keys = keys;
+ }
+
+ public long getStartTs() {
+ return startTs;
+ }
+
+ public void setStartTs(long startTs) {
+ this.startTs = startTs;
+ }
+
+ public long getEndTs() {
+ return endTs;
+ }
+
+ public void setEndTs(long endTs) {
+ this.endTs = endTs;
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java
new file mode 100644
index 0000000..249dfa9
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.telemetry.cmd;
+
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+
+@NoArgsConstructor
+@AllArgsConstructor
+public abstract class SubscriptionCmd implements TelemetryPluginCmd {
+
+ private int cmdId;
+ private String deviceId;
+ private String keys;
+ private boolean unsubscribe;
+
+ public abstract SubscriptionType getType();
+
+ public int getCmdId() {
+ return cmdId;
+ }
+
+ public void setCmdId(int cmdId) {
+ this.cmdId = cmdId;
+ }
+
+ public String getDeviceId() {
+ return deviceId;
+ }
+
+ public void setDeviceId(String deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ public String getKeys() {
+ return keys;
+ }
+
+ public void setTags(String tags) {
+ this.keys = tags;
+ }
+
+ public boolean isUnsubscribe() {
+ return unsubscribe;
+ }
+
+ public void setUnsubscribe(boolean unsubscribe) {
+ this.unsubscribe = unsubscribe;
+ }
+
+ @Override
+ public String toString() {
+ return "SubscriptionCmd [deviceId=" + deviceId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]";
+ }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmd.java
new file mode 100644
index 0000000..cc2febb
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmd.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.telemetry.cmd;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface TelemetryPluginCmd {
+
+ int getCmdId();
+
+ void setCmdId(int cmdId);
+
+ String getKeys();
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmdsWrapper.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmdsWrapper.java
new file mode 100644
index 0000000..6d53969
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmdsWrapper.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.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() {
+ }
+
+ 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/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java
new file mode 100644
index 0000000..0b0ff91
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.telemetry.cmd;
+
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+
+/**
+ * @author Andrew Shvayka
+ */
+@NoArgsConstructor
+public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
+
+ private long timeWindow;
+
+ public TimeseriesSubscriptionCmd(int cmdId, String deviceId, String keys, boolean unsubscribe, long timeWindow) {
+ super(cmdId, deviceId, keys, unsubscribe);
+ this.timeWindow = timeWindow;
+ }
+
+ public long getTimeWindow() {
+ return timeWindow;
+ }
+
+ public void setTimeWindow(long timeWindow) {
+ this.timeWindow = timeWindow;
+ }
+
+ @Override
+ public SubscriptionType getType() {
+ return SubscriptionType.TIMESERIES;
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
new file mode 100644
index 0000000..dee981a
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
@@ -0,0 +1,201 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.telemetry.handlers;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.extensions.api.plugins.PluginCallback;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.DefaultRestMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+import org.thingsboard.server.extensions.api.plugins.rest.RestRequest;
+import org.thingsboard.server.extensions.core.plugin.telemetry.AttributeData;
+import org.thingsboard.server.extensions.core.plugin.telemetry.TsData;
+
+import javax.servlet.ServletException;
+import java.io.IOException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Slf4j
+public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
+
+ @Override
+ public void handleHttpGetRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
+ RestRequest request = msg.getRequest();
+ String[] pathParams = request.getPathParams();
+ if (pathParams.length >= 3) {
+ String deviceIdStr = pathParams[0];
+ String method = pathParams[1];
+ String entity = pathParams[2];
+ String scope = pathParams.length >= 4 ? pathParams[3] : null;
+ if (StringUtils.isEmpty(method) || StringUtils.isEmpty(entity) || StringUtils.isEmpty(deviceIdStr)) {
+ msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+ return;
+ }
+
+ DeviceId deviceId = DeviceId.fromString(deviceIdStr);
+
+ if (method.equals("keys")) {
+ if (entity.equals("timeseries")) {
+ ctx.loadLatestTimeseries(deviceId, new PluginCallback<List<TsKvEntry>>() {
+ @Override
+ public void onSuccess(PluginContext ctx, List<TsKvEntry> value) {
+ List<String> keys = value.stream().map(tsKv -> tsKv.getKey()).collect(Collectors.toList());
+ msg.getResponseHolder().setResult(new ResponseEntity<>(keys, HttpStatus.OK));
+ }
+
+ @Override
+ public void onFailure(PluginContext ctx, Exception e) {
+ msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+ });
+ } else if (entity.equals("attributes")) {
+ List<AttributeKvEntry> attributes;
+ if (!StringUtils.isEmpty(scope)) {
+ attributes = ctx.loadAttributes(deviceId, scope);
+ } else {
+ attributes = ctx.loadAttributes(deviceId, DataConstants.CLIENT_SCOPE);
+ attributes.addAll(ctx.loadAttributes(deviceId, DataConstants.SERVER_SCOPE));
+ attributes.addAll(ctx.loadAttributes(deviceId, DataConstants.SHARED_SCOPE));
+ }
+ List<String> keys = attributes.stream().map(attrKv -> attrKv.getKey()).collect(Collectors.toList());
+ msg.getResponseHolder().setResult(new ResponseEntity<>(keys, HttpStatus.OK));
+ }
+ } else if (method.equals("values")) {
+ if ("timeseries".equals(entity)) {
+ String keys = request.getParameter("keys");
+ Optional<Long> startTs = request.getLongParamValue("startTs");
+ Optional<Long> endTs = request.getLongParamValue("endTs");
+ Optional<Integer> limit = request.getIntParamValue("limit");
+ Map<String, List<TsData>> data = new LinkedHashMap<>();
+ for (String key : keys.split(",")) {
+ List<TsKvEntry> entries = ctx.loadTimeseries(deviceId, new BaseTsKvQuery(key, startTs, endTs, limit));
+ data.put(key, entries.stream().map(v -> new TsData(v.getTs(), v.getValueAsString())).collect(Collectors.toList()));
+ }
+ msg.getResponseHolder().setResult(new ResponseEntity<>(data, HttpStatus.OK));
+ } else if ("attributes".equals(entity)) {
+ String keys = request.getParameter("keys", "");
+ List<AttributeKvEntry> attributes;
+ if (!StringUtils.isEmpty(scope)) {
+ attributes = getAttributeKvEntries(ctx, scope, deviceId, keys);
+ } else {
+ attributes = getAttributeKvEntries(ctx, DataConstants.CLIENT_SCOPE, deviceId, keys);
+ attributes.addAll(getAttributeKvEntries(ctx, DataConstants.SHARED_SCOPE, deviceId, keys));
+ attributes.addAll(getAttributeKvEntries(ctx, DataConstants.SERVER_SCOPE, deviceId, keys));
+ }
+ List<AttributeData> values = attributes.stream().map(attribute -> new AttributeData(attribute.getLastUpdateTs(),
+ attribute.getKey(), attribute.getValue())).collect(Collectors.toList());
+ msg.getResponseHolder().setResult(new ResponseEntity<>(values, HttpStatus.OK));
+ }
+ }
+ } else {
+ msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+ }
+ }
+
+ @Override
+ public void handleHttpPostRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
+ RestRequest request = msg.getRequest();
+ try {
+ String[] pathParams = request.getPathParams();
+ if (pathParams.length == 2) {
+ DeviceId deviceId = DeviceId.fromString(pathParams[0]);
+ String scope = pathParams[1];
+ if (DataConstants.SERVER_SCOPE.equals(scope) ||
+ DataConstants.SHARED_SCOPE.equals(scope)) {
+ JsonNode jsonNode = jsonMapper.readTree(request.getRequestBody());
+ if (jsonNode.isObject()) {
+ 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()) {
+ attributes.add(new BaseAttributeKvEntry(new LongDataEntry(key, value.longValue()), ts));
+ }
+ });
+ if (attributes.size() > 0) {
+ ctx.saveAttributes(deviceId, scope, attributes, new PluginCallback<Void>() {
+ @Override
+ public void onSuccess(PluginContext ctx, Void value) {
+ msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
+ }
+
+ @Override
+ public void onFailure(PluginContext ctx, Exception e) {
+ msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+ });
+ return;
+ }
+ }
+ }
+ }
+ } catch (IOException e) {
+ log.debug("Failed to process POST request due to IO exception", e);
+ }
+ msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+ }
+
+ @Override
+ public void handleHttpDeleteRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
+ RestRequest request = msg.getRequest();
+ try {
+ String[] pathParams = request.getPathParams();
+ if (pathParams.length == 2) {
+ DeviceId deviceId = DeviceId.fromString(pathParams[0]);
+ String scope = pathParams[1];
+ if (DataConstants.SERVER_SCOPE.equals(scope) ||
+ DataConstants.SHARED_SCOPE.equals(scope)) {
+ String keysParam = request.getParameter("keys");
+ if (!StringUtils.isEmpty(keysParam)) {
+ String[] keys = keysParam.split(",");
+ ctx.removeAttributes(deviceId, scope, Arrays.asList(keys));
+ msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
+ return;
+ }
+ }
+ }
+ } catch (RuntimeException e) {
+ log.debug("Failed to process DELETE request due to Runtime exception", e);
+ }
+ msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+ }
+
+ private List<AttributeKvEntry> getAttributeKvEntries(PluginContext ctx, String scope, DeviceId deviceId, String keysParam) {
+ List<AttributeKvEntry> attributes;
+ if (!StringUtils.isEmpty(keysParam)) {
+ String[] keys = keysParam.split(",");
+ attributes = ctx.loadAttributes(deviceId, scope, Arrays.asList(keys));
+ } else {
+ attributes = ctx.loadAttributes(deviceId, scope);
+ }
+ return attributes;
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
new file mode 100644
index 0000000..b166dae
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
@@ -0,0 +1,185 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.telemetry.handlers;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RpcMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
+import org.thingsboard.server.extensions.core.plugin.telemetry.SubscriptionManager;
+import org.thingsboard.server.extensions.core.plugin.telemetry.gen.TelemetryPluginProtos.*;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class TelemetryRpcMsgHandler implements RpcMsgHandler {
+ private final SubscriptionManager subscriptionManager;
+
+ private static final int SUBSCRIPTION_CLAZZ = 1;
+ private static final int SUBSCRIPTION_UPDATE_CLAZZ = 2;
+ private static final int SESSION_CLOSE_CLAZZ = 3;
+ private static final int SUBSCRIPTION_CLOSE_CLAZZ = 4;
+
+ @Override
+ public void process(PluginContext ctx, RpcMsg msg) {
+ switch (msg.getMsgClazz()) {
+ case SUBSCRIPTION_CLAZZ:
+ processSubscriptionCmd(ctx, msg);
+ break;
+ case SUBSCRIPTION_UPDATE_CLAZZ:
+ processRemoteSubscriptionUpdate(ctx, msg);
+ break;
+ case SESSION_CLOSE_CLAZZ:
+ processSessionClose(ctx, msg);
+ break;
+ case SUBSCRIPTION_CLOSE_CLAZZ:
+ processSubscriptionClose(ctx, msg);
+ break;
+ default:
+ throw new RuntimeException("Unknown command id: " + msg.getMsgClazz());
+ }
+ }
+
+ private void processRemoteSubscriptionUpdate(PluginContext ctx, RpcMsg msg) {
+ SubscriptionUpdateProto proto;
+ try {
+ proto = SubscriptionUpdateProto.parseFrom(msg.getMsgData());
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ subscriptionManager.onRemoteSubscriptionUpdate(ctx, proto.getSessionId(), convert(proto));
+ }
+
+ private void processSubscriptionCmd(PluginContext ctx, RpcMsg msg) {
+ SubscriptionProto proto;
+ try {
+ proto = SubscriptionProto.parseFrom(msg.getMsgData());
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ Map<String, Long> statesMap = proto.getKeyStatesList().stream().collect(Collectors.toMap(SubscriptionKetStateProto::getKey, SubscriptionKetStateProto::getTs));
+ Subscription subscription = new Subscription(
+ new SubscriptionState(proto.getSessionId(), proto.getSubscriptionId(), DeviceId.fromString(proto.getDeviceId()), SubscriptionType.valueOf(proto.getType()), proto.getAllKeys(), statesMap),
+ false, msg.getServerAddress());
+ subscriptionManager.addRemoteWsSubscription(ctx, msg.getServerAddress(), proto.getSessionId(), subscription);
+ }
+
+ public void onNewSubscription(PluginContext ctx, ServerAddress address, String sessionId, Subscription cmd) {
+ SubscriptionProto.Builder builder = SubscriptionProto.newBuilder();
+ builder.setSessionId(sessionId);
+ builder.setSubscriptionId(cmd.getSubscriptionId());
+ builder.setDeviceId(cmd.getDeviceId().toString());
+ builder.setType(cmd.getType().name());
+ builder.setAllKeys(cmd.isAllKeys());
+ cmd.getKeyStates().entrySet().stream().forEach(e -> builder.addKeyStates(SubscriptionKetStateProto.newBuilder().setKey(e.getKey()).setTs(e.getValue()).build()));
+ ctx.sendPluginRpcMsg(new RpcMsg(address, SUBSCRIPTION_CLAZZ, builder.build().toByteArray()));
+ }
+
+ public void onSubscriptionUpdate(PluginContext ctx, ServerAddress address, String sessionId, SubscriptionUpdate update) {
+ SubscriptionUpdateProto proto = getSubscriptionUpdateProto(sessionId, update);
+ ctx.sendPluginRpcMsg(new RpcMsg(address, SUBSCRIPTION_UPDATE_CLAZZ, proto.toByteArray()));
+ }
+
+ public void onSessionClose(PluginContext ctx, ServerAddress address, String vSessionId) {
+ SessionCloseProto proto = SessionCloseProto.newBuilder().setSessionId(vSessionId).build();
+ ctx.sendPluginRpcMsg(new RpcMsg(address, SESSION_CLOSE_CLAZZ, proto.toByteArray()));
+ }
+
+ public void onSubscriptionClose(PluginContext ctx, ServerAddress address, String vSessionId, int subscriptionId) {
+ SubscriptionCloseProto proto = SubscriptionCloseProto.newBuilder().setSessionId(vSessionId).setSubscriptionId(subscriptionId).build();
+ ctx.sendPluginRpcMsg(new RpcMsg(address, SUBSCRIPTION_CLOSE_CLAZZ, proto.toByteArray()));
+ }
+
+ private void processSessionClose(PluginContext ctx, RpcMsg msg) {
+ SessionCloseProto proto;
+ try {
+ proto = SessionCloseProto.parseFrom(msg.getMsgData());
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ subscriptionManager.cleanupRemoteWsSessionSubscriptions(ctx, proto.getSessionId());
+ }
+
+ private void processSubscriptionClose(PluginContext ctx, RpcMsg msg) {
+ SubscriptionCloseProto proto;
+ try {
+ proto = SubscriptionCloseProto.parseFrom(msg.getMsgData());
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ subscriptionManager.removeSubscription(ctx, proto.getSessionId(), proto.getSubscriptionId());
+ }
+
+ private static SubscriptionUpdateProto getSubscriptionUpdateProto(String sessionId, SubscriptionUpdate update) {
+ SubscriptionUpdateProto.Builder builder = SubscriptionUpdateProto.newBuilder();
+ builder.setSessionId(sessionId);
+ builder.setSubscriptionId(update.getSubscriptionId());
+ builder.setErrorCode(update.getErrorCode());
+ if (update.getErrorMsg() != null) {
+ builder.setErrorMsg(update.getErrorMsg());
+ }
+ update.getData().entrySet().stream().forEach(
+ e -> {
+ SubscriptionUpdateValueListProto.Builder dataBuilder = 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());
+ }
+ );
+ return builder.build();
+ }
+
+ private SubscriptionUpdate convert(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().stream().forEach(v -> {
+ List<Object> values = data.get(v.getKey());
+ if (values == null) {
+ values = new ArrayList<>();
+ data.put(v.getKey(), values);
+ }
+ 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);
+ }
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java
new file mode 100644
index 0000000..f69d17b
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.telemetry.handlers;
+
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+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.msg.core.*;
+import org.thingsboard.server.common.msg.kv.BasicAttributeKVMsg;
+import org.thingsboard.server.extensions.api.plugins.PluginCallback;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.DefaultRuleMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.msg.GetAttributesRequestRuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.TelemetryUploadRequestRuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.UpdateAttributesRequestRuleToPluginMsg;
+import org.thingsboard.server.extensions.core.plugin.telemetry.SubscriptionManager;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
+ private final SubscriptionManager subscriptionManager;
+
+ public TelemetryRuleMsgHandler(SubscriptionManager subscriptionManager) {
+ this.subscriptionManager = subscriptionManager;
+ }
+
+ @Override
+ public void handleGetAttributesRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, GetAttributesRequestRuleToPluginMsg msg) {
+ GetAttributesRequest request = msg.getPayload();
+
+ List<AttributeKvEntry> clientAttributes = getAttributeKvEntries(ctx, msg.getDeviceId(), DataConstants.CLIENT_SCOPE, request.getClientAttributeNames());
+ List<AttributeKvEntry> sharedAttributes = getAttributeKvEntries(ctx, msg.getDeviceId(), DataConstants.SHARED_SCOPE, request.getSharedAttributeNames());
+
+ BasicGetAttributesResponse response = BasicGetAttributesResponse.onSuccess(request.getMsgType(),
+ request.getRequestId(), BasicAttributeKVMsg.from(clientAttributes, sharedAttributes));
+
+ ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, response));
+ }
+
+ private List<AttributeKvEntry> getAttributeKvEntries(PluginContext ctx, DeviceId deviceId, String scope, Set<String> names) {
+ List<AttributeKvEntry> attributes;
+ if (!names.isEmpty()) {
+ attributes = ctx.loadAttributes(deviceId, scope, new ArrayList<>(names));
+ } else {
+ attributes = Collections.emptyList();
+ }
+ return attributes;
+ }
+
+ @Override
+ public void handleTelemetryUploadRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, TelemetryUploadRequestRuleToPluginMsg msg) {
+ TelemetryUploadRequest request = msg.getPayload();
+ List<TsKvEntry> tsKvEntries = new ArrayList<>();
+ for (Map.Entry<Long, List<KvEntry>> entry : request.getData().entrySet()) {
+ for (KvEntry kv : entry.getValue()) {
+ tsKvEntries.add(new BasicTsKvEntry(entry.getKey(), kv));
+ }
+ }
+ ctx.saveTsData(msg.getDeviceId(), tsKvEntries, new PluginCallback<Void>() {
+ @Override
+ public void onSuccess(PluginContext ctx, Void data) {
+ ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, BasicStatusCodeResponse.onSuccess(request.getMsgType(), request.getRequestId())));
+ subscriptionManager.onLocalSubscriptionUpdate(ctx, msg.getDeviceId(), SubscriptionType.TIMESERIES, s -> {
+ List<TsKvEntry> subscriptionUpdate = new ArrayList<TsKvEntry>();
+ for (Map.Entry<Long, List<KvEntry>> entry : request.getData().entrySet()) {
+ for (KvEntry kv : entry.getValue()) {
+ if (s.isAllKeys() || s.getKeyStates().containsKey((kv.getKey()))) {
+ subscriptionUpdate.add(new BasicTsKvEntry(entry.getKey(), kv));
+ }
+ }
+ }
+ return subscriptionUpdate;
+ });
+ }
+
+ @Override
+ public void onFailure(PluginContext ctx, Exception e) {
+ ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, BasicStatusCodeResponse.onError(request.getMsgType(), request.getRequestId(), e)));
+ }
+ });
+ }
+
+ @Override
+ public void handleUpdateAttributesRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, UpdateAttributesRequestRuleToPluginMsg msg) {
+ UpdateAttributesRequest request = msg.getPayload();
+ ctx.saveAttributes(msg.getDeviceId(), DataConstants.CLIENT_SCOPE, request.getAttributes().stream().collect(Collectors.toList()),
+ new PluginCallback<Void>() {
+ @Override
+ public void onSuccess(PluginContext ctx, Void value) {
+ ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, BasicStatusCodeResponse.onSuccess(request.getMsgType(), request.getRequestId())));
+
+ subscriptionManager.onLocalSubscriptionUpdate(ctx, msg.getDeviceId(), SubscriptionType.ATTRIBUTES, s -> {
+ List<TsKvEntry> subscriptionUpdate = new ArrayList<TsKvEntry>();
+ for (AttributeKvEntry kv : request.getAttributes()) {
+ if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) {
+ subscriptionUpdate.add(new BasicTsKvEntry(kv.getLastUpdateTs(), kv));
+ }
+ }
+ return subscriptionUpdate;
+ });
+ }
+
+ @Override
+ public void onFailure(PluginContext ctx, Exception e) {
+ ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, BasicStatusCodeResponse.onError(request.getMsgType(), request.getRequestId(), e)));
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
new file mode 100644
index 0000000..6ea7489
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
@@ -0,0 +1,314 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.telemetry.handlers;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.extensions.api.plugins.PluginCallback;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.DefaultWebsocketMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+import org.thingsboard.server.extensions.api.plugins.ws.WsSessionMetaData;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.BinaryPluginWebSocketMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.TextPluginWebSocketMsg;
+import org.thingsboard.server.extensions.core.plugin.telemetry.SubscriptionManager;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.*;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionErrorCode;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionUpdate;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
+
+ private static final int UNKNOWN_SUBSCRIPTION_ID = 0;
+
+ private final SubscriptionManager subscriptionManager;
+
+ public TelemetryWebsocketMsgHandler(SubscriptionManager subscriptionManager) {
+ this.subscriptionManager = subscriptionManager;
+ }
+
+ @Override
+ protected void handleWebSocketMsg(PluginContext ctx, PluginWebsocketSessionRef sessionRef, PluginWebsocketMsg<?> wsMsg) {
+ try {
+ TelemetryPluginCmdsWrapper cmdsWrapper = null;
+ if (wsMsg instanceof TextPluginWebSocketMsg) {
+ TextPluginWebSocketMsg textMsg = (TextPluginWebSocketMsg) wsMsg;
+ cmdsWrapper = jsonMapper.readValue(textMsg.getPayload(), TelemetryPluginCmdsWrapper.class);
+ } else if (wsMsg instanceof BinaryPluginWebSocketMsg) {
+ throw new IllegalStateException("Not Implemented!");
+ // TODO: add support of BSON here based on
+ // https://github.com/michel-kraemer/bson4jackson
+ }
+ if (cmdsWrapper != null) {
+ if (cmdsWrapper.getAttrSubCmds() != null) {
+ cmdsWrapper.getAttrSubCmds().forEach(cmd -> handleWsAttributesSubscriptionCmd(ctx, sessionRef, cmd));
+ }
+ if (cmdsWrapper.getTsSubCmds() != null) {
+ cmdsWrapper.getTsSubCmds().forEach(cmd -> handleWsTimeseriesSubscriptionCmd(ctx, sessionRef, cmd));
+ }
+ if (cmdsWrapper.getHistoryCmds() != null) {
+ cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(ctx, 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(ctx, sessionRef, update);
+ }
+ }
+
+ @Override
+ protected void cleanupWebSocketSession(PluginContext ctx, String sessionId) {
+ subscriptionManager.cleanupLocalWsSessionSubscriptions(ctx, sessionId);
+ }
+
+ private void handleWsAttributesSubscriptionCmd(PluginContext ctx, PluginWebsocketSessionRef sessionRef, AttributesSubscriptionCmd cmd) {
+ String sessionId = sessionRef.getSessionId();
+ log.debug("[{}] Processing: {}", sessionId, cmd);
+
+ if (validateSessionMetadata(ctx, sessionRef, cmd, sessionId)) {
+ if (cmd.isUnsubscribe()) {
+ unsubscribe(ctx, cmd, sessionId);
+ } else if (validateSubscriptionCmd(ctx, sessionRef, cmd)) {
+ log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), cmd.getDeviceId());
+ DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
+ Optional<Set<String>> keysOptional = getKeys(cmd);
+ SubscriptionState sub;
+ if (keysOptional.isPresent()) {
+ List<String> keys = new ArrayList<>(keysOptional.get());
+ List<AttributeKvEntry> data = ctx.loadAttributes(deviceId, DataConstants.CLIENT_SCOPE, keys);
+ List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
+ sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
+
+ Map<String, Long> subState = new HashMap<>(keys.size());
+ keys.stream().forEach(key -> subState.put(key, 0L));
+ attributesData.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+
+ sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, false, subState);
+ } else {
+ List<AttributeKvEntry> data = ctx.loadAttributes(deviceId, DataConstants.CLIENT_SCOPE);
+ List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
+ sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
+
+ Map<String, Long> subState = new HashMap<>(attributesData.size());
+ attributesData.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+
+ sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, true, subState);
+ }
+ subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
+ }
+ }
+ }
+
+ private void handleWsTimeseriesSubscriptionCmd(PluginContext ctx, PluginWebsocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd) {
+ String sessionId = sessionRef.getSessionId();
+ log.debug("[{}] Processing: {}", sessionId, cmd);
+
+ if (validateSessionMetadata(ctx, sessionRef, cmd, sessionId)) {
+ if (cmd.isUnsubscribe()) {
+ unsubscribe(ctx, cmd, sessionId);
+ } else if (validateSubscriptionCmd(ctx, sessionRef, cmd)) {
+ DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
+ Optional<Set<String>> keysOptional = getKeys(cmd);
+
+ if (keysOptional.isPresent()) {
+ long startTs;
+ if (cmd.getTimeWindow() > 0) {
+ List<TsKvEntry> data = new ArrayList<>();
+ 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(), cmd.getDeviceId());
+ long endTs = System.currentTimeMillis();
+ startTs = endTs - cmd.getTimeWindow();
+ for (String key : keys) {
+ TsKvQuery query = new BaseTsKvQuery(key, startTs, endTs);
+ data.addAll(ctx.loadTimeseries(deviceId, query));
+ }
+ sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+
+ Map<String, Long> subState = new HashMap<>(keys.size());
+ keys.stream().forEach(key -> subState.put(key, startTs));
+ data.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+ SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, false, subState);
+ subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
+ } 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(), cmd.getDeviceId());
+ ctx.loadLatestTimeseries(deviceId, keys, new PluginCallback<List<TsKvEntry>>() {
+ @Override
+ public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
+ sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+
+ Map<String, Long> subState = new HashMap<>(keys.size());
+ keys.stream().forEach(key -> subState.put(key, startTs));
+ data.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+ SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, false, subState);
+ subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
+ }
+
+ @Override
+ public void onFailure(PluginContext ctx, Exception e) {
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+ "Failed to fetch data!");
+ sendWsMsg(ctx, sessionRef, update);
+ }
+ });
+ }
+ } else {
+ ctx.loadLatestTimeseries(deviceId, new PluginCallback<List<TsKvEntry>>() {
+ @Override
+ public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
+ sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+ Map<String, Long> subState = new HashMap<>(data.size());
+ data.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+ SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, true, subState);
+ subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
+ }
+
+ @Override
+ public void onFailure(PluginContext ctx, Exception e) {
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+ "Failed to fetch data!");
+ sendWsMsg(ctx, sessionRef, update);
+ }
+ });
+ }
+ }
+ }
+ }
+
+ private void handleWsHistoryCmd(PluginContext ctx, PluginWebsocketSessionRef 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(ctx, sessionRef, update);
+ return;
+ }
+ if (cmd.getDeviceId() == null || cmd.getDeviceId().isEmpty()) {
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+ "Device id is empty!");
+ sendWsMsg(ctx, sessionRef, update);
+ return;
+ }
+ if (cmd.getKeys() == null || cmd.getKeys().isEmpty()) {
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+ "Keys are empty!");
+ sendWsMsg(ctx, sessionRef, update);
+ return;
+ }
+ DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
+ if (!ctx.checkAccess(deviceId)) {
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
+ SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
+ sendWsMsg(ctx, sessionRef, update);
+ return;
+ }
+ List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
+ List<TsKvEntry> data = new ArrayList<>();
+ for (String key : keys) {
+ TsKvQuery query = new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs());
+ data.addAll(ctx.loadTimeseries(deviceId, query));
+ }
+ sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+ }
+
+ private boolean validateSessionMetadata(PluginContext ctx, PluginWebsocketSessionRef 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(ctx, sessionRef, update);
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ private void unsubscribe(PluginContext ctx, SubscriptionCmd cmd, String sessionId) {
+ if (cmd.getDeviceId() == null || cmd.getDeviceId().isEmpty()) {
+ cleanupWebSocketSession(ctx, sessionId);
+ } else {
+ subscriptionManager.removeSubscription(ctx, sessionId, cmd.getCmdId());
+ }
+ }
+
+ private boolean validateSubscriptionCmd(PluginContext ctx, PluginWebsocketSessionRef sessionRef, SubscriptionCmd cmd) {
+ if (cmd.getDeviceId() == null || cmd.getDeviceId().isEmpty()) {
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+ "Device id is empty!");
+ sendWsMsg(ctx, sessionRef, update);
+ return false;
+ }
+ DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
+ if (!ctx.checkAccess(deviceId)) {
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
+ SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
+ sendWsMsg(ctx, sessionRef, update);
+ return false;
+ }
+ return true;
+ }
+
+ private void sendWsMsg(PluginContext ctx, PluginWebsocketSessionRef sessionRef, SubscriptionUpdate update) {
+ TextPluginWebSocketMsg reply;
+ try {
+ reply = new TextPluginWebSocketMsg(sessionRef, jsonMapper.writeValueAsString(update));
+ ctx.send(reply);
+ } 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);
+ }
+ }
+
+ public static Optional<Set<String>> getKeys(TelemetryPluginCmd cmd) {
+ if (!StringUtils.isEmpty(cmd.getKeys())) {
+ Set<String> keys = new HashSet<>();
+ for (String key : cmd.getKeys().split(",")) {
+ keys.add(key);
+ }
+ return Optional.of(keys);
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ public void sendWsMsg(PluginContext ctx, String sessionId, SubscriptionUpdate update) {
+ WsSessionMetaData md = wsSessionsMap.get(sessionId);
+ if (md != null) {
+ sendWsMsg(ctx, md.getSessionRef(), update);
+ }
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java
new file mode 100644
index 0000000..9359d7b
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.telemetry.sub;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+
+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 DeviceId getDeviceId() {
+ return getSub().getDeviceId();
+ }
+
+ public SubscriptionType getType() {
+ return getSub().getType();
+ }
+
+ 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/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionErrorCode.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionErrorCode.java
new file mode 100644
index 0000000..4f229f9
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionErrorCode.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.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/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java
new file mode 100644
index 0000000..8606ff6
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.telemetry.sub;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.DeviceId;
+
+import java.util.Map;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+@AllArgsConstructor
+public class SubscriptionState {
+
+ private final String wsSessionId;
+ private final int subscriptionId;
+ private final DeviceId deviceId;
+ private final SubscriptionType type;
+ private final boolean allKeys;
+ private final Map<String, Long> keyStates;
+
+ @Override
+ public String toString() {
+ return "SubscriptionState{" +
+ "type=" + type +
+ ", deviceId=" + deviceId +
+ ", subscriptionId=" + subscriptionId +
+ ", wsSessionId='" + wsSessionId + '\'' +
+ '}';
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionType.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionType.java
new file mode 100644
index 0000000..7dafde7
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionType.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.telemetry.sub;
+
+/**
+ * @author Andrew Shvayka
+ */
+public enum SubscriptionType {
+ ATTRIBUTES, TIMESERIES
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java
new file mode 100644
index 0000000..33aabe1
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.telemetry.sub;
+
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+
+import java.util.*;
+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<>();
+ for (TsKvEntry tsEntry : data) {
+ List<Object> values = this.data.get(tsEntry.getKey());
+ if (values == null) {
+ values = new ArrayList<>();
+ this.data.put(tsEntry.getKey(), values);
+ }
+ 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/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
new file mode 100644
index 0000000..637500e
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
@@ -0,0 +1,284 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.telemetry;
+
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryRpcMsgHandler;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryWebsocketMsgHandler;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.Subscription;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionUpdate;
+
+import java.util.*;
+import java.util.function.Function;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class SubscriptionManager {
+
+ private final Map<DeviceId, Set<Subscription>> subscriptionsByDeviceId = new HashMap<>();
+
+ private final Map<String, Map<Integer, Subscription>> subscriptionsByWsSessionId = new HashMap<>();
+
+ @Setter
+ private TelemetryWebsocketMsgHandler websocketHandler;
+ @Setter
+ private TelemetryRpcMsgHandler rpcHandler;
+
+ public void addLocalWsSubscription(PluginContext ctx, String sessionId, DeviceId deviceId, SubscriptionState sub) {
+ Optional<ServerAddress> server = ctx.resolve(deviceId);
+ Subscription subscription;
+ if (server.isPresent()) {
+ ServerAddress address = server.get();
+ log.trace("[{}] Forwarding subscription [{}] for device [{}] to [{}]", sessionId, sub.getSubscriptionId(), deviceId, address);
+ subscription = new Subscription(sub, true, address);
+ rpcHandler.onNewSubscription(ctx, address, sessionId, subscription);
+ } else {
+ log.trace("[{}] Registering local subscription [{}] for device [{}]", sessionId, sub.getSubscriptionId(), deviceId);
+ subscription = new Subscription(sub, true);
+ }
+ registerSubscription(sessionId, deviceId, subscription);
+ }
+
+ public void addRemoteWsSubscription(PluginContext ctx, ServerAddress address, String sessionId, Subscription subscription) {
+ DeviceId deviceId = subscription.getDeviceId();
+ log.trace("[{}] Registering remote subscription [{}] for device [{}] to [{}]", sessionId, subscription.getSubscriptionId(), deviceId, address);
+ registerSubscription(sessionId, deviceId, subscription);
+ List<TsKvEntry> missedUpdates = new ArrayList<>();
+ if (subscription.getType() == SubscriptionType.ATTRIBUTES) {
+ subscription.getKeyStates().entrySet().stream().forEach(e -> {
+ Optional<AttributeKvEntry> latestOpt = ctx.loadAttribute(deviceId, DataConstants.CLIENT_SCOPE, e.getKey());
+ if (latestOpt.isPresent()) {
+ AttributeKvEntry latestEntry = latestOpt.get();
+ if (latestEntry.getLastUpdateTs() > e.getValue()) {
+ missedUpdates.add(new BasicTsKvEntry(latestEntry.getLastUpdateTs(), latestEntry));
+ }
+ }
+ }
+ );
+ } else if (subscription.getType() == SubscriptionType.TIMESERIES) {
+ long curTs = System.currentTimeMillis();
+ subscription.getKeyStates().entrySet().forEach(e -> {
+ TsKvQuery query = new BaseTsKvQuery(e.getKey(), e.getValue() + 1L, curTs);
+ missedUpdates.addAll(ctx.loadTimeseries(deviceId, query));
+ });
+ }
+ if (!missedUpdates.isEmpty()) {
+ rpcHandler.onSubscriptionUpdate(ctx, address, sessionId, new SubscriptionUpdate(subscription.getSubscriptionId(), missedUpdates));
+ }
+ }
+
+ private void registerSubscription(String sessionId, DeviceId deviceId, Subscription subscription) {
+ Set<Subscription> deviceSubscriptions = subscriptionsByDeviceId.get(subscription.getDeviceId());
+ if (deviceSubscriptions == null) {
+ deviceSubscriptions = new HashSet<>();
+ subscriptionsByDeviceId.put(deviceId, deviceSubscriptions);
+ }
+ deviceSubscriptions.add(subscription);
+ Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId);
+ if (sessionSubscriptions == null) {
+ sessionSubscriptions = new HashMap<>();
+ subscriptionsByWsSessionId.put(sessionId, sessionSubscriptions);
+ }
+ sessionSubscriptions.put(subscription.getSubscriptionId(), subscription);
+ }
+
+ public void removeSubscription(PluginContext ctx, String sessionId, Integer 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) {
+ DeviceId deviceId = subscription.getDeviceId();
+ if (subscription.isLocal() && subscription.getServer() != null) {
+ rpcHandler.onSubscriptionClose(ctx, 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 = subscriptionsByDeviceId.get(deviceId);
+ if (deviceSubscriptions != null) {
+ boolean result = deviceSubscriptions.remove(subscription);
+ if (result) {
+ if (deviceSubscriptions.size() == 0) {
+ log.debug("[{}] Removed last subscription for particular device.", sessionId);
+ subscriptionsByDeviceId.remove(deviceId);
+ } else {
+ log.debug("[{}] Removed device subscription.", sessionId);
+ }
+ } else {
+ log.debug("[{}] Subscription not found!", sessionId);
+ }
+ } else {
+ log.debug("[{}] No device subscriptions found!", sessionId);
+ }
+ } else {
+ log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId);
+ }
+ } else {
+ log.debug("[{}] No session subscriptions found!", sessionId);
+ }
+ }
+
+ public void onLocalSubscriptionUpdate(PluginContext ctx, DeviceId deviceId, SubscriptionType type, Function<Subscription, List<TsKvEntry>> f) {
+ Set<Subscription> deviceSubscriptions = subscriptionsByDeviceId.get(deviceId);
+ if (deviceSubscriptions != null) {
+ deviceSubscriptions.stream().filter(s -> type == s.getType()).forEach(s -> {
+ String sessionId = s.getWsSessionId();
+ List<TsKvEntry> subscriptionUpdate = f.apply(s);
+ if (!subscriptionUpdate.isEmpty()) {
+ SubscriptionUpdate update = new SubscriptionUpdate(s.getSubscriptionId(), subscriptionUpdate);
+ if (s.isLocal()) {
+ updateSubscriptionState(sessionId, s, update);
+ websocketHandler.sendWsMsg(ctx, sessionId, update);
+ } else {
+ rpcHandler.onSubscriptionUpdate(ctx, s.getServer(), sessionId, update);
+ }
+ }
+ });
+ } else {
+ log.debug("[{}] No device subscriptions to process!", deviceId);
+ }
+ }
+
+ public void onRemoteSubscriptionUpdate(PluginContext ctx, String sessionId, SubscriptionUpdate update) {
+ log.trace("[{}] Processing remote subscription onUpdate [{}]", sessionId, update);
+ Optional<Subscription> subOpt = getSubscription(sessionId, update.getSubscriptionId());
+ if (subOpt.isPresent()) {
+ updateSubscriptionState(sessionId, subOpt.get(), update);
+ websocketHandler.sendWsMsg(ctx, sessionId, update);
+ }
+ }
+
+ 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 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);
+ }
+
+ public void cleanupLocalWsSessionSubscriptions(PluginContext ctx, String sessionId) {
+ cleanupWsSessionSubscriptions(ctx, sessionId, true);
+ }
+
+ public void cleanupRemoteWsSessionSubscriptions(PluginContext ctx, String sessionId) {
+ cleanupWsSessionSubscriptions(ctx, sessionId, false);
+ }
+
+ private void cleanupWsSessionSubscriptions(PluginContext ctx, 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()) {
+ DeviceId deviceId = subscription.getDeviceId();
+ Set<Subscription> deviceSubscriptions = subscriptionsByDeviceId.get(deviceId);
+ deviceSubscriptions.remove(subscription);
+ if (deviceSubscriptions.isEmpty()) {
+ subscriptionsByDeviceId.remove(deviceId);
+ }
+ }
+ subscriptionsByWsSessionId.remove(sessionId);
+ log.debug("[{}] Removed {} subscriptions for particular session.", sessionId, sessionSubscriptionSize);
+
+ if (localSession) {
+ 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);
+ rpcHandler.onSessionClose(ctx, address, sessionId);
+ }
+ }
+ } else {
+ log.debug("[{}] No subscriptions found!", sessionId);
+ }
+ }
+
+ public void onClusterUpdate(PluginContext ctx) {
+ log.trace("Processing cluster onUpdate msg!");
+ Iterator<Map.Entry<DeviceId, Set<Subscription>>> deviceIterator = subscriptionsByDeviceId.entrySet().iterator();
+ while (deviceIterator.hasNext()) {
+ Map.Entry<DeviceId, Set<Subscription>> e = deviceIterator.next();
+ Set<Subscription> subscriptions = e.getValue();
+ Optional<ServerAddress> newAddressOptional = ctx.resolve(e.getKey());
+ if (newAddressOptional.isPresent()) {
+ ServerAddress newAddress = newAddressOptional.get();
+ 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);
+ rpcHandler.onNewSubscription(ctx, 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.
+ }
+ }
+ } else {
+ Iterator<Subscription> subscriptionIterator = subscriptions.iterator();
+ while (subscriptionIterator.hasNext()) {
+ Subscription s = subscriptionIterator.next();
+ if (s.isLocal()) {
+ if (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());
+ }
+ }
+ }
+ if (subscriptions.size() == 0) {
+ log.trace("[{}] No more subscriptions for this device on current server.", e.getKey());
+ deviceIterator.remove();
+ }
+ }
+ }
+
+ public void clear() {
+ subscriptionsByWsSessionId.clear();
+ subscriptionsByDeviceId.clear();
+ }
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TelemetryStoragePlugin.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TelemetryStoragePlugin.java
new file mode 100644
index 0000000..63d145d
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TelemetryStoragePlugin.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.telemetry;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.api.component.EmptyComponentConfiguration;
+import org.thingsboard.server.extensions.api.component.Plugin;
+import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RestMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.handlers.RpcMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.handlers.WebsocketMsgHandler;
+import org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryRestMsgHandler;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryRpcMsgHandler;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryRuleMsgHandler;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryWebsocketMsgHandler;
+
+@Plugin(name = "Telemetry Plugin", actions = {TelemetryPluginAction.class})
+@Slf4j
+public class TelemetryStoragePlugin extends AbstractPlugin<EmptyComponentConfiguration> {
+
+ private final TelemetryRestMsgHandler restMsgHandler;
+ private final TelemetryRuleMsgHandler ruleMsgHandler;
+ private final TelemetryWebsocketMsgHandler websocketMsgHandler;
+ private final TelemetryRpcMsgHandler rpcMsgHandler;
+ private final SubscriptionManager subscriptionManager;
+
+ public TelemetryStoragePlugin() {
+ this.subscriptionManager = new SubscriptionManager();
+ this.restMsgHandler = new TelemetryRestMsgHandler();
+ this.ruleMsgHandler = new TelemetryRuleMsgHandler(subscriptionManager);
+ this.websocketMsgHandler = new TelemetryWebsocketMsgHandler(subscriptionManager);
+ this.rpcMsgHandler = new TelemetryRpcMsgHandler(subscriptionManager);
+ this.subscriptionManager.setWebsocketHandler(this.websocketMsgHandler);
+ this.subscriptionManager.setRpcHandler(this.rpcMsgHandler);
+ }
+
+ @Override
+ public void init(EmptyComponentConfiguration configuration) {
+
+ }
+
+ @Override
+ protected RestMsgHandler getRestMsgHandler() {
+ return restMsgHandler;
+ }
+
+ @Override
+ protected RuleMsgHandler getRuleMsgHandler() {
+ return ruleMsgHandler;
+ }
+
+ @Override
+ protected WebsocketMsgHandler getWebsocketMsgHandler() {
+ return websocketMsgHandler;
+ }
+
+ @Override
+ protected RpcMsgHandler getRpcMsgHandler() {
+ return rpcMsgHandler;
+ }
+
+ @Override
+ public void onServerAdded(PluginContext ctx, ServerAddress server) {
+ subscriptionManager.onClusterUpdate(ctx);
+ }
+
+ @Override
+ public void onServerRemoved(PluginContext ctx, ServerAddress server) {
+ subscriptionManager.onClusterUpdate(ctx);
+ }
+
+
+ @Override
+ public void resume(PluginContext ctx) {
+ log.info("Plugin activated!");
+ }
+
+ @Override
+ public void suspend(PluginContext ctx) {
+ log.info("Plugin suspended!");
+ }
+
+ @Override
+ public void stop(PluginContext ctx) {
+ subscriptionManager.clear();
+ websocketMsgHandler.clear(ctx);
+ log.info("Plugin stopped!");
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TsData.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TsData.java
new file mode 100644
index 0000000..57a1ce2
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TsData.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.telemetry;
+
+public class TsData implements Comparable<TsData>{
+
+ private final long ts;
+ private final String value;
+
+ public TsData(long ts, String value) {
+ super();
+ this.ts = ts;
+ this.value = value;
+ }
+
+ public long getTs() {
+ return ts;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public int compareTo(TsData o) {
+ return Long.compare(ts, o.ts);
+ }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java
new file mode 100644
index 0000000..97fc9ad
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java
@@ -0,0 +1,92 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.time;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.ToServerRpcRequestMsg;
+import org.thingsboard.server.common.msg.core.ToServerRpcResponseMsg;
+import org.thingsboard.server.extensions.api.component.Plugin;
+import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.msg.RpcResponsePluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+import org.thingsboard.server.extensions.core.action.rpc.RpcPluginAction;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Plugin(name = "Time Plugin", actions = {RpcPluginAction.class},
+ descriptor = "TimePluginDescriptor.json", configuration = TimePluginConfiguration.class)
+@Slf4j
+public class TimePlugin extends AbstractPlugin<TimePluginConfiguration> implements RuleMsgHandler {
+
+ private DateTimeFormatter formatter;
+ private String format;
+
+ @Override
+ public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
+ if (msg.getPayload() instanceof ToServerRpcRequestMsg) {
+ ToServerRpcRequestMsg request = (ToServerRpcRequestMsg) msg.getPayload();
+
+ String reply;
+ if (!StringUtils.isEmpty(format)) {
+ reply = "\"" + formatter.format(LocalDateTime.now()) + "\"";
+ } else {
+ reply = Long.toString(System.currentTimeMillis());
+ }
+ ToServerRpcResponseMsg response = new ToServerRpcResponseMsg(request.getRequestId(), "{\"time\":" + reply + "}");
+ ctx.reply(new RpcResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, response));
+ } else {
+ throw new RuntimeException("Not supported msg type: " + msg.getPayload().getClass() + "!");
+ }
+ }
+
+ @Override
+ public void init(TimePluginConfiguration configuration) {
+ format = configuration.getTimeFormat();
+ if (!StringUtils.isEmpty(format)) {
+ formatter = DateTimeFormatter.ofPattern(format);
+ }
+ }
+
+ @Override
+ public void resume(PluginContext ctx) {
+
+ }
+
+ @Override
+ public void suspend(PluginContext ctx) {
+
+ }
+
+ @Override
+ public void stop(PluginContext ctx) {
+
+ }
+
+ @Override
+ protected RuleMsgHandler getRuleMsgHandler() {
+ return this;
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePluginConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePluginConfiguration.java
new file mode 100644
index 0000000..971da74
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePluginConfiguration.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 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.extensions.core.plugin.time;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class TimePluginConfiguration {
+ private String timeFormat;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessor.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessor.java
new file mode 100644
index 0000000..ea01e45
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessor.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright © 2016 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.extensions.core.processor;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.velocity.Template;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.extensions.api.component.Processor;
+import org.thingsboard.server.extensions.api.rules.*;
+import org.thingsboard.server.extensions.core.utils.VelocityUtils;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Processor(name = "Alarm Deduplication Processor", descriptor = "AlarmDeduplicationProcessorDescriptor.json",
+ configuration = AlarmDeduplicationProcessorConfiguration.class)
+@Slf4j
+public class AlarmDeduplicationProcessor extends SimpleRuleLifecycleComponent
+ implements RuleProcessor<AlarmDeduplicationProcessorConfiguration> {
+
+ public static final String IS_NEW_ALARM = "isNewAlarm";
+ private ObjectMapper mapper = new ObjectMapper();
+ private AlarmDeduplicationProcessorConfiguration configuration;
+ private Template alarmIdTemplate;
+ private Template alarmBodyTemplate;
+
+ @Override
+ public void init(AlarmDeduplicationProcessorConfiguration configuration) {
+ this.configuration = configuration;
+ try {
+ this.alarmIdTemplate = VelocityUtils.create(configuration.getAlarmIdTemplate(), "Alarm Id Template");
+ this.alarmBodyTemplate = VelocityUtils.create(configuration.getAlarmBodyTemplate(), "Alarm Body Template");
+ } catch (ParseException e) {
+ log.error("Failed to create templates based on provided configuration!", e);
+ throw new RuntimeException("Failed to create templates based on provided configuration!", e);
+ }
+ }
+
+ @Override
+ public RuleProcessingMetaData process(RuleContext ctx, ToDeviceActorMsg msg) throws RuleException {
+ RuleProcessingMetaData md = new RuleProcessingMetaData();
+ VelocityContext context = VelocityUtils.createContext(ctx.getDeviceAttributes(), msg.getPayload());
+ String alarmId = VelocityUtils.merge(alarmIdTemplate, context);
+ String alarmBody = VelocityUtils.merge(alarmBodyTemplate, context);
+ Optional<Event> existingEvent = ctx.findEvent(DataConstants.ALARM, alarmId);
+ if (!existingEvent.isPresent()) {
+ Event event = new Event();
+ event.setType(DataConstants.ALARM);
+ event.setUid(alarmId);
+ event.setBody(mapper.createObjectNode().put("body", alarmBody));
+ Optional<Event> savedEvent = ctx.saveIfNotExists(event);
+ if (savedEvent.isPresent()) {
+ log.info("New Alarm detected: '{}'", alarmId);
+ md.put(IS_NEW_ALARM, Boolean.TRUE);
+ md.put("alarmId", alarmId);
+ md.put("alarmBody", alarmBody);
+ for (Object key : context.getKeys()) {
+ md.put(key.toString(), context.get(key.toString()));
+ }
+ }
+ }
+ return md;
+ }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessorConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessorConfiguration.java
new file mode 100644
index 0000000..fc8becc
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessorConfiguration.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.extensions.core.processor;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class AlarmDeduplicationProcessorConfiguration {
+
+ private String alarmIdTemplate;
+ private String alarmBodyTemplate;
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/utils/VelocityUtils.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/utils/VelocityUtils.java
new file mode 100644
index 0000000..edbfba1
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/utils/VelocityUtils.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright © 2016 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.extensions.core.utils;
+
+import org.apache.velocity.Template;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.RuntimeSingleton;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.node.SimpleNode;
+import org.apache.velocity.tools.generic.DateTool;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.extensions.api.device.DeviceAttributes;
+import org.thingsboard.server.extensions.api.rules.RuleProcessingMetaData;
+import org.thingsboard.server.extensions.core.filter.DeviceAttributesFilter;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class VelocityUtils {
+
+ public static Template create(String source, String templateName) throws ParseException {
+ RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices();
+ StringReader reader = new StringReader(source);
+ SimpleNode node = runtimeServices.parse(reader, templateName);
+ Template template = new Template();
+ template.setRuntimeServices(runtimeServices);
+ template.setData(node);
+ template.initDocument();
+ return template;
+ }
+
+ public static String merge(Template template, VelocityContext context) {
+ StringWriter writer = new StringWriter();
+ template.merge(context, writer);
+ return writer.toString();
+ }
+
+ public static VelocityContext createContext(RuleProcessingMetaData metadata) {
+ VelocityContext context = new VelocityContext();
+ metadata.getValues().forEach((k, v) -> context.put(k, v));
+ return context;
+ }
+
+ public static VelocityContext createContext(DeviceAttributes deviceAttributes, FromDeviceMsg payload) {
+ VelocityContext context = new VelocityContext();
+ context.put("date", new DateTool());
+ pushAttributes(context, deviceAttributes.getClientSideAttributes(), DeviceAttributesFilter.CLIENT_SIDE);
+ pushAttributes(context, deviceAttributes.getServerSideAttributes(), DeviceAttributesFilter.SERVER_SIDE);
+ pushAttributes(context, deviceAttributes.getServerSidePublicAttributes(), DeviceAttributesFilter.SHARED);
+
+ switch (payload.getMsgType()) {
+ case POST_TELEMETRY_REQUEST:
+ pushTsEntries(context, (TelemetryUploadRequest) payload);
+ break;
+ }
+
+ return context;
+ }
+
+ private static void pushTsEntries(VelocityContext context, TelemetryUploadRequest payload) {
+ payload.getData().forEach((k, vList) -> {
+ vList.forEach(v -> {
+ context.put(v.getKey(), new BasicTsKvEntry(k, v));
+ });
+ });
+ }
+
+ private static void pushAttributes(VelocityContext context, Collection<AttributeKvEntry> deviceAttributes, String prefix) {
+ Map<String, String> values = new HashMap<>();
+ deviceAttributes.forEach(v -> values.put(v.getKey(), v.getValueAsString()));
+ context.put(prefix, values);
+ }
+}
diff --git a/extensions-core/src/main/proto/telemetry.proto b/extensions-core/src/main/proto/telemetry.proto
new file mode 100644
index 0000000..5c7d7a4
--- /dev/null
+++ b/extensions-core/src/main/proto/telemetry.proto
@@ -0,0 +1,57 @@
+/**
+ * Copyright © 2016 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 telemetry;
+
+option java_package = "org.thingsboard.server.extensions.core.plugin.telemetry.gen";
+option java_outer_classname = "TelemetryPluginProtos";
+
+message SubscriptionProto {
+ string sessionId = 1;
+ int32 subscriptionId = 2;
+ string deviceId = 3;
+ string type = 4;
+ bool allKeys = 5;
+ repeated SubscriptionKetStateProto keyStates = 6;
+}
+
+message SubscriptionUpdateProto {
+ string sessionId = 1;
+ int32 subscriptionId = 2;
+ int32 errorCode = 3;
+ string errorMsg = 4;
+ repeated SubscriptionUpdateValueListProto data = 5;
+}
+
+message SessionCloseProto {
+ string sessionId = 1;
+}
+
+message SubscriptionCloseProto {
+ string sessionId = 1;
+ int32 subscriptionId = 2;
+}
+
+message SubscriptionKetStateProto {
+ string key = 1;
+ int64 ts = 2;
+}
+
+message SubscriptionUpdateValueListProto {
+ string key = 1;
+ repeated int64 ts = 2;
+ repeated string value = 3;
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/AlarmDeduplicationProcessorDescriptor.json b/extensions-core/src/main/resources/AlarmDeduplicationProcessorDescriptor.json
new file mode 100644
index 0000000..c082fc8
--- /dev/null
+++ b/extensions-core/src/main/resources/AlarmDeduplicationProcessorDescriptor.json
@@ -0,0 +1,30 @@
+{
+ "schema": {
+ "title": "Send Mail Action Configuration",
+ "type": "object",
+ "properties": {
+ "alarmIdTemplate": {
+ "title": "Alarm Id template",
+ "type": "string",
+ "default": "TODO"
+ },
+ "alarmBodyTemplate": {
+ "title": "Alarm body template",
+ "type": "string",
+ "default": "TODO"
+ }
+ },
+ "required": [
+ "alarmIdTemplate",
+ "alarmBodyTemplate"
+ ]
+ },
+ "form": [
+ "alarmIdTemplate",
+ {
+ "key": "alarmBodyTemplate",
+ "type": "textarea",
+ "rows": 2
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/DeviceMessagingPluginDescriptor.json b/extensions-core/src/main/resources/DeviceMessagingPluginDescriptor.json
new file mode 100644
index 0000000..5f6740d
--- /dev/null
+++ b/extensions-core/src/main/resources/DeviceMessagingPluginDescriptor.json
@@ -0,0 +1,37 @@
+{
+ "schema": {
+ "title": "Device Messaging configuration",
+ "type": "object",
+ "properties": {
+ "maxDeviceCountPerCustomer": {
+ "title": "Maximum amount of devices per customer",
+ "type": "integer",
+ "default": 1024,
+ "minimum": 0,
+ "maximum": 1024
+ },
+ "defaultTimeout": {
+ "title": "Default request timeout",
+ "type": "integer",
+ "default": 20000,
+ "minimum": 0
+ },
+ "maxTimeout": {
+ "title": "Maximum request timeout",
+ "type": "integer",
+ "default": 60000,
+ "minimum": 0
+ }
+ },
+ "required": [
+ "maxDeviceCountPerCustomer",
+ "defaultTimeout",
+ "maxTimeout"
+ ]
+ },
+ "form": [
+ "maxDeviceCountPerCustomer",
+ "defaultTimeout",
+ "maxTimeout"
+ ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/JsFilterDescriptor.json b/extensions-core/src/main/resources/JsFilterDescriptor.json
new file mode 100644
index 0000000..29624ed
--- /dev/null
+++ b/extensions-core/src/main/resources/JsFilterDescriptor.json
@@ -0,0 +1,21 @@
+{
+ "schema": {
+ "title": "Filter Configuration",
+ "type": "object",
+ "properties": {
+ "filter": {
+ "title": "Filter",
+ "type": "string"
+ }
+ },
+ "required": [
+ "filter"
+ ]
+ },
+ "form": [
+ {
+ "key": "filter",
+ "type": "javascript"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/MailPluginData.json b/extensions-core/src/main/resources/MailPluginData.json
new file mode 100644
index 0000000..8adae62
--- /dev/null
+++ b/extensions-core/src/main/resources/MailPluginData.json
@@ -0,0 +1,16 @@
+{
+ "host": "smtp.ukr.net",
+ "port": 465,
+ "username": "thingsboard@ukr.net",
+ "password": "thingsboard123",
+ "otherProperties": [
+ {
+ "key":"mailsmtp.auth",
+ "value":"true"
+ },
+ {
+ "key":"mail.smtp.starttls.enable",
+ "value":"true"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/MailPluginDescriptor.json b/extensions-core/src/main/resources/MailPluginDescriptor.json
new file mode 100644
index 0000000..d2291e0
--- /dev/null
+++ b/extensions-core/src/main/resources/MailPluginDescriptor.json
@@ -0,0 +1,61 @@
+{
+ "schema": {
+ "title": "Mail Plugin Configuration",
+ "type": "object",
+ "properties": {
+ "host": {
+ "title": "Mail server host",
+ "type": "string"
+ },
+ "port": {
+ "title": "Mail server port",
+ "type": "integer",
+ "default": 25,
+ "minimum": 0,
+ "maximum": 65536
+ },
+ "username": {
+ "title": "Username",
+ "type": "string"
+ },
+ "password": {
+ "title": "Password",
+ "type": "string"
+ },
+ "otherProperties": {
+ "title": "Other mail properties",
+ "type": "array",
+ "items": {
+ "title": "Mail property",
+ "type": "object",
+ "properties": {
+ "key": {
+ "title": "Key",
+ "type": "string"
+ },
+ "value": {
+ "title": "Value",
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "required": [
+ "host",
+ "port",
+ "username",
+ "password"
+ ]
+ },
+ "form": [
+ "host",
+ "port",
+ "username",
+ {
+ "key": "password",
+ "type": "password"
+ },
+ "otherProperties"
+ ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/MethodNameFilterDescriptor.json b/extensions-core/src/main/resources/MethodNameFilterDescriptor.json
new file mode 100644
index 0000000..fe1ac7b
--- /dev/null
+++ b/extensions-core/src/main/resources/MethodNameFilterDescriptor.json
@@ -0,0 +1,28 @@
+{
+ "schema": {
+ "title": "Method Name Filter Configuration",
+ "type": "object",
+ "properties": {
+ "methodNames": {
+ "title": "Method names",
+ "type": "array",
+ "minItems" : 1,
+ "items": {
+ "type": "object",
+ "title": "Method name",
+ "properties": {
+ "name": {
+ "title": "Method Name",
+ "type": "string"
+ }
+ }
+ },
+ "uniqueItems": true
+ }
+ },
+ "required": ["methodNames"]
+ },
+ "form": [
+ "methodNames"
+ ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/MsgTypeFilterDescriptor.json b/extensions-core/src/main/resources/MsgTypeFilterDescriptor.json
new file mode 100644
index 0000000..a0339c9
--- /dev/null
+++ b/extensions-core/src/main/resources/MsgTypeFilterDescriptor.json
@@ -0,0 +1,40 @@
+{
+ "schema": {
+ "title": "Message Type Filter Configuration",
+ "type": "object",
+ "properties": {
+ "messageTypes": {
+ "title": "Message types",
+ "type": "array",
+ "minItems" : 1,
+ "items": [
+ {
+ "value": "GET_ATTRIBUTES",
+ "label": "Get attributes"
+ },
+ {
+ "value": "POST_ATTRIBUTES",
+ "label": "Post attributes"
+ },
+ {
+ "value": "POST_TELEMETRY",
+ "label": "Post telemetry"
+ },
+ {
+ "value": "RPC_REQUEST",
+ "label": "RPC Request"
+ }
+ ],
+ "uniqueItems": true
+ }
+ },
+ "required": ["messageTypes"]
+ },
+ "form": [
+ {
+ "key": "messageTypes",
+ "type": "rc-select",
+ "multiple": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/RpcPluginData.json b/extensions-core/src/main/resources/RpcPluginData.json
new file mode 100644
index 0000000..454e33a
--- /dev/null
+++ b/extensions-core/src/main/resources/RpcPluginData.json
@@ -0,0 +1,3 @@
+{
+ "defaultTimeout": 60000
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/RpcPluginDescriptor.json b/extensions-core/src/main/resources/RpcPluginDescriptor.json
new file mode 100644
index 0000000..1e84b26
--- /dev/null
+++ b/extensions-core/src/main/resources/RpcPluginDescriptor.json
@@ -0,0 +1,18 @@
+{
+ "schema": {
+ "title": "RPC Plugin Configuration",
+ "type": "object",
+ "properties": {
+ "defaultTimeout": {
+ "title": "Default timeout",
+ "type": "number"
+ }
+ },
+ "required": [
+ "defaultTimeout"
+ ]
+ },
+ "form": [
+ "*"
+ ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/SendMailActionData.json b/extensions-core/src/main/resources/SendMailActionData.json
new file mode 100644
index 0000000..d9ad53d
--- /dev/null
+++ b/extensions-core/src/main/resources/SendMailActionData.json
@@ -0,0 +1,7 @@
+{
+ "sendFlag": "isNewAlarm",
+ "fromTemplate": "thingsboard@ukr.net",
+ "toTemplate": "andrew.shvayka@gmail.com",
+ "subjectTemplate": "Thingsboard",
+ "bodyTemplate": "Hello World!"
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/SendMailActionDescriptor.json b/extensions-core/src/main/resources/SendMailActionDescriptor.json
new file mode 100644
index 0000000..ed8cdf4
--- /dev/null
+++ b/extensions-core/src/main/resources/SendMailActionDescriptor.json
@@ -0,0 +1,55 @@
+{
+ "schema": {
+ "title": "Send Mail Action Configuration",
+ "type": "object",
+ "properties": {
+ "sendFlag": {
+ "title": "Send flag",
+ "type": "string"
+ },
+ "fromTemplate": {
+ "title": "From template",
+ "type": "string"
+ },
+ "toTemplate": {
+ "title": "To template",
+ "type": "string"
+ },
+ "ccTemplate": {
+ "title": "CC template",
+ "type": "string"
+ },
+ "bccTemplate": {
+ "title": "BCC template",
+ "type": "string"
+ },
+ "subjectTemplate": {
+ "title": "Subject template",
+ "type": "string"
+ },
+ "bodyTemplate": {
+ "title": "Body template",
+ "type": "string"
+ }
+ },
+ "required": [
+ "fromTemplate",
+ "toTemplate",
+ "subjectTemplate",
+ "bodyTemplate"
+ ]
+ },
+ "form": [
+ "sendFlag",
+ "fromTemplate",
+ "toTemplate",
+ "ccTemplate",
+ "bccTemplate",
+ "subjectTemplate",
+ {
+ "key": "bodyTemplate",
+ "type": "textarea",
+ "rows": 5
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/TimePluginDescriptor.json b/extensions-core/src/main/resources/TimePluginDescriptor.json
new file mode 100644
index 0000000..d357958
--- /dev/null
+++ b/extensions-core/src/main/resources/TimePluginDescriptor.json
@@ -0,0 +1,16 @@
+{
+ "schema": {
+ "title": "Time Plugin Configuration",
+ "type": "object",
+ "properties": {
+ "timeFormat": {
+ "title": "Time format",
+ "type": "string",
+ "default": "yyyy MM dd HH:mm:ss.SSS"
+ }
+ }
+ },
+ "form": [
+ "timeFormat"
+ ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java b/extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java
new file mode 100644
index 0000000..3095f5d
--- /dev/null
+++ b/extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java
@@ -0,0 +1,126 @@
+/**
+ * Copyright © 2016 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.extensions.core.filter;
+
+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.extensions.api.device.DeviceAttributes;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Andrew Shvayka
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class DeviceAttributesFilterTest {
+
+ @Mock
+ RuleContext ruleCtx;
+
+ private static JsFilterConfiguration wrap(String filterBody) {
+ return new JsFilterConfiguration(filterBody);
+ }
+
+ @Test
+ public void basicMissingAttributesTest() {
+ DeviceAttributesFilter filter = new DeviceAttributesFilter();
+ filter.init(wrap("((typeof nonExistingVal === 'undefined') || nonExistingVal == true) && booleanValue == false"));
+ List<AttributeKvEntry> clientAttributes = new ArrayList<>();
+ clientAttributes.add(new BaseAttributeKvEntry(new BooleanDataEntry("booleanValue", false), 42));
+ DeviceAttributes attributes = new DeviceAttributes(clientAttributes, new ArrayList<>(), new ArrayList<>());
+
+ Mockito.when(ruleCtx.getDeviceAttributes()).thenReturn(attributes);
+ Assert.assertTrue(filter.filter(ruleCtx, null));
+ filter.stop();
+ }
+
+ @Test
+ public void basicClientAttributesTest() {
+ DeviceAttributesFilter filter = new DeviceAttributesFilter();
+ filter.init(wrap("doubleValue == 1.0 && booleanValue == false"));
+ List<AttributeKvEntry> clientAttributes = new ArrayList<>();
+ clientAttributes.add(new BaseAttributeKvEntry(new DoubleDataEntry("doubleValue", 1.0), 42));
+ clientAttributes.add(new BaseAttributeKvEntry(new BooleanDataEntry("booleanValue", false), 42));
+ DeviceAttributes attributes = new DeviceAttributes(clientAttributes, new ArrayList<>(), new ArrayList<>());
+
+ Mockito.when(ruleCtx.getDeviceAttributes()).thenReturn(attributes);
+ Assert.assertTrue(filter.filter(ruleCtx, null));
+ filter.stop();
+ }
+
+ @Test(timeout = 10000)
+ public void basicClientAttributesStressTest() {
+ DeviceAttributesFilter filter = new DeviceAttributesFilter();
+ filter.init(wrap("doubleValue == 1.0 && booleanValue == false"));
+
+ List<AttributeKvEntry> clientAttributes = new ArrayList<>();
+ clientAttributes.add(new BaseAttributeKvEntry(new DoubleDataEntry("doubleValue", 1.0), 42));
+ clientAttributes.add(new BaseAttributeKvEntry(new BooleanDataEntry("booleanValue", false), 42));
+ DeviceAttributes attributes = new DeviceAttributes(clientAttributes, new ArrayList<>(), new ArrayList<>());
+
+ Mockito.when(ruleCtx.getDeviceAttributes()).thenReturn(attributes);
+
+ for (int i = 0; i < 10000; i++) {
+ Assert.assertTrue(filter.filter(ruleCtx, null));
+ }
+ filter.stop();
+ }
+
+ @Test
+ public void basicServerAttributesTest() {
+ DeviceAttributesFilter filter = new DeviceAttributesFilter();
+ filter.init(wrap("doubleValue == 1.0 && booleanValue == false"));
+
+ List<AttributeKvEntry> serverAttributes = new ArrayList<>();
+ serverAttributes.add(new BaseAttributeKvEntry(new DoubleDataEntry("doubleValue", 1.0), 42));
+ serverAttributes.add(new BaseAttributeKvEntry(new BooleanDataEntry("booleanValue", false), 42));
+ DeviceAttributes attributes = new DeviceAttributes(new ArrayList<>(), serverAttributes, new ArrayList<>());
+
+ Mockito.when(ruleCtx.getDeviceAttributes()).thenReturn(attributes);
+ Assert.assertTrue(filter.filter(ruleCtx, null));
+ filter.stop();
+ }
+
+ @Test
+ public void basicConflictServerAttributesTest() {
+ DeviceAttributesFilter filter = new DeviceAttributesFilter();
+ filter.init(wrap("cs.doubleValue == 1.0 && cs.booleanValue == true && ss.doubleValue == 0.0 && ss.booleanValue == false"));
+
+ List<AttributeKvEntry> clientAttributes = new ArrayList<>();
+ clientAttributes.add(new BaseAttributeKvEntry(new DoubleDataEntry("doubleValue", 1.0), 42));
+ clientAttributes.add(new BaseAttributeKvEntry(new BooleanDataEntry("booleanValue", true), 42));
+
+ List<AttributeKvEntry> serverAttributes = new ArrayList<>();
+ serverAttributes.add(new BaseAttributeKvEntry(new DoubleDataEntry("doubleValue", 0.0), 42));
+ serverAttributes.add(new BaseAttributeKvEntry(new BooleanDataEntry("booleanValue", false), 42));
+ DeviceAttributes attributes = new DeviceAttributes(clientAttributes, serverAttributes, new ArrayList<>());
+
+ Mockito.when(ruleCtx.getDeviceAttributes()).thenReturn(attributes);
+ Assert.assertTrue(filter.filter(ruleCtx, null));
+ filter.stop();
+ }
+
+}
LICENSE 201(+201 -0)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c8f142f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2016 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.
license-header-template.txt 13(+13 -0)
diff --git a/license-header-template.txt b/license-header-template.txt
new file mode 100644
index 0000000..fd245c1
--- /dev/null
+++ b/license-header-template.txt
@@ -0,0 +1,13 @@
+Copyright © ${project.inceptionYear} ${owner}
+
+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.
pom.xml 710(+710 -0)
diff --git a/pom.xml b/pom.xml
new file mode 100755
index 0000000..bb5dab2
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,710 @@
+<!--
+
+ Copyright © 2016 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>
+ <groupId>org.thingsboard</groupId>
+ <artifactId>server</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+ <packaging>pom</packaging>
+
+ <name>Thingsboard Server Components</name>
+ <url>http://thingsboard.org</url>
+ <inceptionYear>2016</inceptionYear>
+
+ <properties>
+ <main.dir>${basedir}</main.dir>
+ <spring-boot.version>1.4.2.RELEASE</spring-boot.version>
+ <spring.version>4.3.4.RELEASE</spring.version>
+ <spring-security.version>4.2.0.RELEASE</spring-security.version>
+ <jjwt.version>0.7.0</jjwt.version>
+ <joda-time.version>2.4</joda-time.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.1.7</logback.version>
+ <mockito.version>1.9.5</mockito.version>
+ <rat.version>0.10</rat.version>
+ <cassandra.version>3.0.0</cassandra.version>
+ <cassandra-unit.version>3.0.0.1</cassandra-unit.version>
+ <takari-cpsuite.version>1.2.7</takari-cpsuite.version>
+ <guava.version>18.0</guava.version>
+ <commons-lang3.version>3.4</commons-lang3.version>
+ <commons-validator.version>1.5.0</commons-validator.version>
+ <jackson.version>2.7.3</jackson.version>
+ <json-schema-validator.version>2.2.6</json-schema-validator.version>
+ <scala.version>2.11</scala.version>
+ <akka.version>2.4.2</akka.version>
+ <californium.version>1.0.2</californium.version>
+ <gson.version>2.6.2</gson.version>
+ <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>
+ <protobuf.version>3.0.2</protobuf.version>
+ <grpc.version>1.0.0</grpc.version>
+ <lombok.version>1.16.10</lombok.version>
+ <paho.client.version>1.1.0</paho.client.version>
+ <netty.version>4.1.3.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>
+ </properties>
+
+ <modules>
+ <module>common</module>
+ <module>dao</module>
+ <module>extensions-api</module>
+ <module>extensions-core</module>
+ <module>extensions</module>
+ <module>transport</module>
+ <module>ui</module>
+ <module>tools</module>
+ <module>application</module>
+ </modules>
+
+ <profiles>
+ <profile>
+ <id>default</id>
+ <activation>
+ <activeByDefault>true</activeByDefault>
+ </activation>
+ </profile>
+ </profiles>
+
+ <build>
+ <extensions>
+ <extension>
+ <groupId>kr.motd.maven</groupId>
+ <artifactId>os-maven-plugin</artifactId>
+ <version>1.5.0.Final</version>
+ </extension>
+ </extensions>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>2.5.1</version>
+ <configuration>
+ <source>1.8</source>
+ <target>1.8</target>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-resources-plugin</artifactId>
+ <version>2.7</version>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <version>2.2.1</version>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <version>3.0.2</version>
+ </plugin>
+ <plugin>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-maven-plugin</artifactId>
+ <version>${spring-boot.version}</version>
+ </plugin>
+ <plugin>
+ <groupId>org.fortasoft</groupId>
+ <artifactId>gradle-maven-plugin</artifactId>
+ <version>1.0.8</version>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-dependency-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>copy-protoc</id>
+ <phase>generate-sources</phase>
+ <goals>
+ <goal>copy</goal>
+ </goals>
+ <configuration>
+ <artifactItems>
+ <artifactItem>
+ <groupId>com.google.protobuf</groupId>
+ <artifactId>protoc</artifactId>
+ <version>${protobuf.version}</version>
+ <classifier>${os.detected.classifier}</classifier>
+ <type>exe</type>
+ <overWrite>true</overWrite>
+ <outputDirectory>${project.build.directory}</outputDirectory>
+ </artifactItem>
+ </artifactItems>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.xolstice.maven.plugins</groupId>
+ <artifactId>protobuf-maven-plugin</artifactId>
+ <version>0.5.0</version>
+ <configuration>
+ <!--
+ The version of protoc must match protobuf-java. If you don't depend on
+ protobuf-java directly, you will be transitively depending on the
+ protobuf-java version that grpc depends on.
+ -->
+ <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
+ </protocArtifact>
+ <pluginId>grpc-java</pluginId>
+ <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.0.0:exe:${os.detected.classifier}
+ </pluginArtifact>
+ </configuration>
+ <executions>
+ <execution>
+ <goals>
+ <goal>compile</goal>
+ <goal>compile-custom</goal>
+ <goal>test-compile</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>build-helper-maven-plugin</artifactId>
+ <version>1.12</version>
+ <executions>
+ <execution>
+ <id>add-source</id>
+ <phase>generate-sources</phase>
+ <goals>
+ <goal>add-source</goal>
+ </goals>
+ <configuration>
+ <sources>
+ <source>${basedir}/target/generated-sources</source>
+ </sources>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.eclipse.m2e</groupId>
+ <artifactId>lifecycle-mapping</artifactId>
+ <version>1.0.0</version>
+ <configuration>
+ <lifecycleMappingMetadata>
+ <pluginExecutions>
+ <pluginExecution>
+ <pluginExecutionFilter>
+ <groupId>
+ org.apache.maven.plugins
+ </groupId>
+ <artifactId>
+ maven-antrun-plugin
+ </artifactId>
+ <versionRange>
+ [1.3,)
+ </versionRange>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ </pluginExecutionFilter>
+ <action>
+ <ignore></ignore>
+ </action>
+ </pluginExecution>
+ </pluginExecutions>
+ </lifecycleMappingMetadata>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>com.mycila</groupId>
+ <artifactId>license-maven-plugin</artifactId>
+ <version>3.0</version>
+ <configuration>
+ <header>${main.dir}/license-header-template.txt</header>
+ <properties>
+ <owner>The Thingsboard Authors</owner>
+ </properties>
+ <excludes>
+ <exclude>**/.env</exclude>
+ <exclude>**/.eslintrc</exclude>
+ <exclude>**/.babelrc</exclude>
+ <exclude>**/.jshintrc</exclude>
+ <exclude>**/.gradle/**</exclude>
+ <exclude>**/nightwatch</exclude>
+ <exclude>**/README</exclude>
+ <exclude>**/LICENSE</exclude>
+ <exclude>**/banner.txt</exclude>
+ <exclude>node_modules/**</exclude>
+ <exclude>**/*.properties</exclude>
+ <exclude>src/test/resources/**</exclude>
+ <exclude>src/vendor/**</exclude>
+ <exclude>src/font/**</exclude>
+ <exclude>src/sh/**</exclude>
+ <exclude>src/main/scripts/control/**</exclude>
+ </excludes>
+ <mapping>
+ <proto>JAVADOC_STYLE</proto>
+ <cql>DOUBLEDASHES_STYLE</cql>
+ <scss>JAVADOC_STYLE</scss>
+ <jsx>SLASHSTAR_STYLE</jsx>
+ <conf>SCRIPT_STYLE</conf>
+ <gradle>JAVADOC_STYLE</gradle>
+ </mapping>
+ </configuration>
+ <executions>
+ <execution>
+ <goals>
+ <goal>check</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>com.mycila</groupId>
+ <artifactId>license-maven-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>extensions-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>extensions-core</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server.extensions</groupId>
+ <artifactId>extension-rabbitmq</artifactId>
+ <classifier>extension</classifier>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server.extensions</groupId>
+ <artifactId>extension-rest-api-call</artifactId>
+ <classifier>extension</classifier>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server.extensions</groupId>
+ <artifactId>extension-kafka</artifactId>
+ <classifier>extension</classifier>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server.common</groupId>
+ <artifactId>data</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server.common</groupId>
+ <artifactId>message</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server.common</groupId>
+ <artifactId>transport</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server.transport</groupId>
+ <artifactId>http</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server.transport</groupId>
+ <artifactId>coap</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server.transport</groupId>
+ <artifactId>mqtt</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>dao</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>dao</artifactId>
+ <version>${project.version}</version>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-security</artifactId>
+ <version>${spring-boot.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ <version>${spring-boot.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-websocket</artifactId>
+ <version>${spring-boot.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-autoconfigure</artifactId>
+ <version>${spring-boot.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-test</artifactId>
+ <version>${spring-boot.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-context</artifactId>
+ <version>${spring.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-context-support</artifactId>
+ <version>${spring.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-tx</artifactId>
+ <version>${spring.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-web</artifactId>
+ <version>${spring.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.security</groupId>
+ <artifactId>spring-security-test</artifactId>
+ <version>${spring-security.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.jsonwebtoken</groupId>
+ <artifactId>jjwt</artifactId>
+ <version>${jjwt.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
+ <version>${joda-time.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.velocity</groupId>
+ <artifactId>velocity</artifactId>
+ <version>${velocity.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.velocity</groupId>
+ <artifactId>velocity-tools</artifactId>
+ <version>${velocity-tools.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>javax.servlet</groupId>
+ <artifactId>servlet-api</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>com.rabbitmq</groupId>
+ <artifactId>amqp-client</artifactId>
+ <version>${rabbitmq.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>javax.mail</groupId>
+ <artifactId>mail</artifactId>
+ <version>${mail.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.curator</groupId>
+ <artifactId>curator-recipes</artifactId>
+ <version>${curator.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.curator</groupId>
+ <artifactId>curator-test</artifactId>
+ <scope>test</scope>
+ <version>${curator.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.jayway.jsonpath</groupId>
+ <artifactId>json-path</artifactId>
+ <version>${json-path.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.jayway.jsonpath</groupId>
+ <artifactId>json-path-assert</artifactId>
+ <version>${json-path.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-all</artifactId>
+ <version>${netty.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-handler</artifactId>
+ <version>${netty.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.datastax.cassandra</groupId>
+ <artifactId>cassandra-driver-core</artifactId>
+ <version>${cassandra.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.datastax.cassandra</groupId>
+ <artifactId>cassandra-driver-mapping</artifactId>
+ <version>${cassandra.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.datastax.cassandra</groupId>
+ <artifactId>cassandra-driver-extras</artifactId>
+ <version>${cassandra.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ <version>${commons-lang3.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>commons-validator</groupId>
+ <artifactId>commons-validator</artifactId>
+ <version>${commons-validator.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <version>${jackson.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.github.fge</groupId>
+ <artifactId>json-schema-validator</artifactId>
+ <version>${json-schema-validator.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.typesafe.akka</groupId>
+ <artifactId>akka-actor_${scala.version}</artifactId>
+ <version>${akka.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.typesafe.akka</groupId>
+ <artifactId>akka-slf4j_${scala.version}</artifactId>
+ <version>${akka.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.californium</groupId>
+ <artifactId>californium-core</artifactId>
+ <version>${californium.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ <version>${gson.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <version>${slf4j.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>log4j-over-slf4j</artifactId>
+ <version>${slf4j.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>jul-to-slf4j</artifactId>
+ <version>${slf4j.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-core</artifactId>
+ <version>${logback.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ <version>${logback.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <version>${guava.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.protobuf</groupId>
+ <artifactId>protobuf-java</artifactId>
+ <version>${protobuf.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-netty</artifactId>
+ <version>${grpc.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-protobuf</artifactId>
+ <version>${grpc.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-stub</artifactId>
+ <version>${grpc.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-test</artifactId>
+ <version>${spring.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.takari.junit</groupId>
+ <artifactId>takari-cpsuite</artifactId>
+ <version>${takari-cpsuite.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.cassandraunit</groupId>
+ <artifactId>cassandra-unit</artifactId>
+ <version>${cassandra-unit.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>${junit.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <version>${mockito.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ <version>${lombok.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.kafka</groupId>
+ <artifactId>kafka_2.10</artifactId>
+ <version>${kafka.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.paho</groupId>
+ <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
+ <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>
+ </dependencies>
+ </dependencyManagement>
+
+ <repositories>
+ <repository>
+ <id>central</id>
+ <url>http://repo1.maven.org/maven2/</url>
+ </repository>
+ <repository>
+ <id>spring-snapshots</id>
+ <name>Spring Snapshots</name>
+ <url>https://repo.spring.io/snapshot</url>
+ <snapshots>
+ <enabled>true</enabled>
+ </snapshots>
+ </repository>
+ <repository>
+ <id>spring-milestones</id>
+ <name>Spring Milestones</name>
+ <url>https://repo.spring.io/milestone</url>
+ <snapshots>
+ <enabled>false</enabled>
+ </snapshots>
+ </repository>
+ <repository>
+ <id>typesafe</id>
+ <name>Typesafe Repository</name>
+ <url>http://repo.typesafe.com/typesafe/releases/</url>
+ </repository>
+ <repository>
+ <id>sonatype</id>
+ <url>https://oss.sonatype.org/content/groups/public</url>
+ </repository>
+ </repositories>
+
+</project>
README.md 29(+29 -0)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..dfd8735
--- /dev/null
+++ b/README.md
@@ -0,0 +1,29 @@
+# iotrules
+IoT Rules Engine
+
+**Docker usage**
+
+**start platfrom using docker:**
+- install docker
+- cd to 'docker' folder
+- create folder for cassandra data directory on your local env (host)
+ - `mkdir /home/user/data_dir`
+- modify .env file to point to the directory created in previous step
+- start ./deploy.sh script to run all the services
+
+
+**start-up for local development**
+
+cassandra with thingsboard schema (9042 and 9061 ports are exposed).
+zookeper services (2181 port is exposed).
+9042, 9061 and 2181 ports must be free so 'Thingsboard' server that is running outside docker container is able to connect to services.
+you can change these ports in docker-compose.static.yml file to some others, but 'Thingsbaord' application.yml file must be updated accordingly.
+if you would like to change cassandra port, change it to "9999:9042" for example and update cassandra.node_list entry in application.yml file to localhost:9999.
+
+- install docker
+- cd to 'docker' folder
+- create folder for cassandra data directory on your local env (host)
+ - `mkdir /home/user/data_dir`
+- modify .env file to point to the directory created in previous step
+- start ./deploy_cassandra_zookeeper.sh script to run cassandra with thingsboard schema and zookeper services
+- Start boot class: _org.thingsboard.server.ThingsboardServerApplication_
tools/pom.xml 83(+83 -0)
diff --git a/tools/pom.xml b/tools/pom.xml
new file mode 100644
index 0000000..f022ac5
--- /dev/null
+++ b/tools/pom.xml
@@ -0,0 +1,83 @@
+<!--
+
+ Copyright © 2016 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>0.0.1-SNAPSHOT</version>
+ <artifactId>server</artifactId>
+ </parent>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>tools</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Thingsboard Server Tools</name>
+ <url>http://thingsboard.org</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <main.dir>${basedir}/..</main.dir>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.thingsboard.server.common</groupId>
+ <artifactId>data</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.paho</groupId>
+ <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>log4j-over-slf4j</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-test</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ </dependencies>
+</project>
diff --git a/tools/src/main/java/MqttStressTestClient.java b/tools/src/main/java/MqttStressTestClient.java
new file mode 100644
index 0000000..8202f94
--- /dev/null
+++ b/tools/src/main/java/MqttStressTestClient.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright © 2016 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 lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.*;
+import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class MqttStressTestClient {
+
+ @Getter
+ private final String deviceToken;
+ @Getter
+ private final String clientId;
+ private final MqttClientPersistence persistence;
+ private final MqttAsyncClient client;
+ private final ResultAccumulator results;
+
+ public MqttStressTestClient(ResultAccumulator results, String brokerUri, String deviceToken) throws MqttException {
+ this.results = results;
+ this.clientId = MqttAsyncClient.generateClientId();
+ this.deviceToken = deviceToken;
+ this.persistence = new MemoryPersistence();
+ this.client = new MqttAsyncClient(brokerUri, clientId, persistence);
+ }
+
+ public void connect() throws MqttException {
+ MqttConnectOptions options = new MqttConnectOptions();
+ options.setUserName(deviceToken);
+ client.connect(options, null, new IMqttActionListener() {
+ @Override
+ public void onSuccess(IMqttToken iMqttToken) {
+ log.info("OnSuccess");
+ }
+
+ @Override
+ public void onFailure(IMqttToken iMqttToken, Throwable e) {
+ log.info("OnFailure", e);
+ }
+ });
+ }
+
+ public void disconnect() throws MqttException {
+ client.disconnect();
+ }
+
+ public void publishTelemetry(byte[] data) throws MqttException {
+ long sendTime = System.currentTimeMillis();
+ MqttMessage msg = new MqttMessage(data);
+ client.publish("v1/devices/me/telemetry", msg, null, new IMqttActionListener() {
+ @Override
+ public void onSuccess(IMqttToken asyncActionToken) {
+ long ackTime = System.currentTimeMillis();
+// log.info("Delivery time: {}", ackTime - sendTime);
+ results.onResult(true, ackTime - sendTime);
+ }
+
+ @Override
+ public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
+ long failTime = System.currentTimeMillis();
+// log.info("Failure time: {}", failTime - sendTime);
+ results.onResult(false, failTime - sendTime);
+ }
+ });
+ }
+}
tools/src/main/java/MqttStressTestTool.java 88(+88 -0)
diff --git a/tools/src/main/java/MqttStressTestTool.java b/tools/src/main/java/MqttStressTestTool.java
new file mode 100644
index 0000000..4cb20f7
--- /dev/null
+++ b/tools/src/main/java/MqttStressTestTool.java
@@ -0,0 +1,88 @@
+/**
+ * Copyright © 2016 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 lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class MqttStressTestTool {
+
+ private static final long TEST_DURATION = TimeUnit.MINUTES.toMillis(1);
+ private static final long TEST_ITERATION = TimeUnit.MILLISECONDS.toMillis(100);
+ private static final long TEST_SUB_ITERATION = TimeUnit.MILLISECONDS.toMillis(2);
+ private static final int DEVICE_COUNT = 100;
+ private static final String BASE_URL = "http://localhost:8080";
+ private static final String[] MQTT_URLS = {"tcp://localhost:1883"};
+// private static final String[] MQTT_URLS = {"tcp://localhost:1883", "tcp://localhost:1884", "tcp://localhost:1885"};
+ private static final String USERNAME = "tenant@thingsboard.org";
+ private static final String PASSWORD = "tenant";
+
+
+ public static void main(String[] args) throws Exception {
+ ResultAccumulator results = new ResultAccumulator();
+
+ AtomicLong value = new AtomicLong(Long.MAX_VALUE);
+ log.info("value: {} ", value.incrementAndGet());
+
+ RestClient restClient = new RestClient(BASE_URL);
+ restClient.login(USERNAME, PASSWORD);
+
+ List<MqttStressTestClient> clients = new ArrayList<>();
+ for (int i = 0; i < DEVICE_COUNT; i++) {
+ Device device = restClient.createDevice("Device " + i);
+ DeviceCredentials credentials = restClient.getCredentials(device.getId());
+ String mqttURL = MQTT_URLS[i % MQTT_URLS.length];
+ MqttStressTestClient client = new MqttStressTestClient(results, mqttURL, credentials.getCredentialsId());
+ client.connect();
+ clients.add(client);
+ }
+ Thread.sleep(1000);
+
+
+ byte[] data = "{\"longKey\":73}".getBytes(StandardCharsets.UTF_8);
+ long startTime = System.currentTimeMillis();
+ int iterationsCount = (int) (TEST_DURATION / TEST_ITERATION);
+ int subIterationsCount = (int) (TEST_ITERATION / TEST_SUB_ITERATION);
+ if (clients.size() % subIterationsCount != 0) {
+ throw new IllegalArgumentException("Invalid parameter exception!");
+ }
+ for (int i = 0; i < iterationsCount; i++) {
+ for (int j = 0; j < subIterationsCount; j++) {
+ int packSize = clients.size() / subIterationsCount;
+ for (int k = 0; k < packSize; k++) {
+ int clientIndex = packSize * j + k;
+ clients.get(clientIndex).publishTelemetry(data);
+ }
+ Thread.sleep(TEST_SUB_ITERATION);
+ }
+ }
+ Thread.sleep(1000);
+ for (MqttStressTestClient client : clients) {
+ client.disconnect();
+ }
+ log.info("Results: {} took {}ms", results, System.currentTimeMillis() - startTime);
+ }
+
+}
tools/src/main/java/RestClient.java 72(+72 -0)
diff --git a/tools/src/main/java/RestClient.java b/tools/src/main/java/RestClient.java
new file mode 100644
index 0000000..4a7a88b
--- /dev/null
+++ b/tools/src/main/java/RestClient.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright © 2016 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 com.fasterxml.jackson.databind.JsonNode;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpRequest;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.ClientHttpRequestExecution;
+import org.springframework.http.client.ClientHttpRequestInterceptor;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.http.client.support.HttpRequestWrapper;
+import org.springframework.web.client.RestTemplate;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Andrew Shvayka
+ */
+@RequiredArgsConstructor
+public class RestClient implements ClientHttpRequestInterceptor {
+ private static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
+ private final RestTemplate restTemplate = new RestTemplate();
+ private String token;
+ private final String baseURL;
+
+ public void login(String username, String password) {
+ Map<String, String> loginRequest = new HashMap<>();
+ loginRequest.put("username", username);
+ loginRequest.put("password", password);
+ ResponseEntity<JsonNode> tokenInfo = restTemplate.postForEntity(baseURL + "/api/auth/login", loginRequest, JsonNode.class);
+ this.token = tokenInfo.getBody().get("token").asText();
+ restTemplate.setInterceptors(Collections.singletonList(this));
+ }
+
+ public Device createDevice(String name) {
+ Device device = new Device();
+ device.setName(name);
+ return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody();
+ }
+
+ public DeviceCredentials getCredentials(DeviceId id) {
+ return restTemplate.getForEntity(baseURL + "/api/device/" + id.getId().toString() + "/credentials", DeviceCredentials.class).getBody();
+ }
+
+ @Override
+ public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException {
+ HttpRequest wrapper = new HttpRequestWrapper(request);
+ wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token);
+ return execution.execute(wrapper, bytes);
+ }
+
+}
tools/src/main/java/ResultAccumulator.java 85(+85 -0)
diff --git a/tools/src/main/java/ResultAccumulator.java b/tools/src/main/java/ResultAccumulator.java
new file mode 100644
index 0000000..e7352ea
--- /dev/null
+++ b/tools/src/main/java/ResultAccumulator.java
@@ -0,0 +1,85 @@
+/**
+ * Copyright © 2016 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 lombok.extern.slf4j.Slf4j;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class ResultAccumulator {
+
+ private AtomicLong minTime = new AtomicLong(Long.MAX_VALUE);
+ private AtomicLong maxTime = new AtomicLong(Long.MIN_VALUE);
+ private AtomicLong timeSpentCount = new AtomicLong();
+ private AtomicInteger successCount = new AtomicInteger();
+ private AtomicInteger errorCount = new AtomicInteger();
+
+ public void onResult(boolean success, long timeSpent) {
+ if (success) {
+ successCount.incrementAndGet();
+ } else {
+ errorCount.incrementAndGet();
+ }
+ timeSpentCount.addAndGet(timeSpent);
+
+ while (!setMax(timeSpent)) ;
+ while (!setMin(timeSpent)) ;
+ }
+
+ private boolean setMax(long timeSpent) {
+ long curMax = maxTime.get();
+ long newMax = Math.max(curMax, timeSpent);
+ return maxTime.compareAndSet(curMax, newMax);
+ }
+
+ private boolean setMin(long timeSpent) {
+ long curMin = minTime.get();
+ long newMin = Math.min(curMin, timeSpent);
+ return minTime.compareAndSet(curMin, newMin);
+ }
+
+
+ public int getSuccessCount() {
+ return successCount.get();
+ }
+
+ public int getErrorCount() {
+ return errorCount.get();
+ }
+
+ public long getTimeSpent() {
+ return timeSpentCount.get();
+ }
+
+ public double getAvgTimeSpent() {
+ return ((double) getTimeSpent()) / (getSuccessCount() + getErrorCount());
+ }
+
+ @Override
+ public String toString() {
+ return "ResultAccumulator{" +
+ "successCount=" + getSuccessCount() +
+ ", errorCount=" + getErrorCount() +
+ ", totalTime=" + getTimeSpent() +
+ ", avgTime=" + getAvgTimeSpent() +
+ ", minTime=" + minTime.get() +
+ ", maxTime=" + maxTime.get() +
+ '}';
+ }
+}
diff --git a/tools/src/main/shell/keygen.properties b/tools/src/main/shell/keygen.properties
new file mode 100644
index 0000000..6016bda
--- /dev/null
+++ b/tools/src/main/shell/keygen.properties
@@ -0,0 +1,8 @@
+HOSTNAME="$(hostname)"
+PASSWORD="password"
+
+CLIENT_TRUSTSTORE="client_truststore.crt"
+
+SERVER_KEY_ALIAS="serveralias"
+SERVER_FILE_PREFIX="mqttserver"
+SERVER_KEYSTORE_DIR="../../../../application/src/main/resources/keystore/"
\ No newline at end of file
tools/src/main/shell/keygen.sh 57(+57 -0)
diff --git a/tools/src/main/shell/keygen.sh b/tools/src/main/shell/keygen.sh
new file mode 100755
index 0000000..25b3157
--- /dev/null
+++ b/tools/src/main/shell/keygen.sh
@@ -0,0 +1,57 @@
+#!/bin/sh
+#
+# Copyright © 2016 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.
+#
+
+
+. keygen.properties
+
+echo "Generating SSL Key Pair..."
+
+keytool -genkeypair -v \
+ -alias $SERVER_KEY_ALIAS \
+ -dname "CN=$HOSTNAME, OU=Thingsboard, O=Thingsboard, L=Piscataway, ST=NJ, C=US" \
+ -keystore $SERVER_FILE_PREFIX.jks \
+ -keypass $PASSWORD \
+ -storepass $PASSWORD \
+ -keyalg RSA \
+ -keysize 2048 \
+ -validity 9999
+
+keytool -export \
+ -alias $SERVER_KEY_ALIAS \
+ -keystore $SERVER_FILE_PREFIX.jks \
+ -file $CLIENT_TRUSTSTORE -rfc \
+ -storepass $PASSWORD
+
+read -p "Do you want to copy $SERVER_FILE_PREFIX.jks to server directory? " yn
+ case $yn in
+ [Yy]) echo "Please, specify destination dir: "
+ read -p "(Default: $SERVER_KEYSTORE_DIR): " dir
+ if [[ ! -z $dir ]]; then
+ DESTINATION=$dir;
+ else
+ DESTINATION=$SERVER_KEYSTORE_DIR
+ fi;
+ cp $SERVER_FILE_PREFIX.jks $DESTINATION
+ if [ $? -ne 0 ]; then
+ echo "Failed to copy keystore file."
+ else
+ echo "File copied successfully."
+ fi
+ break;;
+ * ) ;;
+ esac
+echo "Done."
tools/src/main/shell/securemqttclient.py 55(+55 -0)
diff --git a/tools/src/main/shell/securemqttclient.py b/tools/src/main/shell/securemqttclient.py
new file mode 100644
index 0000000..a3fbf17
--- /dev/null
+++ b/tools/src/main/shell/securemqttclient.py
@@ -0,0 +1,55 @@
+#
+# Copyright © 2016 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 paho.mqtt.client as mqtt
+import ssl, socket
+
+# The callback for when the client receives a CONNACK response from the server.
+def on_connect(client, userdata, rc):
+ print('Connected with result code '+str(rc))
+ # Subscribing in on_connect() means that if we lose the connection and
+ # reconnect then subscriptions will be renewed.
+ client.subscribe('v1/devices/me/attributes')
+ client.subscribe('v1/devices/me/attributes/response/+')
+ client.subscribe('v1/devices/me/rpc/request/+')
+
+
+# The callback for when a PUBLISH message is received from the server.
+def on_message(client, userdata, msg):
+ print 'Topic: ' + msg.topic + '\nMessage: ' + str(msg.payload)
+ if msg.topic.startswith( 'v1/devices/me/rpc/request/'):
+ requestId = msg.topic[len('v1/devices/me/rpc/request/'):len(msg.topic)]
+ print 'This is a RPC call. RequestID: ' + requestId + '. Going to reply now!'
+ client.publish('v1/devices/me/rpc/response/' + requestId, "{\"value1\":\"A\", \"value2\":\"B\"}", 1)
+
+
+client = mqtt.Client()
+client.on_connect = on_connect
+client.on_message = on_message
+client.publish('v1/devices/me/attributes/request/1', "{\"clientKeys\":\"model\"}", 1)
+
+client.tls_set(ca_certs="client_truststore.crt", certfile=None, keyfile=None, cert_reqs=ssl.CERT_REQUIRED,
+ tls_version=ssl.PROTOCOL_TLSv1, ciphers=None);
+client.username_pw_set("TEST_TOKEN")
+client.tls_insecure_set(False)
+client.connect(socket.gethostname(), 1883, 1)
+
+
+# Blocking call that processes network traffic, dispatches callbacks and
+# handles reconnecting.
+# Other loop*() functions are available that give a threaded interface and a
+# manual interface.
+client.loop_forever()
tools/src/main/shell/simplemqttclient.py 50(+50 -0)
diff --git a/tools/src/main/shell/simplemqttclient.py b/tools/src/main/shell/simplemqttclient.py
new file mode 100644
index 0000000..166d374
--- /dev/null
+++ b/tools/src/main/shell/simplemqttclient.py
@@ -0,0 +1,50 @@
+#
+# Copyright © 2016 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 paho.mqtt.client as mqtt
+
+# The callback for when the client receives a CONNACK response from the server.
+def on_connect(client, userdata, rc):
+ print('Connected with result code '+str(rc))
+ # Subscribing in on_connect() means that if we lose the connection and
+ # reconnect then subscriptions will be renewed.
+ client.subscribe('v1/devices/me/attributes')
+ client.subscribe('v1/devices/me/attributes/response/+')
+ client.subscribe('v1/devices/me/rpc/request/+')
+
+
+# The callback for when a PUBLISH message is received from the server.
+def on_message(client, userdata, msg):
+ print 'Topic: ' + msg.topic + '\nMessage: ' + str(msg.payload)
+ if msg.topic.startswith( 'v1/devices/me/rpc/request/'):
+ requestId = msg.topic[len('v1/devices/me/rpc/request/'):len(msg.topic)]
+ print 'This is a RPC call. RequestID: ' + requestId + '. Going to reply now!'
+ client.publish('v1/devices/me/rpc/response/' + requestId, "{\"value1\":\"A\", \"value2\":\"B\"}", 1)
+
+
+client = mqtt.Client()
+client.on_connect = on_connect
+client.on_message = on_message
+client.publish('v1/devices/me/attributes/request/1', "{\"clientKeys\":\"model\"}", 1)
+
+client.username_pw_set("TEST_TOKEN")
+client.connect('127.0.0.1', 1883, 1)
+
+# Blocking call that processes network traffic, dispatches callbacks and
+# handles reconnecting.
+# Other loop*() functions are available that give a threaded interface and a
+# manual interface.
+client.loop_forever()
transport/coap/pom.xml 84(+84 -0)
diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml
new file mode 100644
index 0000000..6ddf64b
--- /dev/null
+++ b/transport/coap/pom.xml
@@ -0,0 +1,84 @@
+<!--
+
+ Copyright © 2016 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.server</groupId>
+ <version>0.0.1-SNAPSHOT</version>
+ <artifactId>transport</artifactId>
+ </parent>
+ <groupId>org.thingsboard.server.transport</groupId>
+ <artifactId>coap</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Thingsboard COAP Transport</name>
+ <url>http://thingsboard.org</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <main.dir>${basedir}/../..</main.dir>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.thingsboard.server.common</groupId>
+ <artifactId>transport</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.californium</groupId>
+ <artifactId>californium-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-context</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>log4j-over-slf4j</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java
new file mode 100644
index 0000000..0d64988
--- /dev/null
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap.adaptors;
+
+import org.eclipse.californium.core.coap.Request;
+import org.eclipse.californium.core.coap.Response;
+import org.thingsboard.server.common.transport.TransportAdaptor;
+import org.thingsboard.server.transport.coap.session.CoapSessionCtx;
+
+public interface CoapTransportAdaptor extends TransportAdaptor<CoapSessionCtx, Request, Response> {
+
+}
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
new file mode 100644
index 0000000..26a9056
--- /dev/null
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
@@ -0,0 +1,265 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap.adaptors;
+
+import java.util.*;
+
+import com.google.gson.JsonElement;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.coap.CoAP.ResponseCode;
+import org.eclipse.californium.core.coap.Request;
+import org.eclipse.californium.core.coap.Response;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.msg.core.*;
+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.SessionActorToAdaptorMsg;
+import org.thingsboard.server.common.msg.session.SessionContext;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.common.msg.session.ex.ProcessingTimeoutException;
+import org.thingsboard.server.common.transport.adaptor.AdaptorException;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+import org.springframework.stereotype.Component;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+import org.thingsboard.server.transport.coap.CoapTransportResource;
+import org.thingsboard.server.transport.coap.session.CoapSessionCtx;
+
+@Component("JsonCoapAdaptor")
+@Slf4j
+public class JsonCoapAdaptor implements CoapTransportAdaptor {
+
+ @Override
+ public AdaptorToSessionActorMsg convertToActorMsg(CoapSessionCtx ctx, MsgType type, Request inbound) throws AdaptorException {
+ FromDeviceMsg msg = null;
+ switch (type) {
+ case POST_TELEMETRY_REQUEST:
+ msg = convertToTelemetryUploadRequest(ctx, inbound);
+ break;
+ case POST_ATTRIBUTES_REQUEST:
+ msg = convertToUpdateAttributesRequest(ctx, inbound);
+ break;
+ case GET_ATTRIBUTES_REQUEST:
+ msg = convertToGetAttributesRequest(ctx, inbound);
+ break;
+ case SUBSCRIBE_RPC_COMMANDS_REQUEST:
+ msg = new RpcSubscribeMsg();
+ break;
+ case UNSUBSCRIBE_RPC_COMMANDS_REQUEST:
+ msg = new RpcUnsubscribeMsg();
+ break;
+ case TO_DEVICE_RPC_RESPONSE:
+ msg = convertToDeviceRpcResponse(ctx, inbound);
+ break;
+ case TO_SERVER_RPC_REQUEST:
+ msg = convertToServerRpcRequest(ctx, inbound);
+ break;
+ case SUBSCRIBE_ATTRIBUTES_REQUEST:
+ msg = new AttributesSubscribeMsg();
+ break;
+ case UNSUBSCRIBE_ATTRIBUTES_REQUEST:
+ msg = new AttributesUnsubscribeMsg();
+ break;
+ default:
+ log.warn("[{}] Unsupported msg type: {}!", ctx.getSessionId(), type);
+ throw new AdaptorException(new IllegalArgumentException("Unsupported msg type: " + type + "!"));
+ }
+ return new BasicAdaptorToSessionActorMsg(ctx, msg);
+ }
+
+ private FromDeviceMsg convertToDeviceRpcResponse(CoapSessionCtx ctx, Request inbound) throws AdaptorException {
+ Optional<Integer> requestId = CoapTransportResource.getRequestId(inbound);
+ String payload = validatePayload(ctx, inbound);
+ JsonObject response = new JsonParser().parse(payload).getAsJsonObject();
+ return new ToDeviceRpcResponseMsg(
+ requestId.orElseThrow(() -> new AdaptorException("Request id is missing!")),
+ response.get("response").toString());
+ }
+
+ private FromDeviceMsg convertToServerRpcRequest(CoapSessionCtx ctx, Request inbound) throws AdaptorException {
+
+ String payload = validatePayload(ctx, inbound);
+
+ return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), 0);
+ }
+
+ @Override
+ public Optional<Response> convertToAdaptorMsg(CoapSessionCtx ctx, SessionActorToAdaptorMsg source) throws AdaptorException {
+ ToDeviceMsg msg = source.getMsg();
+ switch (msg.getMsgType()) {
+ case STATUS_CODE_RESPONSE:
+ case TO_DEVICE_RPC_RESPONSE_ACK:
+ return Optional.of(convertStatusCodeResponse((StatusCodeResponse) msg));
+ case GET_ATTRIBUTES_RESPONSE:
+ return Optional.of(convertGetAttributesResponse((GetAttributesResponse) msg));
+ case ATTRIBUTES_UPDATE_NOTIFICATION:
+ return Optional.of(convertNotificationResponse(ctx, (AttributesUpdateNotification) msg));
+ case TO_DEVICE_RPC_REQUEST:
+ return Optional.of(convertToDeviceRpcRequest(ctx, (ToDeviceRpcRequestMsg) msg));
+ case TO_SERVER_RPC_RESPONSE:
+ return Optional.of(convertToServerRpcResponse(ctx, (ToServerRpcResponseMsg) msg));
+ 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() + "!"));
+ }
+ }
+
+ private Response convertToRuleEngineErrorResponse(CoapSessionCtx ctx, RuleEngineErrorMsg msg) {
+ ResponseCode status = ResponseCode.INTERNAL_SERVER_ERROR;
+ switch (msg.getError()) {
+ case PLUGIN_TIMEOUT:
+ status = ResponseCode.GATEWAY_TIMEOUT;
+ break;
+ default:
+ if (msg.getInMsgType() == MsgType.TO_SERVER_RPC_REQUEST) {
+ status = ResponseCode.BAD_REQUEST;
+ }
+ break;
+ }
+ Response response = new Response(status);
+ response.setPayload(JsonConverter.toErrorJson(msg.getErrorMsg()).toString());
+ return response;
+ }
+
+ private Response convertNotificationResponse(CoapSessionCtx ctx, AttributesUpdateNotification msg) {
+ return getObserveNotification(ctx, JsonConverter.toJson(msg.getData(), false));
+ }
+
+ private Response convertToDeviceRpcRequest(CoapSessionCtx ctx, ToDeviceRpcRequestMsg msg) {
+ return getObserveNotification(ctx, JsonConverter.toJson(msg, true));
+ }
+
+ private Response getObserveNotification(CoapSessionCtx ctx, JsonObject json) {
+ Response response = new Response(ResponseCode.CONTENT);
+ response.getOptions().setObserve(ctx.nextSeqNumber());
+ response.setPayload(json.toString());
+ return response;
+ }
+
+ private UpdateAttributesRequest convertToUpdateAttributesRequest(SessionContext ctx, Request inbound) throws AdaptorException {
+ String payload = validatePayload(ctx, inbound);
+ try {
+ return JsonConverter.convertToAttributes(new JsonParser().parse(payload));
+ } catch (IllegalStateException | JsonSyntaxException ex) {
+ throw new AdaptorException(ex);
+ }
+ }
+
+ private FromDeviceMsg convertToGetAttributesRequest(SessionContext ctx, Request inbound) throws AdaptorException {
+ List<String> queryElements = inbound.getOptions().getUriQuery();
+ if (queryElements == null || queryElements.size() == 0) {
+ log.warn("[{}] Query is empty!", ctx.getSessionId());
+ throw new AdaptorException(new IllegalArgumentException("Query is empty!"));
+ }
+
+ Set<String> clientKeys = toKeys(ctx, queryElements, "clientKeys");
+ Set<String> sharedKeys = toKeys(ctx, queryElements, "sharedKeys");
+ if (clientKeys.isEmpty() && sharedKeys.isEmpty()) {
+ throw new AdaptorException("No clientKeys and serverKeys parameters!");
+ }
+ return new BasicGetAttributesRequest(0, clientKeys, sharedKeys);
+ }
+
+ private Set<String> toKeys(SessionContext ctx, List<String> queryElements, String attributeName) throws AdaptorException {
+ String keys = null;
+ for (String queryElement : queryElements) {
+ String[] queryItem = queryElement.split("=");
+ if (queryItem.length == 2 && queryItem[0].equals(attributeName)) {
+ keys = queryItem[1];
+ }
+ }
+ if (!StringUtils.isEmpty(keys)) {
+ return new HashSet<>(Arrays.asList(keys.split(",")));
+ } else {
+ return Collections.emptySet();
+ }
+ }
+
+ private TelemetryUploadRequest convertToTelemetryUploadRequest(SessionContext ctx, Request inbound) throws AdaptorException {
+ String payload = validatePayload(ctx, inbound);
+ try {
+ return JsonConverter.convertToTelemetry(new JsonParser().parse(payload));
+ } catch (IllegalStateException | JsonSyntaxException ex) {
+ throw new AdaptorException(ex);
+ }
+ }
+
+ private Response convertStatusCodeResponse(StatusCodeResponse msg) {
+ if (msg.isSuccess()) {
+ Integer code = msg.getData().get();
+ if (code == 200) {
+ return new Response(ResponseCode.VALID);
+ } else {
+ return new Response(ResponseCode.CREATED);
+ }
+ } else {
+ return convertError(msg.getError().get());
+ }
+ }
+
+ private String validatePayload(SessionContext ctx, Request inbound) throws AdaptorException {
+ String payload = inbound.getPayloadString();
+ if (payload == null) {
+ log.warn("[{}] Payload is empty!", ctx.getSessionId());
+ throw new AdaptorException(new IllegalArgumentException("Payload is empty!"));
+ }
+ return payload;
+ }
+
+ private Response convertToServerRpcResponse(SessionContext ctx, ToServerRpcResponseMsg msg) {
+ if (msg.isSuccess()) {
+ Response response = new Response(ResponseCode.CONTENT);
+ JsonElement result = JsonConverter.toJson(msg);
+ response.setPayload(result.toString());
+ return response;
+ } else {
+ return convertError(new RuntimeException("Server RPC response is empty!"));
+ }
+ }
+
+ private Response convertGetAttributesResponse(GetAttributesResponse msg) {
+ if (msg.isSuccess()) {
+ AttributesKVMsg payload = msg.getData().get();
+ if (payload.getClientAttributes().isEmpty() && payload.getSharedAttributes().isEmpty()) {
+ return new Response(ResponseCode.NOT_FOUND);
+ } else {
+ Response response = new Response(ResponseCode.CONTENT);
+ JsonObject result = JsonConverter.toJson(payload, false);
+ response.setPayload(result.toString());
+ return response;
+ }
+ } else {
+ return convertError(msg.getError().get());
+ }
+ }
+
+ private Response convertError(Exception exception) {
+ log.warn("Converting exception: {}", exception.getMessage(), exception);
+ if (exception instanceof ProcessingTimeoutException) {
+ return new Response(ResponseCode.SERVICE_UNAVAILABLE);
+ } else {
+ return new Response(ResponseCode.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+}
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DeviceEmulator.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DeviceEmulator.java
new file mode 100644
index 0000000..bff0670
--- /dev/null
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DeviceEmulator.java
@@ -0,0 +1,179 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap.client;
+
+import java.io.IOException;
+import java.util.Random;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.CoapClient;
+import org.eclipse.californium.core.CoapHandler;
+import org.eclipse.californium.core.CoapResponse;
+import org.eclipse.californium.core.coap.MediaTypeRegistry;
+import org.thingsboard.server.common.msg.session.FeatureType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+@Slf4j
+public class DeviceEmulator {
+
+ public static final String SN = "SN-" + new Random().nextInt(1000);
+ public static final String MODEL = "Model " + new Random().nextInt(1000);
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ private final String host;
+ private final int port;
+ private final String token;
+
+ private CoapClient attributesClient;
+ private CoapClient telemetryClient;
+ private CoapClient rpcClient;
+ private String[] keys;
+ private ExecutorService executor = Executors.newFixedThreadPool(1);
+ private AtomicInteger seq = new AtomicInteger(100);
+
+ private DeviceEmulator(String host, int port, String token, String keys) {
+ this.host = host;
+ this.port = port;
+ this.token = token;
+ this.attributesClient = new CoapClient(getFeatureTokenUrl(host, port, token, FeatureType.ATTRIBUTES));
+ this.telemetryClient = new CoapClient(getFeatureTokenUrl(host, port, token, FeatureType.TELEMETRY));
+ this.rpcClient = new CoapClient(getFeatureTokenUrl(host, port, token, FeatureType.RPC));
+ this.keys = keys.split(",");
+ }
+
+ public void start() {
+ executor.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ sendObserveRequest(rpcClient);
+ while (!Thread.interrupted()) {
+
+
+ sendRequest(attributesClient, createAttributesRequest());
+ sendRequest(telemetryClient, createTelemetryRequest());
+
+ Thread.sleep(1000);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void sendRequest(CoapClient client, JsonNode request) throws JsonProcessingException {
+ CoapResponse telemetryResponse = client.setTimeout(60000).post(mapper.writeValueAsString(request),
+ MediaTypeRegistry.APPLICATION_JSON);
+ log.info("Response: {}, {}", telemetryResponse.getCode(), telemetryResponse.getResponseText());
+ }
+
+ private void sendObserveRequest(CoapClient client) throws JsonProcessingException {
+ client.observe(new CoapHandler() {
+ @Override
+ public void onLoad(CoapResponse coapResponse) {
+ log.info("Command: {}, {}", coapResponse.getCode(), coapResponse.getResponseText());
+ try {
+ JsonNode node = mapper.readTree(coapResponse.getResponseText());
+ int requestId = node.get("id").asInt();
+ String method = node.get("method").asText();
+ ObjectNode params = (ObjectNode) node.get("params");
+ ObjectNode response = mapper.createObjectNode();
+ response.put("id", requestId);
+ response.set("response", params);
+ log.info("Command Response: {}, {}", requestId, mapper.writeValueAsString(response));
+ CoapClient commandResponseClient = new CoapClient(getFeatureTokenUrl(host, port, token, FeatureType.RPC));
+ commandResponseClient.post(new CoapHandler() {
+ @Override
+ public void onLoad(CoapResponse response) {
+ log.info("Command Response Ack: {}, {}", response.getCode(), response.getResponseText());
+ }
+
+ @Override
+ public void onError() {
+
+ }
+ }, mapper.writeValueAsString(response), MediaTypeRegistry.APPLICATION_JSON);
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onError() {
+
+ }
+ });
+ }
+
+ });
+ }
+
+ private ObjectNode createAttributesRequest() {
+ ObjectNode element = mapper.createObjectNode();
+ element.put("serialNumber", SN);
+ element.put("model", MODEL);
+ return element;
+ }
+
+ private ArrayNode createTelemetryRequest() {
+ ArrayNode rootNode = mapper.createArrayNode();
+ for (String key : keys) {
+ ObjectNode element = mapper.createObjectNode();
+ element.put(key, seq.incrementAndGet());
+ rootNode.add(element);
+ }
+ return rootNode;
+ }
+
+ protected void stop() {
+ executor.shutdownNow();
+ }
+
+ public static void main(String args[]) {
+ if (args.length != 4) {
+ System.out.println("Usage: java -jar " + DeviceEmulator.class.getSimpleName() + ".jar host port device_token keys");
+ }
+ final DeviceEmulator emulator = new DeviceEmulator(args[0], Integer.parseInt(args[1]), args[2], args[3]);
+ emulator.start();
+ Runtime.getRuntime().addShutdownHook(new Thread() {
+ @Override
+ public void run() {
+ emulator.stop();
+ }
+ });
+ }
+
+
+ private String getFeatureTokenUrl(String host, int port, String token, FeatureType featureType) {
+ return getBaseUrl(host, port) + token + "/" + featureType.name().toLowerCase();
+ }
+
+ private String getBaseUrl(String host, int port) {
+ return "coap://" + host + ":" + port + "/api/v1/";
+ }
+
+}
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
new file mode 100644
index 0000000..32ea0f5
--- /dev/null
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
@@ -0,0 +1,222 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap;
+
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.Optional;
+
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.CoapResource;
+import org.eclipse.californium.core.coap.CoAP.ResponseCode;
+import org.eclipse.californium.core.coap.Request;
+import org.eclipse.californium.core.network.Exchange;
+import org.eclipse.californium.core.network.ExchangeObserver;
+import org.eclipse.californium.core.server.resources.CoapExchange;
+import org.eclipse.californium.core.server.resources.Resource;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.data.security.DeviceCredentialsFilter;
+import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
+import org.thingsboard.server.common.msg.session.*;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.common.transport.adaptor.AdaptorException;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+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
+public class CoapTransportResource extends CoapResource {
+ // coap://localhost:port/api/v1/DEVICE_TOKEN/[attributes|telemetry|rpc[/requestId]]
+ private static final int ACCESS_TOKEN_POSITION = 3;
+ private static final int FEATURE_TYPE_POSITION = 4;
+ private static final int REQUEST_ID_POSITION = 5;
+
+ private final CoapTransportAdaptor adaptor;
+ private final SessionMsgProcessor processor;
+ private final DeviceAuthService authService;
+ private final Field observerField;
+ private final long timeout;
+
+ public CoapTransportResource(SessionMsgProcessor processor, DeviceAuthService authService, CoapTransportAdaptor adaptor, String name, long timeout) {
+ super(name);
+ this.processor = processor;
+ this.authService = authService;
+ this.adaptor = adaptor;
+ this.timeout = timeout;
+ // This is important to turn off existing observable logic in
+ // CoapResource. We will have our own observe monitoring due to 1:1
+ // observe relationship.
+ this.setObservable(false);
+ observerField = ReflectionUtils.findField(Exchange.class, "observer");
+ observerField.setAccessible(true);
+ }
+
+ @Override
+ public void handleGET(CoapExchange exchange) {
+ Optional<FeatureType> featureType = getFeatureType(exchange.advanced().getRequest());
+ if (!featureType.isPresent()) {
+ log.trace("Missing feature type parameter");
+ exchange.respond(ResponseCode.BAD_REQUEST);
+ } else if (featureType.get() == FeatureType.TELEMETRY) {
+ log.trace("Can't fetch/subscribe to timeseries updates");
+ exchange.respond(ResponseCode.BAD_REQUEST);
+ } else if (exchange.getRequestOptions().hasObserve()) {
+ boolean unsubscribe = exchange.getRequestOptions().getObserve() == 1;
+ MsgType msgType;
+ if (featureType.get() == FeatureType.RPC) {
+ msgType = unsubscribe ? MsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST : MsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST;
+ } else {
+ msgType = unsubscribe ? MsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST : MsgType.SUBSCRIBE_ATTRIBUTES_REQUEST;
+ }
+ Optional<SessionId> sessionId = processRequest(exchange, msgType);
+ if (sessionId.isPresent()) {
+ if (exchange.getRequestOptions().getObserve() == 1) {
+ exchange.respond(ResponseCode.VALID);
+ }
+ }
+ } else if (featureType.get() == FeatureType.ATTRIBUTES) {
+ processRequest(exchange, MsgType.GET_ATTRIBUTES_REQUEST);
+ } else {
+ log.trace("Invalid feature type parameter");
+ exchange.respond(ResponseCode.BAD_REQUEST);
+ }
+ }
+
+ @Override
+ public void handlePOST(CoapExchange exchange) {
+ Optional<FeatureType> featureType = getFeatureType(exchange.advanced().getRequest());
+ if (!featureType.isPresent()) {
+ log.trace("Missing feature type parameter");
+ exchange.respond(ResponseCode.BAD_REQUEST);
+ } else {
+ switch (featureType.get()) {
+ case ATTRIBUTES:
+ processRequest(exchange, MsgType.POST_ATTRIBUTES_REQUEST);
+ break;
+ case TELEMETRY:
+ processRequest(exchange, MsgType.POST_TELEMETRY_REQUEST);
+ break;
+ case RPC:
+ Optional<Integer> requestId = getRequestId(exchange.advanced().getRequest());
+ if (requestId.isPresent()) {
+ processRequest(exchange, MsgType.TO_DEVICE_RPC_RESPONSE);
+ } else {
+ processRequest(exchange, MsgType.TO_SERVER_RPC_REQUEST);
+ }
+ break;
+ }
+ }
+ }
+
+ private Optional<SessionId> processRequest(CoapExchange exchange, MsgType type) {
+ log.trace("Processing {}", exchange.advanced().getRequest());
+ exchange.accept();
+ Exchange advanced = exchange.advanced();
+ Request request = advanced.getRequest();
+
+ Optional<DeviceCredentialsFilter> credentials = decodeCredentials(request);
+ if (!credentials.isPresent()) {
+ exchange.respond(ResponseCode.BAD_REQUEST);
+ return Optional.empty();
+ }
+
+ CoapSessionCtx ctx = new CoapSessionCtx(exchange, adaptor, processor, authService, timeout);
+
+ if (!ctx.login(credentials.get())) {
+ exchange.respond(ResponseCode.UNAUTHORIZED);
+ return Optional.empty();
+ }
+
+ AdaptorToSessionActorMsg msg;
+ try {
+ switch (type) {
+ case GET_ATTRIBUTES_REQUEST:
+ case POST_ATTRIBUTES_REQUEST:
+ case POST_TELEMETRY_REQUEST:
+ case TO_DEVICE_RPC_RESPONSE:
+ case TO_SERVER_RPC_REQUEST:
+ ctx.setSessionType(SessionType.SYNC);
+ msg = adaptor.convertToActorMsg(ctx, type, request);
+ break;
+ case SUBSCRIBE_ATTRIBUTES_REQUEST:
+ case SUBSCRIBE_RPC_COMMANDS_REQUEST:
+ ExchangeObserver systemObserver = (ExchangeObserver) observerField.get(advanced);
+ advanced.setObserver(new CoapExchangeObserverProxy(systemObserver, ctx));
+ case UNSUBSCRIBE_ATTRIBUTES_REQUEST:
+ case UNSUBSCRIBE_RPC_COMMANDS_REQUEST:
+ ctx.setSessionType(SessionType.ASYNC);
+ msg = adaptor.convertToActorMsg(ctx, type, request);
+ break;
+ default:
+ log.trace("[{}] Unsupported msg type: {}", ctx.getSessionId(), type);
+ throw new IllegalArgumentException("Unsupported msg type: " + type);
+ }
+ log.trace("Processing msg: {}", msg);
+ processor.process(new BasicToDeviceActorSessionMsg(ctx.getDevice(), msg));
+ } catch (AdaptorException e) {
+ log.debug("Failed to decode payload {}", e);
+ exchange.respond(ResponseCode.BAD_REQUEST, e.getMessage());
+ return Optional.empty();
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ log.debug("Failed to process payload {}", e);
+ exchange.respond(ResponseCode.INTERNAL_SERVER_ERROR, e.getMessage());
+ }
+ return Optional.of(ctx.getSessionId());
+ }
+
+ private Optional<DeviceCredentialsFilter> decodeCredentials(Request request) {
+ List<String> uriPath = request.getOptions().getUriPath();
+ DeviceCredentialsFilter credentials = null;
+ if (uriPath.size() >= ACCESS_TOKEN_POSITION) {
+ credentials = new DeviceTokenCredentials(uriPath.get(ACCESS_TOKEN_POSITION - 1));
+ }
+ return Optional.ofNullable(credentials);
+ }
+
+ private Optional<FeatureType> getFeatureType(Request request) {
+ List<String> uriPath = request.getOptions().getUriPath();
+ try {
+ if (uriPath.size() >= FEATURE_TYPE_POSITION) {
+ return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION - 1).toUpperCase()));
+ }
+ } catch (RuntimeException e) {
+ log.warn("Failed to decode feature type: {}", uriPath);
+ }
+ return Optional.empty();
+ }
+
+ public static Optional<Integer> getRequestId(Request request) {
+ List<String> uriPath = request.getOptions().getUriPath();
+ try {
+ if (uriPath.size() >= REQUEST_ID_POSITION) {
+ return Optional.of(Integer.valueOf(uriPath.get(REQUEST_ID_POSITION - 1)));
+ }
+ } catch (RuntimeException e) {
+ log.warn("Failed to decode feature type: {}", uriPath);
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public Resource getChild(String name) {
+ return this;
+ }
+
+}
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
new file mode 100644
index 0000000..31ead0c
--- /dev/null
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.CoapResource;
+import org.eclipse.californium.core.CoapServer;
+import org.eclipse.californium.core.network.CoapEndpoint;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Service;
+
+@Service("CoapTransportService")
+@Slf4j
+public class CoapTransportService {
+
+ private static final String V1 = "v1";
+ private static final String API = "api";
+
+ private CoapServer server;
+
+ @Autowired(required = false)
+ private ApplicationContext appContext;
+
+ @Autowired(required = false)
+ private SessionMsgProcessor processor;
+
+ @Autowired(required = false)
+ private DeviceAuthService authService;
+
+
+ @Value("${coap.bind_address}")
+ private String host;
+ @Value("${coap.bind_port}")
+ private Integer port;
+ @Value("${coap.adaptor}")
+ private String adaptorName;
+ @Value("${coap.timeout}")
+ private Long timeout;
+
+ private CoapTransportAdaptor adaptor;
+
+ @PostConstruct
+ public void init() throws UnknownHostException {
+ log.info("Starting CoAP transport...");
+ log.info("Lookup CoAP transport adaptor {}", adaptorName);
+ this.adaptor = (CoapTransportAdaptor) appContext.getBean(adaptorName);
+ log.info("Starting CoAP transport server");
+ this.server = new CoapServer();
+ createResources();
+ InetAddress addr = InetAddress.getByName(host);
+ InetSocketAddress sockAddr = new InetSocketAddress(addr, port);
+ server.addEndpoint(new CoapEndpoint(sockAddr));
+ server.start();
+ log.info("CoAP transport started!");
+ }
+
+ private void createResources() {
+ CoapResource api = new CoapResource(API);
+ api.add(new CoapTransportResource(processor, authService, adaptor, V1, timeout));
+ server.add(api);
+ }
+
+ @PreDestroy
+ public void shutdown() {
+ log.info("Stopping CoAP transport!");
+ this.server.destroy();
+ log.info("CoAP transport stopped!");
+ }
+}
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapExchangeObserverProxy.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapExchangeObserverProxy.java
new file mode 100644
index 0000000..e883955
--- /dev/null
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapExchangeObserverProxy.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap.session;
+
+import org.eclipse.californium.core.network.Exchange;
+import org.eclipse.californium.core.network.ExchangeObserver;
+
+public class CoapExchangeObserverProxy implements ExchangeObserver {
+
+ private final ExchangeObserver proxy;
+ private final CoapSessionCtx ctx;
+
+ public CoapExchangeObserverProxy(ExchangeObserver proxy, CoapSessionCtx ctx) {
+ super();
+ this.proxy = proxy;
+ this.ctx = ctx;
+ }
+
+ @Override
+ public void completed(Exchange exchange) {
+ proxy.completed(exchange);
+ ctx.close();
+ }
+
+}
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java
new file mode 100644
index 0000000..a61f0bf
--- /dev/null
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java
@@ -0,0 +1,143 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap.session;
+
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.coap.CoAP.ResponseCode;
+import org.eclipse.californium.core.coap.Request;
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.californium.core.server.resources.CoapExchange;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.session.SessionActorToAdaptorMsg;
+import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
+import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
+import org.thingsboard.server.common.msg.session.ex.SessionAuthException;
+import org.thingsboard.server.common.msg.session.ex.SessionException;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.common.transport.adaptor.AdaptorException;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext;
+import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.atomic.AtomicInteger;
+@Slf4j
+public class CoapSessionCtx extends DeviceAwareSessionContext {
+
+ private final SessionId sessionId;
+ private final CoapExchange exchange;
+ private final CoapTransportAdaptor adaptor;
+ private final String token;
+ private final long timeout;
+ private SessionType sessionType;
+ private final AtomicInteger seqNumber = new AtomicInteger(2);
+
+ public CoapSessionCtx(CoapExchange exchange, CoapTransportAdaptor adaptor, SessionMsgProcessor processor, DeviceAuthService authService, long timeout) {
+ super(processor, authService);
+ Request request = exchange.advanced().getRequest();
+ this.token = request.getTokenString();
+ this.sessionId = new CoapSessionId(request.getSource().getHostAddress(), request.getSourcePort(), this.token);
+ this.exchange = exchange;
+ this.adaptor = adaptor;
+ this.timeout = timeout;
+ }
+
+
+ @Override
+ public void onMsg(SessionActorToAdaptorMsg msg) throws SessionException {
+ try {
+ adaptor.convertToAdaptorMsg(this, msg).ifPresent(this::pushToNetwork);
+ } catch (AdaptorException e) {
+ logAndWrap(e);
+ }
+ }
+
+ private void pushToNetwork(Response response) {
+ exchange.respond(response);
+ }
+
+ private void logAndWrap(AdaptorException e) throws SessionException {
+ log.warn("Failed to convert msg: {}", e.getMessage(), e);
+ throw new SessionException(e);
+ }
+
+ @Override
+ public void onMsg(SessionCtrlMsg msg) throws SessionException {
+ log.debug("[{}] onCtrl: {}", sessionId, msg);
+ if (msg instanceof SessionCloseMsg) {
+ onSessionClose((SessionCloseMsg) msg);
+ }
+ }
+
+ private void onSessionClose(SessionCloseMsg msg) {
+ if (msg.isTimeout()) {
+ exchange.respond(ResponseCode.SERVICE_UNAVAILABLE);
+ } else {
+ exchange.respond(ResponseCode.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public void onError(SessionException e) {
+ if (e instanceof SessionAuthException) {
+ log.warn("[{}] onError: {}", sessionId, e.getMessage());
+ exchange.respond(ResponseCode.UNAUTHORIZED);
+ } else {
+ log.warn("[{}] onError: {}", sessionId, e.getMessage(), e);
+ exchange.respond(ResponseCode.BAD_REQUEST);
+ }
+ }
+
+ @Override
+ public SessionId getSessionId() {
+ return sessionId;
+ }
+
+ @Override
+ public String toString() {
+ return "CoapSessionCtx [sessionId=" + sessionId + "]";
+ }
+
+ @Override
+ public boolean isClosed() {
+ return exchange.advanced().isComplete() || exchange.advanced().isTimedOut();
+ }
+
+ public void close() {
+ log.info("[{}] Closing processing context. Timeout: {}", sessionId, exchange.advanced().isTimedOut());
+ processor.process(new SessionCloseMsg(sessionId, exchange.advanced().isTimedOut()));
+ }
+
+ @Override
+ public long getTimeout() {
+ return timeout;
+ }
+
+ public void setSessionType(SessionType sessionType) {
+ this.sessionType = sessionType;
+ }
+
+ @Override
+ public SessionType getSessionType() {
+ return sessionType;
+ }
+
+ public int nextSeqNumber() {
+ return seqNumber.getAndIncrement();
+ }
+}
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionId.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionId.java
new file mode 100644
index 0000000..d679056
--- /dev/null
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionId.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap.session;
+
+import org.thingsboard.server.common.data.id.SessionId;
+
+public final class CoapSessionId implements SessionId {
+
+ private final String clientAddress;
+ private final int clientPort;
+ private final String token;
+
+ public CoapSessionId(String host, int port, String token) {
+ super();
+ this.clientAddress = host;
+ this.clientPort = port;
+ this.token = token;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((clientAddress == null) ? 0 : clientAddress.hashCode());
+ result = prime * result + clientPort;
+ result = prime * result + ((token == null) ? 0 : token.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ CoapSessionId other = (CoapSessionId) obj;
+ if (clientAddress == null) {
+ if (other.clientAddress != null)
+ return false;
+ } else if (!clientAddress.equals(other.clientAddress))
+ return false;
+ if (clientPort != other.clientPort)
+ return false;
+ if (token == null) {
+ if (other.token != null)
+ return false;
+ } else if (!token.equals(other.token))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "CoapSessionId [clientAddress=" + clientAddress + ", clientPort=" + clientPort + ", token=" + token + "]";
+ }
+
+ @Override
+ public String toUidStr() {
+ return clientAddress + ":" + clientPort + ":" + token;
+ }
+
+}
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
new file mode 100644
index 0000000..a2d6c25
--- /dev/null
+++ b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
@@ -0,0 +1,201 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap;
+
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.CoapClient;
+import org.eclipse.californium.core.CoapResponse;
+import org.eclipse.californium.core.coap.CoAP.ResponseCode;
+import org.eclipse.californium.core.coap.MediaTypeRegistry;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.CustomerId;
+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.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.security.DeviceCredentialsFilter;
+import org.thingsboard.server.common.data.security.DeviceCredentialsType;
+import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
+import org.thingsboard.server.common.msg.core.BasicGetAttributesResponse;
+import org.thingsboard.server.common.msg.core.BasicRequest;
+import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
+import org.thingsboard.server.common.msg.kv.BasicAttributeKVMsg;
+import org.thingsboard.server.common.msg.session.*;
+import org.thingsboard.server.common.msg.session.ex.SessionAuthException;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.ClassMode;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.thingsboard.server.common.transport.auth.DeviceAuthResult;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration
+@DirtiesContext(classMode = ClassMode.BEFORE_CLASS)
+@Slf4j
+public class CoapServerTest {
+
+ private static final int TEST_PORT = 5555;
+ private static final String TELEMETRY_POST_MESSAGE = "[{\"key1\":\"value1\"}]";
+ private static final String TEST_ATTRIBUTES_RESPONSE = "{\"key1\":\"value1\",\"key2\":42}";
+ private static final String DEVICE1_TOKEN = "Device1Token";
+ private static final String DEVICE2_TOKEN = "Device2Token";
+
+ @Configuration
+ public static class EchoCoapServerITConfiguration extends CoapServerTestConfiguration {
+
+ @Bean
+ public static DeviceAuthService authService() {
+ return new DeviceAuthService() {
+
+ private final DeviceId devId = new DeviceId(UUID.randomUUID());
+
+ @Override
+ public DeviceAuthResult process(DeviceCredentialsFilter credentials) {
+ if (credentials != null && credentials.getCredentialsType() == DeviceCredentialsType.ACCESS_TOKEN) {
+ DeviceTokenCredentials tokenCredentials = (DeviceTokenCredentials) credentials;
+ if (tokenCredentials.getCredentialsId().equals(DEVICE1_TOKEN)) {
+ return DeviceAuthResult.of(devId);
+ }
+ }
+ return DeviceAuthResult.of("Credentials are invalid!");
+ }
+
+ @Override
+ public Optional<Device> findDeviceById(DeviceId deviceId) {
+ if (deviceId.equals(devId)) {
+ Device dev = new Device();
+ dev.setId(devId);
+ dev.setTenantId(new TenantId(UUID.randomUUID()));
+ dev.setCustomerId(new CustomerId(UUID.randomUUID()));
+ return Optional.of(dev);
+ } else {
+ return Optional.empty();
+ }
+ }
+ };
+ }
+
+ @Bean
+ public static SessionMsgProcessor sessionMsgProcessor() {
+ return new SessionMsgProcessor() {
+
+ @Override
+ public void process(SessionAwareMsg toActorMsg) {
+ if (toActorMsg instanceof ToDeviceActorSessionMsg) {
+ AdaptorToSessionActorMsg sessionMsg = ((ToDeviceActorSessionMsg) toActorMsg).getSessionMsg();
+ try {
+ FromDeviceMsg deviceMsg = sessionMsg.getMsg();
+ ToDeviceMsg toDeviceMsg = null;
+ if (deviceMsg.getMsgType() == MsgType.POST_TELEMETRY_REQUEST) {
+ toDeviceMsg = BasicStatusCodeResponse.onSuccess(deviceMsg.getMsgType(), BasicRequest.DEFAULT_REQUEST_ID);
+ } else if (deviceMsg.getMsgType() == MsgType.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()));
+ BasicAttributeKVMsg kv = BasicAttributeKVMsg.fromClient(data);
+ toDeviceMsg = BasicGetAttributesResponse.onSuccess(deviceMsg.getMsgType(), BasicRequest.DEFAULT_REQUEST_ID, kv);
+ }
+ if (toDeviceMsg != null) {
+ sessionMsg.getSessionContext().onMsg(new BasicSessionActorToAdaptorMsg(sessionMsg.getSessionContext(), toDeviceMsg));
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ };
+ }
+ }
+
+ @Autowired
+ private CoapTransportService service;
+
+ @Before
+ public void beforeTest() {
+ log.info("Service info: {}", service.toString());
+ }
+
+ @Test
+ public void testBadJsonTelemetryPostRequest() {
+ CoapClient client = new CoapClient(getBaseTestUrl() + DEVICE1_TOKEN + "/" + FeatureType.TELEMETRY.name().toLowerCase());
+ CoapResponse response = client.setTimeout(6000).post("test", MediaTypeRegistry.APPLICATION_JSON);
+ Assert.assertEquals(ResponseCode.BAD_REQUEST, response.getCode());
+ log.info("Response: {}, {}", response.getCode(), response.getResponseText());
+ }
+
+ @Test
+ public void testNoCredentialsPostRequest() {
+ CoapClient client = new CoapClient(getBaseTestUrl());
+ CoapResponse response = client.setTimeout(6000).post(TELEMETRY_POST_MESSAGE, MediaTypeRegistry.APPLICATION_JSON);
+ Assert.assertEquals(ResponseCode.BAD_REQUEST, response.getCode());
+ log.info("Response: {}, {}", response.getCode(), response.getResponseText());
+ }
+
+ @Test
+ public void testValidJsonTelemetryPostRequest() {
+ CoapClient client = new CoapClient(getBaseTestUrl() + DEVICE1_TOKEN + "/" + FeatureType.TELEMETRY.name().toLowerCase());
+ CoapResponse response = client.setTimeout(6000).post(TELEMETRY_POST_MESSAGE, MediaTypeRegistry.APPLICATION_JSON);
+ Assert.assertEquals(ResponseCode.CREATED, response.getCode());
+ log.info("Response: {}, {}", response.getCode(), response.getResponseText());
+ }
+
+ @Test
+ public void testNoCredentialsAttributesGetRequest() {
+ CoapClient client = new CoapClient("coap://localhost:5555/api/v1?keys=key1,key2");
+ CoapResponse response = client.setTimeout(6000).get();
+ Assert.assertEquals(ResponseCode.BAD_REQUEST, response.getCode());
+ }
+
+ @Test
+ public void testNoKeysAttributesGetRequest() {
+ CoapClient client = new CoapClient(getBaseTestUrl() + DEVICE1_TOKEN + "/" + FeatureType.ATTRIBUTES.name().toLowerCase() + "?data=key1,key2");
+ CoapResponse response = client.setTimeout(6000).get();
+ Assert.assertEquals(ResponseCode.BAD_REQUEST, response.getCode());
+ }
+
+ @Test
+ public void testValidAttributesGetRequest() {
+ CoapClient client = new CoapClient(getBaseTestUrl() + DEVICE1_TOKEN + "/" + FeatureType.ATTRIBUTES.name().toLowerCase() + "?clientKeys=key1,key2");
+ CoapResponse response = client.setTimeout(6000).get();
+ Assert.assertEquals(ResponseCode.CONTENT, response.getCode());
+ Assert.assertEquals(TEST_ATTRIBUTES_RESPONSE, response.getResponseText());
+ log.info("Response: {}, {}", response.getCode(), response.getResponseText());
+ }
+
+ private String getBaseTestUrl() {
+ return "coap://localhost:" + TEST_PORT + "/api/v1/";
+ }
+
+}
diff --git a/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTestConfiguration.java b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTestConfiguration.java
new file mode 100644
index 0000000..39bd6c5
--- /dev/null
+++ b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTestConfiguration.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
+import org.springframework.test.context.TestPropertySource;
+
+@Configuration
+@ComponentScan({ "org.thingsboard.server.transport.coap" })
+@PropertySource("classpath:coap-transport-test.properties")
+public class CoapServerTestConfiguration {
+
+ @Bean
+ public static PropertySourcesPlaceholderConfigurer propertyConfig() {
+ return new PropertySourcesPlaceholderConfigurer();
+ }
+
+}
diff --git a/transport/coap/src/test/resources/coap-transport-test.properties b/transport/coap/src/test/resources/coap-transport-test.properties
new file mode 100644
index 0000000..469b034
--- /dev/null
+++ b/transport/coap/src/test/resources/coap-transport-test.properties
@@ -0,0 +1,4 @@
+coap.bind_address=0.0.0.0
+coap.bind_port=5555
+coap.adaptor=JsonCoapAdaptor
+coap.timeout=10000
\ No newline at end of file
transport/http/pom.xml 81(+81 -0)
diff --git a/transport/http/pom.xml b/transport/http/pom.xml
new file mode 100644
index 0000000..665a152
--- /dev/null
+++ b/transport/http/pom.xml
@@ -0,0 +1,81 @@
+<!--
+
+ Copyright © 2016 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.server</groupId>
+ <version>0.0.1-SNAPSHOT</version>
+ <artifactId>transport</artifactId>
+ </parent>
+ <groupId>org.thingsboard.server.transport</groupId>
+ <artifactId>http</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Thingsboard HTTP Transport</name>
+ <url>http://thingsboard.org</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <main.dir>${basedir}/../..</main.dir>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.thingsboard.server.common</groupId>
+ <artifactId>transport</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>log4j-over-slf4j</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+</project>
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
new file mode 100644
index 0000000..e3e0666
--- /dev/null
+++ b/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
@@ -0,0 +1,195 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.http;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.context.request.async.DeferredResult;
+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.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.transport.http.session.HttpSessionCtx;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author Andrew Shvayka
+ */
+@RestController
+@RequestMapping("/api/v1")
+@Slf4j
+public class DeviceApiController {
+
+ @Value("${http.request_timeout}")
+ private long defaultTimeout;
+
+ @Autowired(required = false)
+ private SessionMsgProcessor processor;
+
+ @Autowired(required = false)
+ private DeviceAuthService authService;
+
+ @RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.GET, produces = "application/json")
+ public DeferredResult<ResponseEntity> getDeviceAttributes(@PathVariable("deviceToken") String deviceToken,
+ @RequestParam(value = "clientKeys", required = false) String clientKeys,
+ @RequestParam(value = "sharedKeys", required = false) String sharedKeys) {
+ DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
+ if (StringUtils.isEmpty(clientKeys) && StringUtils.isEmpty(sharedKeys)) {
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+ } else {
+ HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
+ if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
+ Set<String> clientKeySet = new HashSet<>(Arrays.asList(clientKeys.split(",")));
+ Set<String> sharedKeySet = new HashSet<>(Arrays.asList(clientKeys.split(",")));
+ process(ctx, new BasicGetAttributesRequest(0, clientKeySet, sharedKeySet));
+ } else {
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
+ }
+ }
+
+ return responseWriter;
+ }
+
+ @RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.POST)
+ public DeferredResult<ResponseEntity> postDeviceAttributes(@PathVariable("deviceToken") String deviceToken,
+ @RequestBody String json) {
+ DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
+ HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
+ if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
+ try {
+ process(ctx, JsonConverter.convertToAttributes(new JsonParser().parse(json)));
+ } catch (IllegalStateException | JsonSyntaxException ex) {
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+ }
+ } else {
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
+ }
+ return responseWriter;
+ }
+
+ @RequestMapping(value = "/{deviceToken}/telemetry", method = RequestMethod.POST)
+ public DeferredResult<ResponseEntity> postTelemetry(@PathVariable("deviceToken") String deviceToken,
+ @RequestBody String json) {
+ DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
+ HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
+ if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
+ try {
+ process(ctx, JsonConverter.convertToTelemetry(new JsonParser().parse(json)));
+ } catch (IllegalStateException | JsonSyntaxException ex) {
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+ }
+ } else {
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
+ }
+ return responseWriter;
+ }
+
+ @RequestMapping(value = "/{deviceToken}/rpc", method = RequestMethod.GET, produces = "application/json")
+ public DeferredResult<ResponseEntity> subscribeToCommands(@PathVariable("deviceToken") String deviceToken,
+ @RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout) {
+ return subscribe(deviceToken, timeout, new RpcSubscribeMsg());
+ }
+
+ @RequestMapping(value = "/{deviceToken}/rpc/{requestId}", method = RequestMethod.POST)
+ public DeferredResult<ResponseEntity> replyToCommand(@PathVariable("deviceToken") String deviceToken,
+ @PathVariable("requestId") Integer requestId,
+ @RequestBody String json) {
+ DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
+ HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
+ if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
+ try {
+ JsonObject response = new JsonParser().parse(json).getAsJsonObject();
+ process(ctx, new ToDeviceRpcResponseMsg(requestId, response.toString()));
+ } catch (IllegalStateException | JsonSyntaxException ex) {
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+ }
+ } else {
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
+ }
+ return responseWriter;
+ }
+
+ @RequestMapping(value = "/{deviceToken}/rpc", method = RequestMethod.POST)
+ public DeferredResult<ResponseEntity> postRpcRequest(@PathVariable("deviceToken") String deviceToken,
+ @RequestBody String json) {
+ DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
+ HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
+ if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
+ try {
+ JsonObject request = new JsonParser().parse(json).getAsJsonObject();
+ process(ctx, new ToServerRpcRequestMsg(0,
+ request.get("method").getAsString(),
+ request.get("params").toString()));
+ } catch (IllegalStateException | JsonSyntaxException ex) {
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+ }
+ } else {
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
+ }
+ return responseWriter;
+ }
+
+ @RequestMapping(value = "/{deviceToken}/attributes/updates", method = RequestMethod.GET, produces = "application/json")
+ public DeferredResult<ResponseEntity> subscribeToAttributes(@PathVariable("deviceToken") String deviceToken,
+ @RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout) {
+ return subscribe(deviceToken, timeout, new AttributesSubscribeMsg());
+ }
+
+ private DeferredResult<ResponseEntity> subscribe(String deviceToken, long timeout, FromDeviceMsg msg) {
+ DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
+ HttpSessionCtx ctx = getHttpSessionCtx(responseWriter, timeout);
+ if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
+ try {
+ process(ctx, msg);
+ } catch (IllegalStateException | JsonSyntaxException ex) {
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+ }
+ } else {
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
+ }
+ return responseWriter;
+ }
+
+ private HttpSessionCtx getHttpSessionCtx(DeferredResult<ResponseEntity> responseWriter) {
+ return getHttpSessionCtx(responseWriter, defaultTimeout);
+ }
+
+ private HttpSessionCtx getHttpSessionCtx(DeferredResult<ResponseEntity> responseWriter, long timeout) {
+ return new HttpSessionCtx(processor, authService, responseWriter, timeout != 0 ? timeout : defaultTimeout);
+ }
+
+ private void process(HttpSessionCtx ctx, FromDeviceMsg request) {
+ AdaptorToSessionActorMsg msg = new BasicAdaptorToSessionActorMsg(ctx, request);
+ processor.process(new BasicToDeviceActorSessionMsg(ctx.getDevice(), msg));
+ }
+
+}
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
new file mode 100644
index 0000000..60805a5
--- /dev/null
+++ b/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionCtx.java
@@ -0,0 +1,162 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.http.session;
+
+import com.google.gson.JsonObject;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.core.*;
+import org.thingsboard.server.common.msg.session.*;
+import org.thingsboard.server.common.msg.session.ex.SessionException;
+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.session.DeviceAwareSessionContext;
+
+import java.util.function.Consumer;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class HttpSessionCtx extends DeviceAwareSessionContext {
+
+ private final SessionId sessionId;
+ private final long timeout;
+ private final DeferredResult<ResponseEntity> responseWriter;
+
+ public HttpSessionCtx(SessionMsgProcessor processor, DeviceAuthService authService, DeferredResult<ResponseEntity> responseWriter, long timeout) {
+ super(processor, authService);
+ this.sessionId = new HttpSessionId();
+ this.responseWriter = responseWriter;
+ this.timeout = timeout;
+ }
+
+ @Override
+ public SessionType getSessionType() {
+ return SessionType.SYNC;
+ }
+
+ @Override
+ public void onMsg(SessionActorToAdaptorMsg source) throws SessionException {
+ ToDeviceMsg msg = source.getMsg();
+ switch (msg.getMsgType()) {
+ case GET_ATTRIBUTES_RESPONSE:
+ reply((GetAttributesResponse) msg);
+ return;
+ case STATUS_CODE_RESPONSE:
+ reply((StatusCodeResponse) msg);
+ return;
+ case ATTRIBUTES_UPDATE_NOTIFICATION:
+ reply((AttributesUpdateNotification) msg);
+ return;
+ case TO_DEVICE_RPC_REQUEST:
+ reply((ToDeviceRpcRequestMsg) msg);
+ return;
+ case TO_SERVER_RPC_RESPONSE:
+ reply((ToServerRpcResponseMsg) msg);
+ return;
+ case RULE_ENGINE_ERROR:
+ reply((RuleEngineErrorMsg) msg);
+ return;
+ }
+ }
+
+ private void reply(RuleEngineErrorMsg msg) {
+ HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
+ switch (msg.getError()) {
+ case PLUGIN_TIMEOUT:
+ status = HttpStatus.REQUEST_TIMEOUT;
+ break;
+ default:
+ if (msg.getInMsgType() == MsgType.TO_SERVER_RPC_REQUEST) {
+ status = HttpStatus.BAD_REQUEST;
+ }
+ break;
+ }
+ responseWriter.setResult(new ResponseEntity<>(JsonConverter.toErrorJson(msg.getErrorMsg()).toString(), status));
+ }
+
+ private <T> void reply(ResponseMsg<? extends T> msg, Consumer<T> f) {
+ if (!msg.getError().isPresent()) {
+ f.accept(msg.getData().get());
+ } else {
+ Exception e = msg.getError().get();
+ responseWriter.setResult(new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+ }
+
+ private void reply(ToDeviceRpcRequestMsg msg) {
+ responseWriter.setResult(new ResponseEntity<>(JsonConverter.toJson(msg, true).toString(), HttpStatus.OK));
+ }
+
+ private void reply(ToServerRpcResponseMsg msg) {
+ responseWriter.setResult(new ResponseEntity<>(JsonConverter.toJson(msg).toString(), HttpStatus.OK));
+ }
+
+ private void reply(AttributesUpdateNotification msg) {
+ responseWriter.setResult(new ResponseEntity<>(JsonConverter.toJson(msg.getData(), false).toString(), HttpStatus.OK));
+ }
+
+ private void reply(GetAttributesResponse msg) {
+ reply(msg, payload -> {
+ if (payload.getClientAttributes().isEmpty() && payload.getSharedAttributes().isEmpty()) {
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.NOT_FOUND));
+ } else {
+ JsonObject result = JsonConverter.toJson(payload, false);
+ responseWriter.setResult(new ResponseEntity<>(result.toString(), HttpStatus.OK));
+ }
+ });
+ }
+
+ private void reply(StatusCodeResponse msg) {
+ reply(msg, payload -> {
+ if (payload == 0) {
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.OK));
+ } else {
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.valueOf(payload)));
+ }
+ });
+ }
+
+ @Override
+ public void onMsg(SessionCtrlMsg msg) throws SessionException {
+
+ }
+
+ @Override
+ public void onError(SessionException e) {
+
+ }
+
+ @Override
+ public boolean isClosed() {
+ return false;
+ }
+
+ @Override
+ public long getTimeout() {
+ return timeout;
+ }
+
+ @Override
+ public SessionId getSessionId() {
+ return sessionId;
+ }
+}
diff --git a/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionId.java b/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionId.java
new file mode 100644
index 0000000..9c9340e
--- /dev/null
+++ b/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionId.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.http.session;
+
+import org.thingsboard.server.common.data.id.SessionId;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class HttpSessionId implements SessionId {
+
+ private final UUID id;
+
+ public HttpSessionId() {
+ this.id = UUID.randomUUID();
+ }
+
+ @Override
+ public String toUidStr() {
+ return id.toString();
+ }
+}
transport/mqtt/pom.xml 90(+90 -0)
diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml
new file mode 100644
index 0000000..045bb1f
--- /dev/null
+++ b/transport/mqtt/pom.xml
@@ -0,0 +1,90 @@
+<!--
+
+ Copyright © 2016 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.server</groupId>
+ <version>0.0.1-SNAPSHOT</version>
+ <artifactId>transport</artifactId>
+ </parent>
+ <groupId>org.thingsboard.server.transport</groupId>
+ <artifactId>mqtt</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Thingsboard MQTT Transport</name>
+ <url>http://thingsboard.org</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <main.dir>${basedir}/../..</main.dir>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.thingsboard.server.common</groupId>
+ <artifactId>transport</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-all</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-context</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>log4j-over-slf4j</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ </dependency>
+ <!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <version>18.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+</project>
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
new file mode 100644
index 0000000..5af244a
--- /dev/null
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
@@ -0,0 +1,238 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt.adaptors;
+
+import com.google.gson.*;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.buffer.UnpooledByteBufAllocator;
+import io.netty.handler.codec.mqtt.*;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.msg.core.*;
+import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
+import org.thingsboard.server.common.msg.session.*;
+import org.thingsboard.server.common.transport.adaptor.AdaptorException;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
+import org.thingsboard.server.transport.mqtt.session.MqttSessionCtx;
+
+import java.nio.charset.Charset;
+import java.util.*;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Component("JsonMqttAdaptor")
+@Slf4j
+public class JsonMqttAdaptor implements MqttTransportAdaptor {
+
+ private static final Gson GSON = new Gson();
+ private static final Charset UTF8 = Charset.forName("UTF-8");
+ private static final ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false);
+
+ @Override
+ public AdaptorToSessionActorMsg convertToActorMsg(MqttSessionCtx ctx, MsgType type, MqttMessage inbound) throws AdaptorException {
+ FromDeviceMsg msg;
+ switch (type) {
+ case POST_TELEMETRY_REQUEST:
+ msg = convertToTelemetryUploadRequest(ctx, (MqttPublishMessage) inbound);
+ break;
+ case POST_ATTRIBUTES_REQUEST:
+ msg = convertToUpdateAttributesRequest(ctx, (MqttPublishMessage) inbound);
+ break;
+ case SUBSCRIBE_ATTRIBUTES_REQUEST:
+ msg = new AttributesSubscribeMsg();
+ break;
+ case UNSUBSCRIBE_ATTRIBUTES_REQUEST:
+ msg = new AttributesUnsubscribeMsg();
+ break;
+ case SUBSCRIBE_RPC_COMMANDS_REQUEST:
+ msg = new RpcSubscribeMsg();
+ break;
+ case UNSUBSCRIBE_RPC_COMMANDS_REQUEST:
+ msg = new RpcUnsubscribeMsg();
+ break;
+ case GET_ATTRIBUTES_REQUEST:
+ msg = convertToGetAttributesRequest(ctx, (MqttPublishMessage) inbound);
+ break;
+ case TO_DEVICE_RPC_RESPONSE:
+ msg = convertToRpcCommandResponse(ctx, (MqttPublishMessage) inbound);
+ break;
+ case TO_SERVER_RPC_REQUEST:
+ msg = convertToServerRpcRequest(ctx, (MqttPublishMessage) inbound);
+ break;
+ default:
+ log.warn("[{}] Unsupported msg type: {}!", ctx.getSessionId(), type);
+ throw new AdaptorException(new IllegalArgumentException("Unsupported msg type: " + type + "!"));
+ }
+ return new BasicAdaptorToSessionActorMsg(ctx, msg);
+ }
+
+ @Override
+ public Optional<MqttMessage> convertToAdaptorMsg(MqttSessionCtx ctx, SessionActorToAdaptorMsg sessionMsg) throws AdaptorException {
+ MqttMessage result = null;
+ ToDeviceMsg msg = sessionMsg.getMsg();
+ switch (msg.getMsgType()) {
+ case STATUS_CODE_RESPONSE:
+ case GET_ATTRIBUTES_RESPONSE:
+ ResponseMsg<?> responseMsg = (ResponseMsg) msg;
+ if (responseMsg.isSuccess()) {
+ MsgType requestMsgType = responseMsg.getRequestMsgType();
+ Integer requestId = responseMsg.getRequestId();
+ if (requestId >= 0) {
+ if (requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) {
+ result = MqttTransportHandler.createMqttPubAckMsg(requestId);
+ } else if (requestMsgType == MsgType.GET_ATTRIBUTES_REQUEST) {
+ GetAttributesResponse response = (GetAttributesResponse) msg;
+ if (response.isSuccess()) {
+ result = createMqttPublishMsg(ctx,
+ MqttTransportHandler.ATTRIBUTES_RESPONSE_TOPIC_PREFIX + requestId,
+ response.getData().get(), true);
+ } else {
+ throw new AdaptorException(response.getError().get());
+ }
+ }
+ }
+ } else {
+ if (responseMsg.getError().isPresent()) {
+ throw new AdaptorException(responseMsg.getError().get());
+ }
+ }
+ break;
+ case ATTRIBUTES_UPDATE_NOTIFICATION:
+ AttributesUpdateNotification notification = (AttributesUpdateNotification) msg;
+ result = createMqttPublishMsg(ctx, MqttTransportHandler.ATTRIBUTES_TOPIC, notification.getData(), false);
+ break;
+ case TO_DEVICE_RPC_REQUEST:
+ ToDeviceRpcRequestMsg rpcRequest = (ToDeviceRpcRequestMsg) msg;
+ result = createMqttPublishMsg(ctx, MqttTransportHandler.RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(),
+ rpcRequest);
+ break;
+ case TO_SERVER_RPC_RESPONSE:
+ ToServerRpcResponseMsg rpcResponse = (ToServerRpcResponseMsg) msg;
+ result = createMqttPublishMsg(ctx, MqttTransportHandler.RPC_REQUESTS_TOPIC + rpcResponse.getRequestId(),
+ rpcResponse);
+ break;
+ case RULE_ENGINE_ERROR:
+ RuleEngineErrorMsg errorMsg = (RuleEngineErrorMsg) msg;
+ result = createMqttPublishMsg(ctx, "errors", JsonConverter.toErrorJson(errorMsg.getErrorMsg()));
+ break;
+ }
+ return Optional.ofNullable(result);
+ }
+
+ private MqttPublishMessage createMqttPublishMsg(MqttSessionCtx ctx, String topic, AttributesKVMsg msg, boolean asMap) {
+ return createMqttPublishMsg(ctx, topic, JsonConverter.toJson(msg, asMap));
+ }
+
+ private MqttPublishMessage createMqttPublishMsg(MqttSessionCtx ctx, String topic, ToDeviceRpcRequestMsg msg) {
+ return createMqttPublishMsg(ctx, topic, JsonConverter.toJson(msg, false));
+ }
+
+ private MqttPublishMessage createMqttPublishMsg(MqttSessionCtx ctx, String topic, ToServerRpcResponseMsg msg) {
+ return createMqttPublishMsg(ctx, topic, JsonConverter.toJson(msg));
+ }
+
+ private MqttPublishMessage createMqttPublishMsg(MqttSessionCtx ctx, String topic, JsonElement json) {
+ MqttFixedHeader mqttFixedHeader =
+ new MqttFixedHeader(MqttMessageType.PUBLISH, false, MqttQoS.AT_LEAST_ONCE, false, 0);
+ MqttPublishVariableHeader header = new MqttPublishVariableHeader(topic, ctx.nextMsgId());
+ ByteBuf payload = ALLOCATOR.buffer();
+ payload.writeBytes(GSON.toJson(json).getBytes(UTF8));
+ return new MqttPublishMessage(mqttFixedHeader, header, payload);
+ }
+
+ private FromDeviceMsg convertToGetAttributesRequest(MqttSessionCtx ctx, MqttPublishMessage inbound) throws AdaptorException {
+ String topicName = inbound.variableHeader().topicName();
+ try {
+ Integer requestId = Integer.valueOf(topicName.substring(MqttTransportHandler.ATTRIBUTES_REQUEST_TOPIC_PREFIX.length()));
+ String payload = inbound.payload().toString(UTF8);
+ JsonElement requestBody = new JsonParser().parse(payload);
+ return new BasicGetAttributesRequest(requestId,
+ toStringSet(requestBody, "clientKeys"), toStringSet(requestBody, "sharedKeys"));
+ } catch (RuntimeException e) {
+ log.warn("Failed to decode get attributes request", e);
+ throw new AdaptorException(e);
+ }
+ }
+
+ private FromDeviceMsg convertToRpcCommandResponse(MqttSessionCtx ctx, MqttPublishMessage inbound) throws AdaptorException {
+ String topicName = inbound.variableHeader().topicName();
+ try {
+ Integer requestId = Integer.valueOf(topicName.substring(MqttTransportHandler.RPC_RESPONSE_TOPIC.length()));
+ String payload = inbound.payload().toString(UTF8);
+ return new ToDeviceRpcResponseMsg(
+ requestId,
+ payload);
+ } catch (RuntimeException e) {
+ log.warn("Failed to decode get attributes request", e);
+ throw new AdaptorException(e);
+ }
+ }
+
+ private Set<String> toStringSet(JsonElement requestBody, String name) {
+ JsonElement element = requestBody.getAsJsonObject().get(name);
+ if (element != null) {
+ return new HashSet<>(Arrays.asList(element.getAsString().split(",")));
+ } else {
+ return Collections.emptySet();
+ }
+ }
+
+ private UpdateAttributesRequest convertToUpdateAttributesRequest(SessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
+ String payload = validatePayload(ctx, inbound.payload());
+ try {
+ return JsonConverter.convertToAttributes(new JsonParser().parse(payload), inbound.variableHeader().messageId());
+ } catch (IllegalStateException | JsonSyntaxException ex) {
+ throw new AdaptorException(ex);
+ }
+ }
+
+ private TelemetryUploadRequest convertToTelemetryUploadRequest(SessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
+ String payload = validatePayload(ctx, inbound.payload());
+ try {
+ return JsonConverter.convertToTelemetry(new JsonParser().parse(payload), inbound.variableHeader().messageId());
+ } catch (IllegalStateException | JsonSyntaxException ex) {
+ throw new AdaptorException(ex);
+ }
+ }
+
+ private FromDeviceMsg convertToServerRpcRequest(MqttSessionCtx ctx, MqttPublishMessage inbound) throws AdaptorException {
+ String topicName = inbound.variableHeader().topicName();
+ String payload = validatePayload(ctx, inbound.payload());
+ try {
+ Integer requestId = Integer.valueOf(topicName.substring(MqttTransportHandler.RPC_REQUESTS_TOPIC.length()));
+ return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), requestId);
+ } catch (IllegalStateException | JsonSyntaxException ex) {
+ throw new AdaptorException(ex);
+ }
+ }
+
+ private String validatePayload(SessionContext ctx, ByteBuf payloadData) throws AdaptorException {
+ try {
+ String payload = payloadData.toString(UTF8);
+ if (payload == null) {
+ log.warn("[{}] Payload is empty!", ctx.getSessionId());
+ throw new AdaptorException(new IllegalArgumentException("Payload is empty!"));
+ }
+ return payload;
+ } finally {
+ payloadData.release();
+ }
+ }
+
+}
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java
new file mode 100644
index 0000000..32878ad
--- /dev/null
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt.adaptors;
+
+import io.netty.handler.codec.mqtt.MqttMessage;
+import org.thingsboard.server.common.transport.TransportAdaptor;
+import org.thingsboard.server.transport.mqtt.session.MqttSessionCtx;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface MqttTransportAdaptor extends TransportAdaptor<MqttSessionCtx, MqttMessage, MqttMessage> {
+}
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
new file mode 100644
index 0000000..f7b38d0
--- /dev/null
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
@@ -0,0 +1,91 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt;
+
+import com.google.common.io.Resources;
+import io.netty.handler.ssl.SslHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+import javax.net.ssl.*;
+import java.io.File;
+import java.io.FileInputStream;
+import java.net.URL;
+import java.security.KeyStore;
+
+/**
+ * Created by valerii.sosliuk on 11/6/16.
+ */
+@Slf4j
+@Component("MqttSslHandlerProvider")
+@ConditionalOnProperty(prefix = "mqtt.ssl", value = "key-store", havingValue = "", matchIfMissing = false)
+public class MqttSslHandlerProvider {
+
+ public static final String TLS = "TLS";
+ @Value("${mqtt.ssl.key-store}")
+ private String keyStoreFile;
+ @Value("${mqtt.ssl.key-store-password}")
+ private String keyStorePassword;
+ @Value("${mqtt.ssl.keyStoreType}")
+ private String keyStoreType;
+
+ @Value("${mqtt.ssl.trust-store}")
+ private String trustStoreFile;
+ @Value("${mqtt.ssl.trust-store-password}")
+ private String trustStorePassword;
+ @Value("${mqtt.ssl.trustStoreType}")
+ private String trustStoreType;
+
+
+ public SslHandler getSslHandler() {
+ try {
+ URL ksUrl = Resources.getResource(keyStoreFile);
+ File ksFile = new File(ksUrl.toURI());
+ URL tsUrl = Resources.getResource(trustStoreFile);
+ File tsFile = new File(tsUrl.toURI());
+
+ TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ KeyStore trustStore = KeyStore.getInstance(trustStoreType);
+ trustStore.load(new FileInputStream(tsFile), trustStorePassword.toCharArray());
+ tmFactory.init(trustStore);
+
+ KeyStore ks = KeyStore.getInstance(keyStoreType);
+
+ ks.load(new FileInputStream(ksFile), keyStorePassword.toCharArray());
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ kmf.init(ks, keyStorePassword.toCharArray());
+
+ KeyManager[] km = kmf.getKeyManagers();
+ TrustManager[] tm = tmFactory.getTrustManagers();
+ SSLContext sslContext = SSLContext.getInstance(TLS);
+ sslContext.init(km, tm, null);
+ SSLEngine sslEngine = sslContext.createSSLEngine();
+ sslEngine.setUseClientMode(false);
+ sslEngine.setNeedClientAuth(false);
+ sslEngine.setWantClientAuth(false);
+ sslEngine.setEnabledProtocols(sslEngine.getSupportedProtocols());
+ sslEngine.setEnabledCipherSuites(sslEngine.getSupportedCipherSuites());
+ sslEngine.setEnableSessionCreation(true);
+ return new SslHandler(sslEngine);
+ } catch (Exception e) {
+ log.error("Unable to set up SSL context. Reason: " + e.getMessage(), e);
+ throw new RuntimeException("Failed to get SSL handler", e);
+ }
+ }
+
+}
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
new file mode 100644
index 0000000..e1bb45c
--- /dev/null
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
@@ -0,0 +1,260 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.mqtt.*;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.GenericFutureListener;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
+import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
+import org.thingsboard.server.common.msg.session.BasicToDeviceActorSessionMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.common.transport.adaptor.AdaptorException;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
+import org.thingsboard.server.transport.mqtt.session.MqttSessionCtx;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class MqttTransportHandler extends ChannelInboundHandlerAdapter implements GenericFutureListener<Future<? super Void>> {
+
+ public static final MqttQoS MAX_SUPPORTED_QOS_LVL = MqttQoS.AT_LEAST_ONCE;
+ public static final String BASE_TOPIC = "v1/devices/me";
+ public static final String ATTRIBUTES_TOPIC = BASE_TOPIC + "/attributes";
+ public static final String TELEMETRY_TOPIC = BASE_TOPIC + "/telemetry";
+ public static final String ATTRIBUTES_REQUEST_TOPIC_PREFIX = BASE_TOPIC + "/attributes/request/";
+ public static final String ATTRIBUTES_RESPONSE_TOPIC_PREFIX = BASE_TOPIC + "/attributes/response/";
+ public static final String ATTRIBUTES_RESPONSES_TOPIC = ATTRIBUTES_RESPONSE_TOPIC_PREFIX + "+";
+ public static final String RPC_REQUESTS_TOPIC = BASE_TOPIC + "/rpc/request/";
+ public static final String RPC_REQUESTS_SUB_TOPIC = RPC_REQUESTS_TOPIC + "+";
+ public static final String RPC_RESPONSE_TOPIC = BASE_TOPIC + "/rpc/response/";
+ public static final String RPC_RESPONSE_SUB_TOPIC = RPC_RESPONSE_TOPIC + "+";
+ private final MqttSessionCtx sessionCtx;
+ private final String sessionId;
+ private final MqttTransportAdaptor adaptor;
+ private final SessionMsgProcessor processor;
+
+ public MqttTransportHandler(SessionMsgProcessor processor, DeviceAuthService authService, MqttTransportAdaptor adaptor) {
+ this.processor = processor;
+ this.adaptor = adaptor;
+ this.sessionCtx = new MqttSessionCtx(processor, authService, adaptor);
+ this.sessionId = sessionCtx.getSessionId().toUidStr();
+ }
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) {
+ log.trace("[{}] Processing msg: {}", sessionId, msg);
+ if (msg instanceof MqttMessage) {
+ processMqttMsg(ctx, (MqttMessage) msg);
+ }
+ }
+
+ private void processMqttMsg(ChannelHandlerContext ctx, MqttMessage msg) {
+ sessionCtx.setChannel(ctx);
+ switch (msg.fixedHeader().messageType()) {
+ case CONNECT:
+ processConnect(ctx, (MqttConnectMessage) msg);
+ break;
+ case PUBLISH:
+ processPublish(ctx, (MqttPublishMessage) msg);
+ break;
+ case SUBSCRIBE:
+ processSubscribe(ctx, (MqttSubscribeMessage) msg);
+ break;
+ case UNSUBSCRIBE:
+ processUnsubscribe(ctx, (MqttUnsubscribeMessage) msg);
+ break;
+ case PINGREQ:
+ ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(MqttMessageType.PINGRESP, false, MqttQoS.AT_MOST_ONCE, false, 0)));
+ break;
+ case DISCONNECT:
+ processDisconnect(ctx);
+ break;
+ }
+ }
+
+ private void processPublish(ChannelHandlerContext ctx, MqttPublishMessage mqttMsg) {
+ String topicName = mqttMsg.variableHeader().topicName();
+ int msgId = mqttMsg.variableHeader().messageId();
+ log.trace("[{}] Processing publish msg [{}][{}]!", sessionId, topicName, msgId);
+ AdaptorToSessionActorMsg msg = null;
+ try {
+ if (topicName.equals(ATTRIBUTES_TOPIC)) {
+ msg = adaptor.convertToActorMsg(sessionCtx, MsgType.POST_ATTRIBUTES_REQUEST, mqttMsg);
+ } else if (topicName.equals(TELEMETRY_TOPIC)) {
+ msg = adaptor.convertToActorMsg(sessionCtx, MsgType.POST_TELEMETRY_REQUEST, mqttMsg);
+ } else if (topicName.startsWith(ATTRIBUTES_REQUEST_TOPIC_PREFIX)) {
+ msg = adaptor.convertToActorMsg(sessionCtx, MsgType.GET_ATTRIBUTES_REQUEST, mqttMsg);
+ if (msgId >= 0) {
+ ctx.writeAndFlush(createMqttPubAckMsg(msgId));
+ }
+ } else if (topicName.startsWith(RPC_RESPONSE_TOPIC)) {
+ msg = adaptor.convertToActorMsg(sessionCtx, MsgType.TO_DEVICE_RPC_RESPONSE, mqttMsg);
+ if (msgId >= 0) {
+ ctx.writeAndFlush(createMqttPubAckMsg(msgId));
+ }
+ } else if (topicName.startsWith(RPC_REQUESTS_TOPIC)) {
+ msg = adaptor.convertToActorMsg(sessionCtx, MsgType.TO_SERVER_RPC_REQUEST, mqttMsg);
+ if (msgId >= 0) {
+ ctx.writeAndFlush(createMqttPubAckMsg(msgId));
+ }
+ }
+ } catch (AdaptorException e) {
+ log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e);
+ }
+
+ if (msg != null) {
+ processor.process(new BasicToDeviceActorSessionMsg(sessionCtx.getDevice(), msg));
+ } else {
+ log.warn("[{}] Closing current session due to invalid publish msg [{}][{}]", sessionId, topicName, msgId);
+ ctx.close();
+ }
+ }
+
+ private void processSubscribe(ChannelHandlerContext ctx, MqttSubscribeMessage mqttMsg) {
+ log.info("[{}] Processing subscription [{}]!", sessionId, mqttMsg.variableHeader().messageId());
+ List<Integer> grantedQoSList = new ArrayList<>();
+ for (MqttTopicSubscription subscription : mqttMsg.payload().topicSubscriptions()) {
+ String topicName = subscription.topicName();
+ //TODO: handle this qos level.
+ MqttQoS reqQoS = subscription.qualityOfService();
+ try {
+ if (topicName.equals(ATTRIBUTES_TOPIC)) {
+ AdaptorToSessionActorMsg msg = adaptor.convertToActorMsg(sessionCtx, MsgType.SUBSCRIBE_ATTRIBUTES_REQUEST, mqttMsg);
+ processor.process(new BasicToDeviceActorSessionMsg(sessionCtx.getDevice(), msg));
+ grantedQoSList.add(getMinSupportedQos(reqQoS));
+ } else if (topicName.equals(RPC_REQUESTS_SUB_TOPIC)) {
+ AdaptorToSessionActorMsg msg = adaptor.convertToActorMsg(sessionCtx, MsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST, mqttMsg);
+ processor.process(new BasicToDeviceActorSessionMsg(sessionCtx.getDevice(), msg));
+ grantedQoSList.add(getMinSupportedQos(reqQoS));
+ } else if (topicName.equals(RPC_RESPONSE_SUB_TOPIC)) {
+ grantedQoSList.add(getMinSupportedQos(reqQoS));
+ } else if (topicName.equals(ATTRIBUTES_RESPONSES_TOPIC)) {
+ sessionCtx.setAllowAttributeResponses();
+ grantedQoSList.add(getMinSupportedQos(reqQoS));
+ } else {
+ log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topicName, reqQoS);
+ grantedQoSList.add(MqttQoS.FAILURE.value());
+ }
+ } catch (AdaptorException e) {
+ log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topicName, reqQoS);
+ grantedQoSList.add(MqttQoS.FAILURE.value());
+ }
+ }
+ ctx.writeAndFlush(createSubAckMessage(mqttMsg.variableHeader().messageId(), grantedQoSList));
+ }
+
+ private void processUnsubscribe(ChannelHandlerContext ctx, MqttUnsubscribeMessage mqttMsg) {
+ log.info("[{}] Processing subscription [{}]!", sessionId, mqttMsg.variableHeader().messageId());
+ for (String topicName : mqttMsg.payload().topics()) {
+ try {
+ if (topicName.equals(ATTRIBUTES_TOPIC)) {
+ AdaptorToSessionActorMsg msg = adaptor.convertToActorMsg(sessionCtx, MsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST, mqttMsg);
+ processor.process(new BasicToDeviceActorSessionMsg(sessionCtx.getDevice(), msg));
+ } else if (topicName.equals(RPC_REQUESTS_SUB_TOPIC)) {
+ AdaptorToSessionActorMsg msg = adaptor.convertToActorMsg(sessionCtx, MsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST, mqttMsg);
+ processor.process(new BasicToDeviceActorSessionMsg(sessionCtx.getDevice(), msg));
+ } else if (topicName.equals(ATTRIBUTES_RESPONSES_TOPIC)) {
+ sessionCtx.setDisallowAttributeResponses();
+ }
+ } catch (AdaptorException e) {
+ log.warn("[{}] Failed to process unsubscription [{}] to [{}]", sessionId, mqttMsg.variableHeader().messageId(), topicName);
+ }
+ }
+ ctx.writeAndFlush(createUnSubAckMessage(mqttMsg.variableHeader().messageId()));
+ }
+
+ private MqttMessage createUnSubAckMessage(int msgId) {
+ MqttFixedHeader mqttFixedHeader =
+ new MqttFixedHeader(MqttMessageType.SUBACK, false, MqttQoS.AT_LEAST_ONCE, false, 0);
+ MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(msgId);
+ return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader);
+ }
+
+ private void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) {
+ log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier());
+ String userName = msg.payload().userName();
+ if (StringUtils.isEmpty(userName)) {
+ ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD));
+ ctx.close();
+ } else if (sessionCtx.login(new DeviceTokenCredentials(msg.payload().userName()))) {
+ ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_ACCEPTED));
+ } else {
+ ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED));
+ ctx.close();
+ }
+ }
+
+ private void processDisconnect(ChannelHandlerContext ctx) {
+ processor.process(new SessionCloseMsg(sessionCtx.getSessionId(), false));
+ ctx.close();
+ }
+
+ private MqttConnAckMessage createMqttConnAckMsg(MqttConnectReturnCode returnCode) {
+ MqttFixedHeader mqttFixedHeader =
+ new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
+ MqttConnAckVariableHeader mqttConnAckVariableHeader =
+ new MqttConnAckVariableHeader(returnCode, true);
+ return new MqttConnAckMessage(mqttFixedHeader, mqttConnAckVariableHeader);
+ }
+
+ @Override
+ public void channelReadComplete(ChannelHandlerContext ctx) {
+ ctx.flush();
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+ log.error("[{}] Unexpected Exception", sessionId, cause);
+ ctx.close();
+ }
+
+ private static MqttSubAckMessage createSubAckMessage(Integer msgId, List<Integer> grantedQoSList) {
+ MqttFixedHeader mqttFixedHeader =
+ new MqttFixedHeader(MqttMessageType.SUBACK, false, MqttQoS.AT_LEAST_ONCE, false, 0);
+ MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(msgId);
+ MqttSubAckPayload mqttSubAckPayload = new MqttSubAckPayload(grantedQoSList);
+ return new MqttSubAckMessage(mqttFixedHeader, mqttMessageIdVariableHeader, mqttSubAckPayload);
+ }
+
+ private static int getMinSupportedQos(MqttQoS reqQoS) {
+ return Math.min(reqQoS.value(), MAX_SUPPORTED_QOS_LVL.value());
+ }
+
+ public static MqttPubAckMessage createMqttPubAckMsg(int requestId) {
+ MqttFixedHeader mqttFixedHeader =
+ new MqttFixedHeader(MqttMessageType.PUBACK, false, MqttQoS.AT_LEAST_ONCE, false, 0);
+ MqttMessageIdVariableHeader mqttMsgIdVariableHeader =
+ MqttMessageIdVariableHeader.from(requestId);
+ return new MqttPubAckMessage(mqttFixedHeader, mqttMsgIdVariableHeader);
+ }
+
+ @Override
+ public void operationComplete(Future<? super Void> future) throws Exception {
+ processor.process(new SessionCloseMsg(sessionCtx.getSessionId(), false));
+ }
+}
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
new file mode 100644
index 0000000..0c60309
--- /dev/null
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt;
+
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.handler.codec.mqtt.MqttDecoder;
+import io.netty.handler.codec.mqtt.MqttEncoder;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.SslHandler;
+import io.netty.handler.ssl.util.SelfSignedCertificate;
+import org.springframework.beans.factory.annotation.Value;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
+
+import javax.net.ssl.SSLException;
+import java.security.cert.CertificateException;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class MqttTransportServerInitializer extends ChannelInitializer<SocketChannel> {
+
+ private final SessionMsgProcessor processor;
+ private final DeviceAuthService authService;
+ private final MqttTransportAdaptor adaptor;
+ private final MqttSslHandlerProvider sslHandlerProvider;
+
+ public MqttTransportServerInitializer(SessionMsgProcessor processor, DeviceAuthService authService, MqttTransportAdaptor adaptor,
+ MqttSslHandlerProvider sslHandlerProvider) {
+ this.processor = processor;
+ this.authService = authService;
+ this.adaptor = adaptor;
+ this.sslHandlerProvider = sslHandlerProvider;
+ }
+
+ @Override
+ public void initChannel(SocketChannel ch) {
+ ChannelPipeline pipeline = ch.pipeline();
+ if (sslHandlerProvider != null) {
+ pipeline.addLast(sslHandlerProvider.getSslHandler());
+ }
+ pipeline.addLast("decoder", new MqttDecoder());
+ pipeline.addLast("encoder", MqttEncoder.INSTANCE);
+
+ MqttTransportHandler handler = new MqttTransportHandler(processor, authService, adaptor);
+ pipeline.addLast(handler);
+ ch.closeFuture().addListener(handler);
+ }
+
+}
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
new file mode 100644
index 0000000..e8569cf
--- /dev/null
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
@@ -0,0 +1,108 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
+import io.netty.handler.ssl.SslHandler;
+import io.netty.util.ResourceLeakDetector;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+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.transport.mqtt.adaptors.MqttTransportAdaptor;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.net.ssl.SSLEngine;
+import java.util.concurrent.Executor;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Service("MqttTransportService")
+@Slf4j
+public class MqttTransportService {
+
+ private static final String V1 = "v1";
+ private static final String DEVICE = "device";
+
+ @Autowired(required = false)
+ private ApplicationContext appContext;
+
+ @Autowired(required = false)
+ private SessionMsgProcessor processor;
+
+ @Autowired(required = false)
+ private DeviceAuthService authService;
+
+ @Autowired(required = false)
+ private MqttSslHandlerProvider sslHandlerProvider;
+
+ @Value("${mqtt.bind_address}")
+ private String host;
+ @Value("${mqtt.bind_port}")
+ private Integer port;
+ @Value("${mqtt.adaptor}")
+ private String adaptorName;
+
+ private MqttTransportAdaptor adaptor;
+
+ private Channel serverChannel;
+ private EventLoopGroup bossGroup;
+ private EventLoopGroup workerGroup;
+
+ @PostConstruct
+ public void init() throws Exception {
+ ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.ADVANCED);
+ log.info("Starting MQTT transport...");
+ log.info("Lookup MQTT transport adaptor {}", adaptorName);
+ this.adaptor = (MqttTransportAdaptor) appContext.getBean(adaptorName);
+
+ log.info("Starting MQTT transport server");
+ bossGroup = new NioEventLoopGroup(1);
+ workerGroup = new NioEventLoopGroup();
+ ServerBootstrap b = new ServerBootstrap();
+ b.group(bossGroup, workerGroup)
+ .channel(NioServerSocketChannel.class)
+ .handler(new LoggingHandler(LogLevel.TRACE))
+ .childHandler(new MqttTransportServerInitializer(processor, authService, adaptor, sslHandlerProvider));
+
+ serverChannel = b.bind(host, port).sync().channel();
+ log.info("Mqtt transport started!");
+ }
+
+ @PreDestroy
+ public void shutdown() throws InterruptedException {
+ log.info("Stopping MQTT transport!");
+ try {
+ serverChannel.close().sync();
+ } finally {
+ bossGroup.shutdownGracefully();
+ workerGroup.shutdownGracefully();
+ }
+ log.info("MQTT transport stopped!");
+ }
+}
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java
new file mode 100644
index 0000000..5cae9f5
--- /dev/null
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt.session;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.mqtt.MqttMessage;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.session.SessionActorToAdaptorMsg;
+import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
+import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ex.SessionException;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.common.transport.adaptor.AdaptorException;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext;
+import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class MqttSessionCtx extends DeviceAwareSessionContext {
+
+ private final MqttTransportAdaptor adaptor;
+ private final MqttSessionId sessionId;
+ private ChannelHandlerContext channel;
+ private volatile boolean allowAttributeResponses;
+ private AtomicInteger msgIdSeq = new AtomicInteger(0);
+
+ public MqttSessionCtx(SessionMsgProcessor processor, DeviceAuthService authService, MqttTransportAdaptor adaptor) {
+ super(processor, authService);
+ this.adaptor = adaptor;
+ this.sessionId = new MqttSessionId();
+ }
+
+ @Override
+ public SessionType getSessionType() {
+ return SessionType.ASYNC;
+ }
+
+ @Override
+ public void onMsg(SessionActorToAdaptorMsg msg) throws SessionException {
+ try {
+ adaptor.convertToAdaptorMsg(this, msg).ifPresent(this::pushToNetwork);
+ } catch (AdaptorException e) {
+ //TODO: close channel with disconnect;
+ logAndWrap(e);
+ }
+ }
+
+ private void logAndWrap(AdaptorException e) throws SessionException {
+ log.warn("Failed to convert msg: {}", e.getMessage(), e);
+ throw new SessionException(e);
+ }
+
+ private void pushToNetwork(MqttMessage msg) {
+ channel.writeAndFlush(msg);
+ }
+
+ @Override
+ public void onMsg(SessionCtrlMsg msg) throws SessionException {
+
+ }
+
+ @Override
+ public void onError(SessionException e) {
+
+ }
+
+ @Override
+ public boolean isClosed() {
+ return false;
+ }
+
+ @Override
+ public long getTimeout() {
+ return 0;
+ }
+
+ @Override
+ public SessionId getSessionId() {
+ return sessionId;
+ }
+
+ public void setChannel(ChannelHandlerContext channel) {
+ this.channel = channel;
+ }
+
+ public void setAllowAttributeResponses() {
+ allowAttributeResponses = true;
+ }
+
+ public void setDisallowAttributeResponses() {
+ allowAttributeResponses = false;
+ }
+
+ public int nextMsgId() {
+ return msgIdSeq.incrementAndGet();
+ }
+}
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionId.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionId.java
new file mode 100644
index 0000000..79b4d77
--- /dev/null
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionId.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt.session;
+
+import org.thingsboard.server.common.data.id.SessionId;
+
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class MqttSessionId implements SessionId {
+
+ private static final AtomicLong idSeq = new AtomicLong();
+
+ private final long id;
+
+ public MqttSessionId() {
+ this.id = idSeq.incrementAndGet();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ MqttSessionId that = (MqttSessionId) o;
+
+ return id == that.id;
+
+ }
+
+ @Override
+ public int hashCode() {
+ return (int) (id ^ (id >>> 32));
+ }
+
+ @Override
+ public String toUidStr() {
+ return "mqtt" + id;
+ }
+}
transport/pom.xml 50(+50 -0)
diff --git a/transport/pom.xml b/transport/pom.xml
new file mode 100644
index 0000000..4675cdf
--- /dev/null
+++ b/transport/pom.xml
@@ -0,0 +1,50 @@
+<!--
+
+ Copyright © 2016 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>0.0.1-SNAPSHOT</version>
+ <artifactId>server</artifactId>
+ </parent>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>transport</artifactId>
+ <packaging>pom</packaging>
+
+ <name>Thingsboard Server Transport Modules</name>
+ <url>http://thingsboard.org</url>
+
+ <properties>
+ <main.dir>${basedir}/..</main.dir>
+ </properties>
+
+ <modules>
+ <module>http</module>
+ <module>coap</module>
+ <module>mqtt</module>
+ </modules>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-autoconfigure</artifactId>
+ </dependency>
+ </dependencies>
+
+</project>