thingsboard-aplcache
Changes
application/pom.xml 8(+8 -0)
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java 63(+28 -35)
application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java 28(+23 -5)
application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java 26(+26 -0)
application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java 3(+3 -0)
application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingContext.java 117(+0 -117)
application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java 345(+0 -345)
application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingContext.java 115(+0 -115)
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java 209(+209 -0)
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java 195(+195 -0)
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java 61(+61 -0)
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java 37(+37 -0)
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java 116(+116 -0)
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java 39(+39 -0)
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfErrorMsg.java 37(+37 -0)
application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java 23(+10 -13)
application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java 3(+0 -3)
application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java 39(+31 -8)
application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java 4(+2 -2)
application/src/main/java/org/thingsboard/server/actors/shared/plugin/TenantPluginManager.java 5(+3 -2)
application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java 59(+59 -0)
application/src/main/java/org/thingsboard/server/actors/shared/rulechain/SystemRuleChainManager.java 25(+16 -9)
application/src/main/java/org/thingsboard/server/actors/shared/rulechain/TenantRuleChainManager.java 25(+16 -9)
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java 2(+0 -2)
application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java 20(+19 -1)
application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java 131(+55 -76)
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java 133(+71 -62)
application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java 4(+2 -2)
application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java 105(+79 -26)
application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java 3(+3 -0)
application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java 59(+59 -0)
application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java 32(+32 -0)
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java 4(+2 -2)
application/src/main/java/org/thingsboard/server/service/security/ValidationCallback.java 71(+71 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java 335(+335 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java 563(+563 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java 33(+33 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java 25(+8 -17)
application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java 31(+31 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java 68(+68 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketTextMsg.java 34(+34 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/WsSessionMetaData.java 53(+53 -0)
application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java 56(+56 -0)
application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java 190(+190 -0)
application/src/test/java/org/thingsboard/server/rules/flow/RuleEngineFlowSqlIntegrationTest.java 27(+6 -21)
application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java 158(+158 -0)
application/src/test/java/org/thingsboard/server/rules/lifecycle/RuleEngineLifecycleSqlIntegrationTest.java 26(+26 -0)
application/src/test/java/org/thingsboard/server/service/script/NashornJsEngineTest.java 151(+151 -0)
common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceStatusQuery.java 40(+40 -0)
common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java 2(+1 -1)
common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardException.java 2(+1 -1)
common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java 28(+28 -0)
common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java 31(+31 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java 6(+3 -3)
common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java 42(+21 -21)
common/message/src/main/java/org/thingsboard/server/common/msg/system/ServiceToRuleEngineMsg.java 37(+37 -0)
common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java 5(+3 -2)
dao/src/main/java/org/thingsboard/server/dao/component/CassandraBaseComponentDescriptorDao.java 4(+2 -2)
dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/repository/impl/CassandraMsgRepositoryTest.java 7(+4 -3)
dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/UnprocessedMsgFilterTest.java 4(+2 -2)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginConstants.java 2(+2 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java 6(+3 -3)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java 4(+2 -2)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java 6(+3 -3)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java 18(+5 -13)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java 18(+13 -5)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java 33(+25 -8)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java 3(+2 -1)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java 3(+2 -1)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java 32(+20 -12)
pom.xml 8(+7 -1)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ListeningExecutor.java 27(+27 -0)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java 10(+7 -3)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeConfiguration.java 6(+4 -2)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java 35(+35 -0)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java 36(+36 -0)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java 40(+40 -0)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeConfiguration.java 4(+2 -2)
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java 4(+4 -0)
rule-engine/rule-engine-components/pom.xml 20(+17 -3)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java 220(+220 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNodeConfiguration.java 44(+44 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java 66(+66 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNodeConfiguration.java 32(+32 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java 31(+31 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java 104(+104 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java 44(+44 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/DonAsynchron.java 44(+23 -21)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java 65(+65 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java 32(+32 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java 70(+70 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java 39(+39 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java 13(+12 -1)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java 11(+10 -1)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/EmailPojo.java 32(+32 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/RuleVelocityUtils.java 68(+68 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java 116(+116 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java 41(+41 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java 89(+89 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNodeConfiguration.java 32(+32 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java 92(+92 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java 69(+51 -18)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java 15(+14 -1)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java 43(+43 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java 40(+40 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java 53(+53 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java 50(+50 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java 45(+45 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTelemetryNode.java 89(+89 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTelemetryNodeConfiguration.java 34(+34 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TelemetryNodeCallback.java 42(+42 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java 47(+19 -28)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java 100(+100 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java 49(+49 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java 61(+61 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java 33(+33 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformNodeConfiguration.java 26(+5 -21)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java 50(+50 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java 60(+60 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java 58(+58 -0)
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css 2(+2 -0)
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js 3(+3 -0)
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java 341(+341 -0)
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java 121(+121 -0)
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java 103(+103 -0)
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java 98(+98 -0)
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java 264(+264 -0)
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java 124(+124 -0)
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java 141(+141 -0)
transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java 8(+7 -1)
transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java 6(+5 -1)
transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java 20(+13 -7)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java 25(+23 -2)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java 7(+5 -2)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java 6(+5 -1)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java 13(+12 -1)
ui/package.json 3(+2 -1)
ui/server.js 20(+20 -0)
ui/src/app/api/entity.service.js 8(+7 -1)
ui/src/app/api/rule-chain.service.js 307(+307 -0)
ui/src/app/app.js 5(+5 -0)
ui/src/app/common/types.constant.js 102(+101 -1)
ui/src/app/components/ace-editor-fix.js 45(+45 -0)
ui/src/app/components/details-sidenav.scss 10(+10 -0)
ui/src/app/components/js-func.directive.js 39(+33 -6)
ui/src/app/components/js-func.scss 12(+10 -2)
ui/src/app/components/js-func.tpl.html 17(+9 -8)
ui/src/app/components/json-content.directive.js 175(+175 -0)
ui/src/app/components/json-content.scss 35(+35 -0)
ui/src/app/components/json-content.tpl.html 31(+31 -0)
ui/src/app/components/json-object-edit.directive.js 183(+183 -0)
ui/src/app/components/json-object-edit.scss 35(+35 -0)
ui/src/app/components/kv-map.directive.js 119(+119 -0)
ui/src/app/components/kv-map.scss 30(+10 -20)
ui/src/app/components/kv-map.tpl.html 58(+58 -0)
ui/src/app/event/event.scss 25(+21 -4)
ui/src/app/event/event-row.directive.js 24(+22 -2)
ui/src/app/event/event-table.directive.js 18(+15 -3)
ui/src/app/layout/index.js 10(+9 -1)
ui/src/app/locale/locale.constant.js 109(+109 -0)
ui/src/app/rulechain/add-link.tpl.html 48(+48 -0)
ui/src/app/rulechain/add-rulechain.tpl.html 48(+48 -0)
ui/src/app/rulechain/add-rulenode.tpl.html 48(+48 -0)
ui/src/app/rulechain/index.js 41(+41 -0)
ui/src/app/rulechain/link.directive.js 71(+71 -0)
ui/src/app/rulechain/link-fieldset.tpl.html 39(+39 -0)
ui/src/app/rulechain/rulechain.controller.js 1287(+1287 -0)
ui/src/app/rulechain/rulechain.directive.js 47(+47 -0)
ui/src/app/rulechain/rulechain.routes.js 127(+127 -0)
ui/src/app/rulechain/rulechain.scss 466(+466 -0)
ui/src/app/rulechain/rulechain.tpl.html 229(+229 -0)
ui/src/app/rulechain/rulechain-card.tpl.html 18(+18 -0)
ui/src/app/rulechain/rulechains.controller.js 188(+188 -0)
ui/src/app/rulechain/rulechains.tpl.html 76(+76 -0)
ui/src/app/rulechain/rulenode.directive.js 81(+81 -0)
ui/src/app/rulechain/rulenode.scss 28(+11 -17)
ui/src/app/rulechain/rulenode.tpl.html 49(+49 -0)
ui/src/app/rulechain/script/node-script-test.scss 112(+112 -0)
ui/src/app/services/item-buffer.service.js 83(+81 -2)
ui/src/app/services/menu.service.js 32(+32 -0)
ui/src/scss/main.scss 18(+18 -0)
Details
application/pom.xml 8(+8 -0)
diff --git a/application/pom.xml b/application/pom.xml
index 8449246..6da40a9 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -54,6 +54,14 @@
<artifactId>extensions-api</artifactId>
</dependency>
<dependency>
+ <groupId>org.thingsboard.rule-engine</groupId>
+ <artifactId>rule-engine-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.rule-engine</groupId>
+ <artifactId>rule-engine-components</artifactId>
+ </dependency>
+ <dependency>
<groupId>org.thingsboard</groupId>
<artifactId>extensions-core</artifactId>
</dependency>
diff --git a/application/src/main/data/upgrade/1.5.0/schema_update.cql b/application/src/main/data/upgrade/1.5.0/schema_update.cql
index aa8b10b..5cdaede 100644
--- a/application/src/main/data/upgrade/1.5.0/schema_update.cql
+++ b/application/src/main/data/upgrade/1.5.0/schema_update.cql
@@ -69,6 +69,7 @@ CREATE TABLE IF NOT EXISTS thingsboard.rule_chain (
search_text text,
first_rule_node_id uuid,
root boolean,
+ debug_mode boolean,
configuration text,
additional_info text,
PRIMARY KEY (id, tenant_id)
@@ -85,9 +86,12 @@ CREATE TABLE IF NOT EXISTS thingsboard.rule_node (
id uuid,
type text,
name text,
+ debug_mode boolean,
search_text text,
configuration text,
additional_info text,
PRIMARY KEY (id)
);
+ALTER TABLE thingsboard.device ADD last_connect bigint;
+ALTER TABLE thingsboard.device ADD last_update bigint;
\ No newline at end of file
diff --git a/application/src/main/data/upgrade/1.5.0/schema_update.sql b/application/src/main/data/upgrade/1.5.0/schema_update.sql
index 0043ed5..ab91166 100644
--- a/application/src/main/data/upgrade/1.5.0/schema_update.sql
+++ b/application/src/main/data/upgrade/1.5.0/schema_update.sql
@@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS rule_chain (
name varchar(255),
first_rule_node_id varchar(31),
root boolean,
+ debug_mode boolean,
search_text varchar(255),
tenant_id varchar(31)
);
@@ -31,5 +32,9 @@ CREATE TABLE IF NOT EXISTS rule_node (
configuration varchar(10000000),
type varchar(255),
name varchar(255),
+ debug_mode boolean,
search_text varchar(255)
-);
\ No newline at end of file
+);
+
+ALTER TABLE device ADD COLUMN IF NOT EXISTS last_connect BIGINT;
+ALTER TABLE device ADD COLUMN IF NOT EXISTS last_update BIGINT;
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index 77953c9..50d2530 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -25,15 +25,19 @@ import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import lombok.Getter;
import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
+import org.thingsboard.rule.engine.api.ListeningExecutor;
+import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
@@ -46,116 +50,200 @@ import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.plugin.PluginService;
import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.rule.RuleChainService;
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.dao.user.UserService;
import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
+import org.thingsboard.server.service.executors.DbCallbackExecutorService;
+import org.thingsboard.server.service.mail.MailExecutorService;
+import org.thingsboard.server.service.script.JsExecutorService;
+import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
+import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Optional;
+@Slf4j
@Component
public class ActorSystemContext {
private static final String AKKA_CONF_FILE_NAME = "actor-system.conf";
protected final ObjectMapper mapper = new ObjectMapper();
- @Getter @Setter private ActorService actorService;
+ @Getter
+ @Setter
+ private ActorService actorService;
@Autowired
- @Getter private DiscoveryService discoveryService;
+ @Getter
+ private DiscoveryService discoveryService;
@Autowired
- @Getter @Setter private ComponentDiscoveryService componentService;
+ @Getter
+ @Setter
+ private ComponentDiscoveryService componentService;
@Autowired
- @Getter private ClusterRoutingService routingService;
+ @Getter
+ private ClusterRoutingService routingService;
@Autowired
- @Getter private ClusterRpcService rpcService;
+ @Getter
+ private ClusterRpcService rpcService;
@Autowired
- @Getter private DeviceAuthService deviceAuthService;
+ @Getter
+ private DeviceAuthService deviceAuthService;
@Autowired
- @Getter private DeviceService deviceService;
+ @Getter
+ private DeviceService deviceService;
@Autowired
- @Getter private AssetService assetService;
+ @Getter
+ private AssetService assetService;
@Autowired
- @Getter private TenantService tenantService;
+ @Getter
+ private TenantService tenantService;
@Autowired
- @Getter private CustomerService customerService;
+ @Getter
+ private CustomerService customerService;
@Autowired
- @Getter private RuleService ruleService;
+ @Getter
+ private UserService userService;
@Autowired
- @Getter private PluginService pluginService;
+ @Getter
+ private RuleService ruleService;
@Autowired
- @Getter private TimeseriesService tsService;
+ @Getter
+ private RuleChainService ruleChainService;
@Autowired
- @Getter private AttributesService attributesService;
+ @Getter
+ private PluginService pluginService;
@Autowired
- @Getter private EventService eventService;
+ @Getter
+ private TimeseriesService tsService;
@Autowired
- @Getter private AlarmService alarmService;
+ @Getter
+ private AttributesService attributesService;
@Autowired
- @Getter private RelationService relationService;
+ @Getter
+ private EventService eventService;
@Autowired
- @Getter private AuditLogService auditLogService;
+ @Getter
+ private AlarmService alarmService;
@Autowired
- @Getter @Setter private PluginWebSocketMsgEndpoint wsMsgEndpoint;
+ @Getter
+ private RelationService relationService;
+
+ @Autowired
+ @Getter
+ private AuditLogService auditLogService;
+
+ @Autowired
+ @Getter
+ private TelemetrySubscriptionService tsSubService;
+
+ @Autowired
+ @Getter
+ @Setter
+ private PluginWebSocketMsgEndpoint wsMsgEndpoint;
+
+ @Autowired
+ @Getter
+ private JsExecutorService jsExecutor;
+
+ @Autowired
+ @Getter
+ private MailExecutorService mailExecutor;
+
+ @Autowired
+ @Getter
+ private DbCallbackExecutorService dbCallbackExecutor;
+
+ @Autowired
+ @Getter
+ private MailService mailService;
@Value("${actors.session.sync.timeout}")
- @Getter private long syncSessionTimeout;
+ @Getter
+ private long syncSessionTimeout;
@Value("${actors.plugin.termination.delay}")
- @Getter private long pluginActorTerminationDelay;
+ @Getter
+ private long pluginActorTerminationDelay;
@Value("${actors.plugin.processing.timeout}")
- @Getter private long pluginProcessingTimeout;
+ @Getter
+ private long pluginProcessingTimeout;
@Value("${actors.plugin.error_persist_frequency}")
- @Getter private long pluginErrorPersistFrequency;
+ @Getter
+ private long pluginErrorPersistFrequency;
+
+ @Value("${actors.rule.chain.error_persist_frequency}")
+ @Getter
+ private long ruleChainErrorPersistFrequency;
+
+ @Value("${actors.rule.node.error_persist_frequency}")
+ @Getter
+ private long ruleNodeErrorPersistFrequency;
@Value("${actors.rule.termination.delay}")
- @Getter private long ruleActorTerminationDelay;
+ @Getter
+ private long ruleActorTerminationDelay;
@Value("${actors.rule.error_persist_frequency}")
- @Getter private long ruleErrorPersistFrequency;
+ @Getter
+ private long ruleErrorPersistFrequency;
@Value("${actors.statistics.enabled}")
- @Getter private boolean statisticsEnabled;
+ @Getter
+ private boolean statisticsEnabled;
@Value("${actors.statistics.persist_frequency}")
- @Getter private long statisticsPersistFrequency;
+ @Getter
+ private long statisticsPersistFrequency;
@Value("${actors.tenant.create_components_on_init}")
- @Getter private boolean tenantComponentsInitEnabled;
+ @Getter
+ private boolean tenantComponentsInitEnabled;
- @Getter @Setter private ActorSystem actorSystem;
+ @Getter
+ @Setter
+ private ActorSystem actorSystem;
- @Getter @Setter private ActorRef appActor;
+ @Getter
+ @Setter
+ private ActorRef appActor;
- @Getter @Setter private ActorRef sessionManagerActor;
+ @Getter
+ @Setter
+ private ActorRef sessionManagerActor;
- @Getter @Setter private ActorRef statsActor;
+ @Getter
+ @Setter
+ private ActorRef statsActor;
- @Getter private final Config config;
+ @Getter
+ private final Config config;
public ActorSystemContext() {
config = ConfigFactory.parseResources(AKKA_CONF_FILE_NAME).withFallback(ConfigFactory.load());
@@ -187,7 +275,7 @@ public class ActorSystemContext {
eventService.save(event);
}
- private String toString(Exception e) {
+ private String toString(Throwable e) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
return sw.toString();
@@ -207,4 +295,60 @@ public class ActorSystemContext {
private JsonNode toBodyJson(ServerAddress server, String method, String body) {
return mapper.createObjectNode().put("server", server.toString()).put("method", method).put("error", body);
}
+
+ public String getServerAddress() {
+ return discoveryService.getCurrentServer().getServerAddress().toString();
+ }
+
+ public void persistDebugInput(TenantId tenantId, EntityId entityId, TbMsg tbMsg) {
+ persistDebug(tenantId, entityId, "IN", tbMsg, null);
+ }
+
+ public void persistDebugInput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, Throwable error) {
+ persistDebug(tenantId, entityId, "IN", tbMsg, error);
+ }
+
+ public void persistDebugOutput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, Throwable error) {
+ persistDebug(tenantId, entityId, "OUT", tbMsg, error);
+ }
+
+ public void persistDebugOutput(TenantId tenantId, EntityId entityId, TbMsg tbMsg) {
+ persistDebug(tenantId, entityId, "OUT", tbMsg, null);
+ }
+
+ private void persistDebug(TenantId tenantId, EntityId entityId, String type, TbMsg tbMsg, Throwable error) {
+ try {
+ Event event = new Event();
+ event.setTenantId(tenantId);
+ event.setEntityId(entityId);
+ event.setType(DataConstants.DEBUG_RULE_NODE);
+
+ String metadata = mapper.writeValueAsString(tbMsg.getMetaData().getData());
+
+ ObjectNode node = mapper.createObjectNode()
+ .put("type", type)
+ .put("server", getServerAddress())
+ .put("entityId", tbMsg.getOriginator().getId().toString())
+ .put("entityName", tbMsg.getOriginator().getEntityType().name())
+ .put("msgId", tbMsg.getId().toString())
+ .put("msgType", tbMsg.getType())
+ .put("dataType", tbMsg.getDataType().name())
+ .put("data", tbMsg.getData())
+ .put("metadata", metadata);
+
+ if (error != null) {
+ node = node.put("error", toString(error));
+ }
+
+ event.setBody(node);
+ eventService.save(event);
+ } catch (IOException ex) {
+ log.warn("Failed to persist rule node debug message", ex);
+ }
+ }
+
+ public static Exception toException(Throwable error) {
+ return Exception.class.isInstance(error) ? (Exception) error : new Exception(error);
+ }
+
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
index b475277..a75158f 100644
--- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
@@ -22,48 +22,41 @@ import akka.event.LoggingAdapter;
import akka.japi.Function;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.plugin.PluginTerminationMsg;
-import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.ruleChain.RuleChainManagerActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.actors.service.DefaultActorService;
-import org.thingsboard.server.actors.shared.plugin.PluginManager;
import org.thingsboard.server.actors.shared.plugin.SystemPluginManager;
-import org.thingsboard.server.actors.shared.rule.RuleManager;
-import org.thingsboard.server.actors.shared.rule.SystemRuleManager;
-import org.thingsboard.server.actors.tenant.RuleChainDeviceMsg;
+import org.thingsboard.server.actors.shared.rulechain.SystemRuleChainManager;
import org.thingsboard.server.actors.tenant.TenantActor;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
-import org.thingsboard.server.extensions.api.rules.ToRuleActorMsg;
import scala.concurrent.duration.Duration;
import java.util.HashMap;
import java.util.Map;
-import java.util.Optional;
-public class AppActor extends ContextAwareActor {
+public class AppActor extends RuleChainManagerActor {
private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
public static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);
- private final RuleManager ruleManager;
- private final PluginManager pluginManager;
private final TenantService tenantService;
private final Map<TenantId, ActorRef> tenantActors;
private AppActor(ActorSystemContext systemContext) {
- super(systemContext);
- this.ruleManager = new SystemRuleManager(systemContext);
- this.pluginManager = new SystemPluginManager(systemContext);
+ super(systemContext, new SystemRuleChainManager(systemContext), new SystemPluginManager(systemContext));
this.tenantService = systemContext.getTenantService();
this.tenantActors = new HashMap<>();
}
@@ -77,8 +70,7 @@ public class AppActor extends ContextAwareActor {
public void preStart() {
logger.info("Starting main system actor.");
try {
- ruleManager.init(this.context());
- pluginManager.init(this.context());
+ initRuleChains();
if (systemContext.isTenantComponentsInitEnabled()) {
PageDataIterable<Tenant> tenantIterator = new PageDataIterable<>(tenantService::findTenants, ENTITY_PACK_LIMIT);
@@ -96,29 +88,51 @@ public class AppActor extends ContextAwareActor {
}
@Override
- public void onReceive(Object msg) throws Exception {
- logger.debug("Received message: {}", msg);
- if (msg instanceof ToDeviceActorMsg) {
- processDeviceMsg((ToDeviceActorMsg) msg);
- } else if (msg instanceof ToPluginActorMsg) {
- onToPluginMsg((ToPluginActorMsg) msg);
- } else if (msg instanceof ToRuleActorMsg) {
- onToRuleMsg((ToRuleActorMsg) msg);
- } else if (msg instanceof ToDeviceActorNotificationMsg) {
- onToDeviceActorMsg((ToDeviceActorNotificationMsg) msg);
- } else if (msg instanceof Terminated) {
- processTermination((Terminated) msg);
- } else if (msg instanceof ClusterEventMsg) {
- broadcast(msg);
- } else if (msg instanceof ComponentLifecycleMsg) {
- onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
- } else if (msg instanceof PluginTerminationMsg) {
- onPluginTerminated((PluginTerminationMsg) msg);
+ protected boolean process(TbActorMsg msg) {
+ switch (msg.getMsgType()) {
+ case COMPONENT_LIFE_CYCLE_MSG:
+ onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+ break;
+ case SERVICE_TO_RULE_ENGINE_MSG:
+ onServiceToRuleEngineMsg((ServiceToRuleEngineMsg) msg);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ private void onServiceToRuleEngineMsg(ServiceToRuleEngineMsg msg) {
+ if (SYSTEM_TENANT.equals(msg.getTenantId())) {
+ //TODO: ashvayka handle this.
} else {
- logger.warning("Unknown message: {}!", msg);
+ getOrCreateTenantActor(msg.getTenantId()).tell(msg, self());
}
}
+
+// @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 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());
}
@@ -128,20 +142,10 @@ public class AppActor extends ContextAwareActor {
tenantActors.values().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());
+ target = pluginManager.getOrCreateActor(this.context(), msg.getPluginId());
} else {
target = getOrCreateTenantActor(msg.getPluginTenantId());
}
@@ -149,26 +153,16 @@ public class AppActor extends ContextAwareActor {
}
private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
- ActorRef target = null;
+ ActorRef target;
if (SYSTEM_TENANT.equals(msg.getTenantId())) {
- Optional<PluginId> pluginId = msg.getPluginId();
- Optional<RuleId> ruleId = msg.getRuleId();
- if (pluginId.isPresent()) {
- target = pluginManager.getOrCreatePluginActor(this.context(), pluginId.get());
- } else if (ruleId.isPresent()) {
- Optional<ActorRef> ref = ruleManager.update(this.context(), ruleId.get(), msg.getEvent());
- if (ref.isPresent()) {
- target = ref.get();
- } else {
- logger.debug("Failed to find actor for rule: [{}]", ruleId);
- return;
- }
- }
+ target = getEntityActorRef(msg.getEntityId());
} else {
target = getOrCreateTenantActor(msg.getTenantId());
}
if (target != null) {
target.tell(msg, ActorRef.noSender());
+ } else {
+ logger.debug("Invalid component lifecycle msg: {}", msg);
}
}
@@ -180,7 +174,7 @@ public class AppActor extends ContextAwareActor {
TenantId tenantId = toDeviceActorMsg.getTenantId();
ActorRef tenantActor = getOrCreateTenantActor(tenantId);
if (toDeviceActorMsg.getPayload().getMsgType().requiresRulesProcessing()) {
- tenantActor.tell(new RuleChainDeviceMsg(toDeviceActorMsg, ruleManager.getRuleChain(this.context())), context().self());
+// tenantActor.tell(new RuleChainDeviceMsg(toDeviceActorMsg, ruleManager.getRuleChain(this.context())), context().self());
} else {
tenantActor.tell(toDeviceActorMsg, context().self());
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
index 861c405..87bc992 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
@@ -18,19 +18,19 @@ 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.TbActorMsg;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.extensions.api.device.DeviceNameOrTypeUpdateMsg;
import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.*;
+import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
public class DeviceActor extends ContextAwareActor {
@@ -48,12 +48,17 @@ public class DeviceActor extends ContextAwareActor {
}
@Override
+ protected boolean process(TbActorMsg msg) {
+ return false;
+ }
+
+ @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) {
+// if (msg instanceof RuleChainDeviceMsg) {
+// processor.process(context(), (RuleChainDeviceMsg) msg);
+// } else if (msg instanceof RulesProcessedMsg) {
+// processor.onRulesProcessedMsg(context(), (RulesProcessedMsg) msg);
+ if (msg instanceof ToDeviceActorMsg) {
processor.process(context(), (ToDeviceActorMsg) msg);
} else if (msg instanceof ToDeviceActorNotificationMsg) {
if (msg instanceof DeviceAttributesEventNotificationMsg) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
index 21112bf..3644a49 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
@@ -19,9 +19,7 @@ import akka.actor.ActorContext;
import akka.actor.ActorRef;
import akka.event.LoggingAdapter;
import org.thingsboard.server.actors.ActorSystemContext;
-import org.thingsboard.server.actors.rule.*;
import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
-import org.thingsboard.server.actors.tenant.RuleChainDeviceMsg;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.DeviceId;
@@ -37,15 +35,10 @@ 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.*;
-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 org.thingsboard.server.extensions.api.device.DeviceAttributes;
+import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.server.extensions.api.device.DeviceNameOrTypeUpdateMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.*;
import java.util.*;
import java.util.concurrent.ExecutionException;
@@ -230,18 +223,18 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
}
- void process(ActorContext context, RuleChainDeviceMsg srcMsg) {
- ChainProcessingMetaData md = new ChainProcessingMetaData(srcMsg.getRuleChain(),
- srcMsg.getToDeviceActorMsg(), new DeviceMetaData(deviceId, deviceName, deviceType, deviceAttributes), context.self());
- ChainProcessingContext ctx = new ChainProcessingContext(md);
- if (ctx.getChainLength() > 0) {
- RuleProcessingMsg msg = new RuleProcessingMsg(ctx);
- ActorRef ruleActorRef = ctx.getCurrentActor();
- ruleActorRef.tell(msg, ActorRef.noSender());
- } else {
- context.self().tell(new RulesProcessedMsg(ctx), context.self());
- }
- }
+// void process(ActorContext context, RuleChainDeviceMsg srcMsg) {
+// ChainProcessingMetaData md = new ChainProcessingMetaData(srcMsg.getRuleChain(),
+// srcMsg.getToDeviceActorMsg(), new DeviceMetaData(deviceId, deviceName, deviceType, deviceAttributes), context.self());
+// ChainProcessingContext ctx = new ChainProcessingContext(md);
+// if (ctx.getChainLength() > 0) {
+// RuleProcessingMsg msg = new RuleProcessingMsg(ctx);
+// ActorRef ruleActorRef = ctx.getCurrentActor();
+// ruleActorRef.tell(msg, ActorRef.noSender());
+// } else {
+// context.self().tell(new RulesProcessedMsg(ctx), context.self());
+// }
+// }
void processRpcResponses(ActorContext context, ToDeviceActorMsg msg) {
SessionId sessionId = msg.getSessionId();
@@ -302,18 +295,18 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
);
}
- 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());
- }
+// 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();
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
index 265da38..88278f3 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActor.java
@@ -23,6 +23,7 @@ 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.TbActorMsg;
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;
@@ -41,6 +42,12 @@ public class PluginActor extends ComponentActor<PluginId, PluginActorMessageProc
}
@Override
+ protected boolean process(TbActorMsg msg) {
+ //TODO Move everything here, to work with TbActorMsg
+ return false;
+ }
+
+ @Override
public void onReceive(Object msg) throws Exception {
if (msg instanceof PluginWebsocketMsg) {
onWebsocketMsg((PluginWebsocketMsg<?>) msg);
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
index 6e78e20..f6bf54d 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
@@ -28,9 +28,14 @@ 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.common.msg.core.BasicStatusCodeResponse;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
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.ResponsePluginToRuleMsg;
+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.PluginRpcMsg;
@@ -57,7 +62,7 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
}
@Override
- public void start() throws Exception {
+ public void start(ActorContext context) throws Exception {
logger.info("[{}] Going to start plugin actor.", entityId);
pluginMd = systemContext.getPluginService().findPluginById(entityId);
if (pluginMd == null) {
@@ -76,7 +81,7 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
}
@Override
- public void stop() throws Exception {
+ public void stop(ActorContext context) throws Exception {
onStop();
}
@@ -98,7 +103,20 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
public void onRuleToPluginMsg(RuleToPluginMsgWrapper msg) throws RuleException {
if (state == ComponentLifecycleState.ACTIVE) {
- pluginImpl.process(trustedCtx, msg.getRuleTenantId(), msg.getRuleId(), msg.getMsg());
+ try {
+ pluginImpl.process(trustedCtx, msg.getRuleTenantId(), msg.getRuleId(), msg.getMsg());
+ } catch (Exception ex) {
+ logger.debug("[{}] Failed to process RuleToPlugin msg: [{}] [{}]", tenantId, msg.getMsg(), ex);
+ RuleToPluginMsg ruleMsg = msg.getMsg();
+ MsgType responceMsgType = MsgType.RULE_ENGINE_ERROR;
+ Integer requestId = 0;
+ if (ruleMsg.getPayload() instanceof FromDeviceRequestMsg) {
+ requestId = ((FromDeviceRequestMsg) ruleMsg.getPayload()).getRequestId();
+ }
+ trustedCtx.reply(
+ new ResponsePluginToRuleMsg(ruleMsg.getUid(), tenantId, msg.getRuleId(),
+ BasicStatusCodeResponse.onError(responceMsgType, requestId, ex)));
+ }
} else {
//TODO: reply with plugin suspended message
}
@@ -191,7 +209,7 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
if (pluginImpl != null) {
pluginImpl.stop(trustedCtx);
}
- start();
+ start(context);
}
}
@@ -217,7 +235,7 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
pluginImpl.resume(trustedCtx);
logger.info("[{}] Plugin resumed.", entityId);
} else {
- start();
+ start(context);
}
}
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
index ce95ee4..10f16ce 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
@@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.plugin.PluginMetaData;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleMetaData;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
@@ -330,6 +331,9 @@ public final class PluginProcessingContext implements PluginContext {
case RULE:
validateRule(ctx, entityId, callback);
return;
+ case RULE_CHAIN:
+ validateRuleChain(ctx, entityId, callback);
+ return;
case PLUGIN:
validatePlugin(ctx, entityId, callback);
return;
@@ -411,6 +415,28 @@ public final class PluginProcessingContext implements PluginContext {
}
}
+ private void validateRuleChain(final PluginApiCallSecurityContext ctx, EntityId entityId, ValidationCallback callback) {
+ if (ctx.isCustomerUser()) {
+ callback.onSuccess(this, ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+ } else {
+ ListenableFuture<RuleChain> ruleChainFuture = pluginCtx.ruleChainService.findRuleChainByIdAsync(new RuleChainId(entityId.getId()));
+ Futures.addCallback(ruleChainFuture, getCallback(callback, ruleChain -> {
+ if (ruleChain == null) {
+ return ValidationResult.entityNotFound("Rule chain with requested id wasn't found!");
+ } else {
+ if (ctx.isTenantAdmin() && !ruleChain.getTenantId().equals(ctx.getTenantId())) {
+ return ValidationResult.accessDenied("Rule chain doesn't belong to the current Tenant!");
+ } else if (ctx.isSystemAdmin() && !ruleChain.getTenantId().isNullUid()) {
+ return ValidationResult.accessDenied("Rule chain is not in system scope!");
+ } else {
+ return ValidationResult.ok();
+ }
+ }
+ }));
+ }
+ }
+
+
private void validatePlugin(final PluginApiCallSecurityContext ctx, EntityId entityId, ValidationCallback callback) {
if (ctx.isCustomerUser()) {
callback.onSuccess(this, ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
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
index 06138a3..1828970 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
@@ -32,6 +32,7 @@ import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.plugin.PluginService;
import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.rule.RuleService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
@@ -56,6 +57,7 @@ public final class SharedPluginProcessingContext {
final AssetService assetService;
final DeviceService deviceService;
final RuleService ruleService;
+ final RuleChainService ruleChainService;
final PluginService pluginService;
final CustomerService customerService;
final TenantService tenantService;
@@ -84,6 +86,7 @@ public final class SharedPluginProcessingContext {
this.rpcService = sysContext.getRpcService();
this.routingService = sysContext.getRoutingService();
this.ruleService = sysContext.getRuleService();
+ this.ruleChainService = sysContext.getRuleChainService();
this.pluginService = sysContext.getPluginService();
this.customerService = sysContext.getCustomerService();
this.tenantService = sysContext.getTenantService();
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
index 9290a8f..ba20013 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
@@ -23,6 +23,7 @@ import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.service.ContextAwareActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.actors.service.DefaultActorService;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
@@ -57,6 +58,12 @@ public class RpcManagerActor extends ContextAwareActor {
}
@Override
+ protected boolean process(TbActorMsg msg) {
+ //TODO Move everything here, to work with TbActorMsg
+ return false;
+ }
+
+ @Override
public void onReceive(Object msg) throws Exception {
if (msg instanceof RpcSessionTellMsg) {
onMsg((RpcSessionTellMsg) msg);
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
index db029fa..a187444 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
@@ -23,6 +23,7 @@ import io.grpc.stub.StreamObserver;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.service.ContextAwareActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
import org.thingsboard.server.gen.cluster.ClusterRpcServiceGrpc;
@@ -48,6 +49,12 @@ public class RpcSessionActor extends ContextAwareActor {
}
@Override
+ protected boolean process(TbActorMsg msg) {
+ //TODO Move everything here, to work with TbActorMsg
+ return false;
+ }
+
+ @Override
public void onReceive(Object msg) throws Exception {
if (msg instanceof RpcSessionTellMsg) {
tell((RpcSessionTellMsg) msg);
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
new file mode 100644
index 0000000..8ffd378
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
@@ -0,0 +1,209 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import akka.actor.ActorRef;
+import akka.actor.Cancellable;
+import com.google.common.base.Function;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.dao.alarm.AlarmService;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.dao.user.UserService;
+import org.thingsboard.server.service.script.NashornJsEngine;
+import scala.concurrent.duration.Duration;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+class DefaultTbContext implements TbContext {
+
+ private static final Function<? super List<Void>, ? extends Void> LIST_VOID_FUNCTION = v -> null;
+ private final ActorSystemContext mainCtx;
+ private final RuleNodeCtx nodeCtx;
+
+ public DefaultTbContext(ActorSystemContext mainCtx, RuleNodeCtx nodeCtx) {
+ this.mainCtx = mainCtx;
+ this.nodeCtx = nodeCtx;
+ }
+
+ @Override
+ public void tellNext(TbMsg msg) {
+ tellNext(msg, (String) null);
+ }
+
+ @Override
+ public void tellNext(TbMsg msg, String relationType) {
+ if (nodeCtx.getSelf().isDebugMode()) {
+ mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg);
+ }
+ nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), relationType, msg), nodeCtx.getSelfActor());
+ }
+
+ @Override
+ public void tellSelf(TbMsg msg, long delayMs) {
+ //TODO: add persistence layer
+ scheduleMsgWithDelay(new RuleNodeToSelfMsg(msg), delayMs, nodeCtx.getSelfActor());
+ }
+
+ private void scheduleMsgWithDelay(Object msg, long delayInMs, ActorRef target) {
+ mainCtx.getScheduler().scheduleOnce(Duration.create(delayInMs, TimeUnit.MILLISECONDS), target, msg, mainCtx.getActorSystem().dispatcher(), nodeCtx.getSelfActor());
+ }
+
+ @Override
+ public void tellOthers(TbMsg msg) {
+ throw new RuntimeException("Not Implemented!");
+ }
+
+ @Override
+ public void tellSibling(TbMsg msg, ServerAddress address) {
+ throw new RuntimeException("Not Implemented!");
+ }
+
+ @Override
+ public void spawn(TbMsg msg) {
+ throw new RuntimeException("Not Implemented!");
+ }
+
+ @Override
+ public void ack(TbMsg msg) {
+
+ }
+
+ @Override
+ public void tellError(TbMsg msg, Throwable th) {
+ if (nodeCtx.getSelf().isDebugMode()) {
+ mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, th);
+ }
+ nodeCtx.getSelfActor().tell(new RuleNodeToSelfErrorMsg(msg, th), nodeCtx.getSelfActor());
+ }
+
+ @Override
+ public void updateSelf(RuleNode self) {
+ nodeCtx.setSelf(self);
+ }
+
+ @Override
+ public RuleNodeId getSelfId() {
+ return nodeCtx.getSelf().getId();
+ }
+
+ @Override
+ public TenantId getTenantId() {
+ return nodeCtx.getTenantId();
+ }
+
+ @Override
+ public void tellNext(TbMsg msg, Set<String> relationTypes) {
+ relationTypes.forEach(type -> tellNext(msg, type));
+ }
+
+ @Override
+ public ListeningExecutor getJsExecutor() {
+ return mainCtx.getJsExecutor();
+ }
+
+ @Override
+ public ListeningExecutor getMailExecutor() {
+ return mainCtx.getMailExecutor();
+ }
+
+ @Override
+ public ListeningExecutor getDbCallbackExecutor() {
+ return mainCtx.getDbCallbackExecutor();
+ }
+
+ @Override
+ public ScriptEngine createJsScriptEngine(String script, String functionName, String... argNames) {
+ return new NashornJsEngine(script, functionName, argNames);
+ }
+
+ @Override
+ public AttributesService getAttributesService() {
+ return mainCtx.getAttributesService();
+ }
+
+ @Override
+ public CustomerService getCustomerService() {
+ return mainCtx.getCustomerService();
+ }
+
+ @Override
+ public UserService getUserService() {
+ return mainCtx.getUserService();
+ }
+
+ @Override
+ public PluginService getPluginService() {
+ return mainCtx.getPluginService();
+ }
+
+ @Override
+ public AssetService getAssetService() {
+ return mainCtx.getAssetService();
+ }
+
+ @Override
+ public DeviceService getDeviceService() {
+ return mainCtx.getDeviceService();
+ }
+
+ @Override
+ public AlarmService getAlarmService() {
+ return mainCtx.getAlarmService();
+ }
+
+ @Override
+ public RuleChainService getRuleChainService() {
+ return mainCtx.getRuleChainService();
+ }
+
+ @Override
+ public TimeseriesService getTimeseriesService() {
+ return mainCtx.getTsService();
+ }
+
+ @Override
+ public RuleEngineTelemetryService getTelemetryService() {
+ return mainCtx.getTsSubService();
+ }
+
+ @Override
+ public RelationService getRelationService() {
+ return mainCtx.getRelationService();
+ }
+
+ @Override
+ public MailService getMailService() {
+ return mainCtx.getMailService();
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java
new file mode 100644
index 0000000..f539e32
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java
@@ -0,0 +1,88 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import akka.actor.OneForOneStrategy;
+import akka.actor.SupervisorStrategy;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ComponentActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import scala.concurrent.duration.Duration;
+
+public class RuleChainActor extends ComponentActor<RuleChainId, RuleChainActorMessageProcessor> {
+
+ private RuleChainActor(ActorSystemContext systemContext, TenantId tenantId, RuleChainId ruleChainId) {
+ super(systemContext, tenantId, ruleChainId);
+ setProcessor(new RuleChainActorMessageProcessor(tenantId, ruleChainId, systemContext,
+ logger, context().parent(), context().self()));
+ }
+
+ @Override
+ protected boolean process(TbActorMsg msg) {
+ switch (msg.getMsgType()) {
+ case COMPONENT_LIFE_CYCLE_MSG:
+ onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+ break;
+ case SERVICE_TO_RULE_ENGINE_MSG:
+ processor.onServiceToRuleEngineMsg((ServiceToRuleEngineMsg) msg);
+ break;
+ case RULE_TO_RULE_CHAIN_TELL_NEXT_MSG:
+ processor.onTellNext((RuleNodeToRuleChainTellNextMsg) msg);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ public static class ActorCreator extends ContextBasedCreator<RuleChainActor> {
+ private static final long serialVersionUID = 1L;
+
+ private final TenantId tenantId;
+ private final RuleChainId ruleChainId;
+
+ public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleChainId pluginId) {
+ super(context);
+ this.tenantId = tenantId;
+ this.ruleChainId = pluginId;
+ }
+
+ @Override
+ public RuleChainActor create() throws Exception {
+ return new RuleChainActor(context, tenantId, ruleChainId);
+ }
+ }
+
+ @Override
+ protected long getErrorPersistFrequency() {
+ return systemContext.getRuleChainErrorPersistFrequency();
+ }
+
+ @Override
+ public SupervisorStrategy supervisorStrategy() {
+ return strategy;
+ }
+
+ private final SupervisorStrategy strategy = new OneForOneStrategy(3, Duration.create("1 minute"), t -> {
+ logAndPersist("Unknown Failure", ActorSystemContext.toException(t));
+ return SupervisorStrategy.resume();
+ });
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
new file mode 100644
index 0000000..d588a63
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
@@ -0,0 +1,195 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.actor.Props;
+import akka.event.LoggingAdapter;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.DefaultActorService;
+import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleChainId> {
+
+ private final ActorRef parent;
+ private final ActorRef self;
+ private final Map<RuleNodeId, RuleNodeCtx> nodeActors;
+ private final Map<RuleNodeId, List<RuleNodeRelation>> nodeRoutes;
+ private final RuleChainService service;
+
+ private RuleNodeId firstId;
+ private RuleNodeCtx firstNode;
+
+ RuleChainActorMessageProcessor(TenantId tenantId, RuleChainId ruleChainId, ActorSystemContext systemContext
+ , LoggingAdapter logger, ActorRef parent, ActorRef self) {
+ super(systemContext, logger, tenantId, ruleChainId);
+ this.parent = parent;
+ this.self = self;
+ this.nodeActors = new HashMap<>();
+ this.nodeRoutes = new HashMap<>();
+ this.service = systemContext.getRuleChainService();
+ }
+
+ @Override
+ public void start(ActorContext context) throws Exception {
+ RuleChain ruleChain = service.findRuleChainById(entityId);
+ List<RuleNode> ruleNodeList = service.getRuleChainNodes(entityId);
+ // Creating and starting the actors;
+ for (RuleNode ruleNode : ruleNodeList) {
+ ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode);
+ nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode));
+ }
+ initRoutes(ruleChain, ruleNodeList);
+ }
+
+ @Override
+ public void onUpdate(ActorContext context) throws Exception {
+ RuleChain ruleChain = service.findRuleChainById(entityId);
+ List<RuleNode> ruleNodeList = service.getRuleChainNodes(entityId);
+
+ for (RuleNode ruleNode : ruleNodeList) {
+ RuleNodeCtx existing = nodeActors.get(ruleNode.getId());
+ if (existing == null) {
+ ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode);
+ nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode));
+ } else {
+ existing.setSelf(ruleNode);
+ existing.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, existing.getSelf().getId(), ComponentLifecycleEvent.UPDATED), self);
+ }
+ }
+
+ Set<RuleNodeId> existingNodes = ruleNodeList.stream().map(RuleNode::getId).collect(Collectors.toSet());
+ List<RuleNodeId> removedRules = nodeActors.keySet().stream().filter(node -> !existingNodes.contains(node)).collect(Collectors.toList());
+ removedRules.forEach(ruleNodeId -> {
+ RuleNodeCtx removed = nodeActors.remove(ruleNodeId);
+ removed.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, removed.getSelf().getId(), ComponentLifecycleEvent.DELETED), self);
+ });
+
+ initRoutes(ruleChain, ruleNodeList);
+ }
+
+ @Override
+ public void stop(ActorContext context) throws Exception {
+ nodeActors.values().stream().map(RuleNodeCtx::getSelfActor).forEach(context::stop);
+ nodeActors.clear();
+ nodeRoutes.clear();
+ context.stop(self);
+ }
+
+ @Override
+ public void onClusterEventMsg(ClusterEventMsg msg) throws Exception {
+
+ }
+
+ private ActorRef createRuleNodeActor(ActorContext context, RuleNode ruleNode) {
+ String dispatcherName = tenantId.getId().equals(EntityId.NULL_UUID) ?
+ DefaultActorService.SYSTEM_RULE_DISPATCHER_NAME : DefaultActorService.TENANT_RULE_DISPATCHER_NAME;
+ return context.actorOf(
+ Props.create(new RuleNodeActor.ActorCreator(systemContext, tenantId, entityId, ruleNode.getId()))
+ .withDispatcher(dispatcherName), ruleNode.getId().toString());
+ }
+
+ private void initRoutes(RuleChain ruleChain, List<RuleNode> ruleNodeList) {
+ nodeRoutes.clear();
+ // Populating the routes map;
+ for (RuleNode ruleNode : ruleNodeList) {
+ List<EntityRelation> relations = service.getRuleNodeRelations(ruleNode.getId());
+ for (EntityRelation relation : relations) {
+ if (relation.getTo().getEntityType() == EntityType.RULE_NODE) {
+ RuleNodeCtx ruleNodeCtx = nodeActors.get(new RuleNodeId(relation.getTo().getId()));
+ if (ruleNodeCtx == null) {
+ throw new IllegalArgumentException("Rule Node [" + relation.getFrom() + "] has invalid relation to Rule node [" + relation.getTo() + "]");
+ }
+ }
+ nodeRoutes.computeIfAbsent(ruleNode.getId(), k -> new ArrayList<>())
+ .add(new RuleNodeRelation(ruleNode.getId(), relation.getTo(), relation.getType()));
+ }
+ }
+
+ firstId = ruleChain.getFirstRuleNodeId();
+ firstNode = nodeActors.get(ruleChain.getFirstRuleNodeId());
+ state = ComponentLifecycleState.ACTIVE;
+ }
+
+ void onServiceToRuleEngineMsg(ServiceToRuleEngineMsg envelope) {
+ checkActive();
+ TbMsg tbMsg = envelope.getTbMsg();
+ //TODO: push to queue and act on ack in async way
+ pushMsgToNode(firstNode, tbMsg);
+ }
+
+ void onTellNext(RuleNodeToRuleChainTellNextMsg envelope) {
+ checkActive();
+ RuleNodeId originator = envelope.getOriginator();
+ String targetRelationType = envelope.getRelationType();
+ List<RuleNodeRelation> relations = nodeRoutes.get(originator);
+ if (relations == null) {
+ return;
+ }
+ boolean copy = relations.size() > 1;
+ for (RuleNodeRelation relation : relations) {
+ TbMsg msg = envelope.getMsg();
+ if (copy) {
+ msg = msg.copy();
+ }
+ if (targetRelationType == null || targetRelationType.equalsIgnoreCase(relation.getType())) {
+ switch (relation.getOut().getEntityType()) {
+ case RULE_NODE:
+ RuleNodeId targetRuleNodeId = new RuleNodeId(relation.getOut().getId());
+ RuleNodeCtx targetRuleNode = nodeActors.get(targetRuleNodeId);
+ pushMsgToNode(targetRuleNode, msg);
+ break;
+ case RULE_CHAIN:
+// TODO: implement
+ break;
+ }
+ }
+ }
+ }
+
+ private void pushMsgToNode(RuleNodeCtx nodeCtx, TbMsg msg) {
+ if (nodeCtx != null) {
+ nodeCtx.getSelfActor().tell(new RuleChainToRuleNodeMsg(new DefaultTbContext(systemContext, nodeCtx), msg), self);
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java
new file mode 100644
index 0000000..940bd5b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import akka.actor.ActorRef;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.shared.plugin.PluginManager;
+import org.thingsboard.server.actors.shared.rulechain.RuleChainManager;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+/**
+ * Created by ashvayka on 15.03.18.
+ */
+public abstract class RuleChainManagerActor extends ContextAwareActor {
+
+ protected final RuleChainManager ruleChainManager;
+ protected final PluginManager pluginManager;
+ protected final RuleChainService ruleChainService;
+
+ public RuleChainManagerActor(ActorSystemContext systemContext, RuleChainManager ruleChainManager, PluginManager pluginManager) {
+ super(systemContext);
+ this.ruleChainManager = ruleChainManager;
+ this.pluginManager = pluginManager;
+ this.ruleChainService = systemContext.getRuleChainService();
+ }
+
+ protected void initRuleChains() {
+ pluginManager.init(this.context());
+ ruleChainManager.init(this.context());
+ }
+
+ protected ActorRef getEntityActorRef(EntityId entityId) {
+ ActorRef target = null;
+ switch (entityId.getEntityType()) {
+ case PLUGIN:
+ target = pluginManager.getOrCreateActor(this.context(), (PluginId) entityId);
+ break;
+ case RULE_CHAIN:
+ target = ruleChainManager.getOrCreateActor(this.context(), (RuleChainId) entityId);
+ break;
+ }
+ return target;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java
new file mode 100644
index 0000000..e7d866c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+final class RuleChainToRuleNodeMsg implements TbActorMsg {
+
+ private final TbContext ctx;
+ private final TbMsg msg;
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.RULE_CHAIN_TO_RULE_MSG;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java
new file mode 100644
index 0000000..f7ca0d8
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java
@@ -0,0 +1,109 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ComponentActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+
+public class RuleNodeActor extends ComponentActor<RuleNodeId, RuleNodeActorMessageProcessor> {
+
+ private final RuleChainId ruleChainId;
+
+ private RuleNodeActor(ActorSystemContext systemContext, TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
+ super(systemContext, tenantId, ruleNodeId);
+ this.ruleChainId = ruleChainId;
+ setProcessor(new RuleNodeActorMessageProcessor(tenantId, ruleChainId, ruleNodeId, systemContext,
+ logger, context().parent(), context().self()));
+ }
+
+ @Override
+ protected boolean process(TbActorMsg msg) {
+ switch (msg.getMsgType()) {
+ case COMPONENT_LIFE_CYCLE_MSG:
+ onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+ break;
+ case RULE_CHAIN_TO_RULE_MSG:
+ onRuleChainToRuleNodeMsg((RuleChainToRuleNodeMsg) msg);
+ break;
+ case RULE_TO_SELF_ERROR_MSG:
+ onRuleNodeToSelfErrorMsg((RuleNodeToSelfErrorMsg) msg);
+ break;
+ case RULE_TO_SELF_MSG:
+ onRuleNodeToSelfMsg((RuleNodeToSelfMsg) msg);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ private void onRuleNodeToSelfMsg(RuleNodeToSelfMsg msg) {
+ logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg());
+ try {
+ processor.onRuleToSelfMsg(msg);
+ increaseMessagesProcessedCount();
+ } catch (Exception e) {
+ logAndPersist("onRuleMsg", e);
+ }
+ }
+
+ private void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) {
+ logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg());
+ try {
+ processor.onRuleChainToRuleNodeMsg(msg);
+ increaseMessagesProcessedCount();
+ } catch (Exception e) {
+ logAndPersist("onRuleMsg", e);
+ }
+ }
+
+ private void onRuleNodeToSelfErrorMsg(RuleNodeToSelfErrorMsg msg) {
+ logAndPersist("onRuleMsg", ActorSystemContext.toException(msg.getError()));
+ }
+
+ public static class ActorCreator extends ContextBasedCreator<RuleNodeActor> {
+ private static final long serialVersionUID = 1L;
+
+ private final TenantId tenantId;
+ private final RuleChainId ruleChainId;
+ private final RuleNodeId ruleNodeId;
+
+ public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
+ super(context);
+ this.tenantId = tenantId;
+ this.ruleChainId = ruleChainId;
+ this.ruleNodeId = ruleNodeId;
+
+ }
+
+ @Override
+ public RuleNodeActor create() throws Exception {
+ return new RuleNodeActor(context, tenantId, ruleChainId, ruleNodeId);
+ }
+ }
+
+ @Override
+ protected long getErrorPersistFrequency() {
+ return systemContext.getRuleNodeErrorPersistFrequency();
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
new file mode 100644
index 0000000..ea857db
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.event.LoggingAdapter;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNode;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+import java.util.concurrent.ExecutionException;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> {
+
+ private final ActorRef parent;
+ private final ActorRef self;
+ private final RuleChainService service;
+ private RuleNode ruleNode;
+ private TbNode tbNode;
+ private TbContext defaultCtx;
+
+ RuleNodeActorMessageProcessor(TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId, ActorSystemContext systemContext
+ , LoggingAdapter logger, ActorRef parent, ActorRef self) {
+ super(systemContext, logger, tenantId, ruleNodeId);
+ this.parent = parent;
+ this.self = self;
+ this.service = systemContext.getRuleChainService();
+ this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(entityId);
+ this.defaultCtx = new DefaultTbContext(systemContext, new RuleNodeCtx(tenantId, parent, self, ruleNode));
+ }
+
+ @Override
+ public void start(ActorContext context) throws Exception {
+ tbNode = initComponent(ruleNode);
+ state = ComponentLifecycleState.ACTIVE;
+ }
+
+ @Override
+ public void onUpdate(ActorContext context) throws Exception {
+ RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(entityId);
+ boolean restartRequired = !(ruleNode.getType().equals(newRuleNode.getType())
+ && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration()));
+ this.ruleNode = newRuleNode;
+ this.defaultCtx.updateSelf(newRuleNode);
+ if (restartRequired) {
+ if (tbNode != null) {
+ tbNode.destroy();
+ }
+ start(context);
+ }
+ }
+
+ @Override
+ public void stop(ActorContext context) throws Exception {
+ if (tbNode != null) {
+ tbNode.destroy();
+ }
+ context.stop(self);
+ }
+
+ @Override
+ public void onClusterEventMsg(ClusterEventMsg msg) throws Exception {
+
+ }
+
+ public void onRuleToSelfMsg(RuleNodeToSelfMsg msg) throws Exception {
+ checkActive();
+ if (ruleNode.isDebugMode()) {
+ systemContext.persistDebugInput(tenantId, entityId, msg.getMsg());
+ }
+ tbNode.onMsg(defaultCtx, msg.getMsg());
+ }
+
+ void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception {
+ checkActive();
+ if (ruleNode.isDebugMode()) {
+ systemContext.persistDebugInput(tenantId, entityId, msg.getMsg());
+ }
+ tbNode.onMsg(msg.getCtx(), msg.getMsg());
+ }
+
+ private TbNode initComponent(RuleNode ruleNode) throws Exception {
+ Class<?> componentClazz = Class.forName(ruleNode.getType());
+ TbNode tbNode = (TbNode) (componentClazz.newInstance());
+ tbNode.init(defaultCtx, new TbNodeConfiguration(ruleNode.getConfiguration()));
+ return tbNode;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeCtx.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeCtx.java
new file mode 100644
index 0000000..10fcc8b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeCtx.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import akka.actor.ActorRef;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.rule.RuleNode;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+@AllArgsConstructor
+final class RuleNodeCtx {
+ private final TenantId tenantId;
+ private final ActorRef chainActor;
+ private final ActorRef selfActor;
+ private RuleNode self;
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeRelation.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeRelation.java
new file mode 100644
index 0000000..7861e54
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeRelation.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.EntityId;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+
+@Data
+final class RuleNodeRelation {
+
+ private final EntityId in;
+ private final EntityId out;
+ private final String type;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java
new file mode 100644
index 0000000..054284d
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+final class RuleNodeToRuleChainTellNextMsg implements TbActorMsg {
+
+ private final RuleNodeId originator;
+ private final String relationType;
+ private final TbMsg msg;
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.RULE_TO_RULE_CHAIN_TELL_NEXT_MSG;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfErrorMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfErrorMsg.java
new file mode 100644
index 0000000..e6248f1
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfErrorMsg.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+final class RuleNodeToSelfErrorMsg implements TbActorMsg {
+
+ private final TbMsg msg;
+ private final Throwable error;
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.RULE_TO_SELF_ERROR_MSG;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfMsg.java
new file mode 100644
index 0000000..5c5af42
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfMsg.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+final class RuleNodeToSelfMsg implements TbActorMsg {
+
+ private final TbMsg msg;
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.RULE_TO_SELF_MSG;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
index baae376..0be0385 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
@@ -15,20 +15,19 @@
*/
package org.thingsboard.server.actors.service;
-import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
-import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.*;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.service.cluster.discovery.DiscoveryServiceListener;
import org.thingsboard.server.service.cluster.rpc.RpcMsgListener;
public interface ActorService extends SessionMsgProcessor, WebSocketMsgProcessor, RestMsgProcessor, RpcMsgListener, DiscoveryServiceListener {
- void onPluginStateChange(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent state);
+ void onEntityStateChange(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state);
- void onRuleStateChange(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent state);
+ void onMsg(ServiceToRuleEngineMsg msg);
void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId);
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
index 76b9be9..6aa68d3 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
@@ -54,7 +54,7 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
@Override
public void preStart() {
try {
- processor.start();
+ processor.start(context());
logLifecycleEvent(ComponentLifecycleEvent.STARTED);
if (systemContext.isStatisticsEnabled()) {
scheduleStatsPersistTick();
@@ -78,7 +78,7 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
@Override
public void postStop() {
try {
- processor.stop();
+ processor.stop(context());
logLifecycleEvent(ComponentLifecycleEvent.STOPPED);
} catch (Exception e) {
logger.warning("[{}][{}] Failed to stop {} processor: {}", tenantId, id, id.getEntityType(), e.getMessage());
@@ -141,7 +141,6 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
messagesProcessed++;
}
-
protected void logAndPersist(String method, Exception e) {
logAndPersist(method, e, false);
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java b/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java
index 825c971..1d9c671 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java
@@ -16,9 +16,13 @@
package org.thingsboard.server.actors.service;
import akka.actor.UntypedActor;
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.msg.TbActorMsg;
public abstract class ContextAwareActor extends UntypedActor {
+ protected final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
public static final int ENTITY_PACK_LIMIT = 1024;
@@ -28,4 +32,20 @@ public abstract class ContextAwareActor extends UntypedActor {
super();
this.systemContext = systemContext;
}
+
+ @Override
+ public void onReceive(Object msg) throws Exception {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Processing msg: {}", msg);
+ }
+ if (msg instanceof TbActorMsg) {
+ if(!process((TbActorMsg) msg)){
+ logger.warning("Unknown message: {}!", msg);
+ }
+ } else {
+ logger.warning("Unknown message: {}!", msg);
+ }
+ }
+
+ protected abstract boolean process(TbActorMsg msg);
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
index bb84a30..fbb5a14 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
@@ -30,16 +30,14 @@ import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg;
import org.thingsboard.server.actors.rpc.RpcSessionTellMsg;
import org.thingsboard.server.actors.session.SessionManagerActor;
import org.thingsboard.server.actors.stats.StatsActor;
-import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
-import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.*;
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.ServerAddress;
import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
import org.thingsboard.server.extensions.api.device.DeviceNameOrTypeUpdateMsg;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
@@ -129,6 +127,11 @@ public class DefaultActorService implements ActorService {
}
@Override
+ public void onMsg(ServiceToRuleEngineMsg msg) {
+ appActor.tell(msg, ActorRef.noSender());
+ }
+
+ @Override
public void process(SessionAwareMsg msg) {
log.debug("Processing session aware msg: {}", msg);
sessionManagerActor.tell(msg, ActorRef.noSender());
@@ -212,15 +215,9 @@ public class DefaultActorService implements ActorService {
}
@Override
- public void onPluginStateChange(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent state) {
- log.trace("[{}] Processing onPluginStateChange event: {}", pluginId, state);
- broadcast(ComponentLifecycleMsg.forPlugin(tenantId, pluginId, state));
- }
-
- @Override
- public void onRuleStateChange(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent state) {
- log.trace("[{}] Processing onRuleStateChange event: {}", ruleId, state);
- broadcast(ComponentLifecycleMsg.forRule(tenantId, ruleId, state));
+ public void onEntityStateChange(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state) {
+ log.trace("[{}] Processing {} state change event: {}", tenantId, entityId.getEntityType(), state);
+ broadcast(new ComponentLifecycleMsg(tenantId, entityId, state));
}
@Override
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java b/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java
index 37827d6..9d324c5 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java
@@ -23,6 +23,7 @@ import org.thingsboard.server.actors.service.ContextAwareActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
import org.thingsboard.server.common.msg.session.ToDeviceActorSessionMsg;
@@ -61,6 +62,12 @@ public class SessionActor extends ContextAwareActor {
}
@Override
+ protected boolean process(TbActorMsg msg) {
+ //TODO Move everything here, to work with TbActorMsg
+ return false;
+ }
+
+ @Override
public void onReceive(Object msg) throws Exception {
logger.debug("[{}] Processing: {}.", sessionId, msg);
if (msg instanceof ToDeviceActorSessionMsg) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
index 9d67dab..b5b1791 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
@@ -26,6 +26,7 @@ import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
import akka.event.Logging;
@@ -49,6 +50,12 @@ public class SessionManagerActor extends ContextAwareActor {
}
@Override
+ protected boolean process(TbActorMsg msg) {
+ //TODO Move everything here, to work with TbActorMsg
+ return false;
+ }
+
+ @Override
public void onReceive(Object msg) throws Exception {
if (msg instanceof SessionCtrlMsg) {
onSessionCtrlMsg((SessionCtrlMsg) msg);
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
index 73b221f..e1313d2 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
@@ -102,9 +102,6 @@ public abstract class AbstractContextAwareMsgProcessor {
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;
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
index 18d32d9..e25d3a7 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
@@ -20,12 +20,14 @@ 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.data.plugin.ComponentLifecycleState;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
public abstract class ComponentMsgProcessor<T> extends AbstractContextAwareMsgProcessor {
protected final TenantId tenantId;
protected final T entityId;
+ protected ComponentLifecycleState state;
protected ComponentMsgProcessor(ActorSystemContext systemContext, LoggingAdapter logger, TenantId tenantId, T id) {
super(systemContext, logger);
@@ -33,23 +35,44 @@ public abstract class ComponentMsgProcessor<T> extends AbstractContextAwareMsgPr
this.entityId = id;
}
- public abstract void start() throws Exception;
+ public abstract void start(ActorContext context) throws Exception;
- public abstract void stop() throws Exception;
+ public abstract void stop(ActorContext context) throws Exception;
- public abstract void onCreated(ActorContext context) throws Exception;
+ public abstract void onClusterEventMsg(ClusterEventMsg msg) throws Exception;
- public abstract void onUpdate(ActorContext context) throws Exception;
+ public void onCreated(ActorContext context) throws Exception {
+ start(context);
+ }
- public abstract void onActivate(ActorContext context) throws Exception;
+ public void onUpdate(ActorContext context) throws Exception {
+ restart(context);
+ }
- public abstract void onSuspend(ActorContext context) throws Exception;
+ public void onActivate(ActorContext context) throws Exception {
+ restart(context);
+ }
- public abstract void onStop(ActorContext context) throws Exception;
+ public void onSuspend(ActorContext context) throws Exception {
+ stop(context);
+ }
- public abstract void onClusterEventMsg(ClusterEventMsg msg) throws Exception;
+ public void onStop(ActorContext context) throws Exception {
+ stop(context);
+ }
+
+ private void restart(ActorContext context) throws Exception {
+ stop(context);
+ start(context);
+ }
public void scheduleStatsPersistTick(ActorContext context, long statsPersistFrequency) {
schedulePeriodicMsgWithDelay(context, new StatsPersistTick(), statsPersistFrequency, statsPersistFrequency);
}
+
+ protected void checkActive() {
+ if (state != ComponentLifecycleState.ACTIVE) {
+ throw new IllegalStateException("Rule chain is not active!");
+ }
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/EntityActorsManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/EntityActorsManager.java
new file mode 100644
index 0000000..d4a1f34
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/EntityActorsManager.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.shared;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.actor.Props;
+import akka.actor.UntypedActor;
+import akka.japi.Creator;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.page.PageDataIterable;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by ashvayka on 15.03.18.
+ */
+@Slf4j
+public abstract class EntityActorsManager<T extends EntityId, A extends UntypedActor, M extends SearchTextBased<? extends UUIDBased>> {
+
+ protected final ActorSystemContext systemContext;
+ protected final Map<T, ActorRef> actors;
+
+ public EntityActorsManager(ActorSystemContext systemContext) {
+ this.systemContext = systemContext;
+ this.actors = new HashMap<>();
+ }
+
+ protected abstract TenantId getTenantId();
+
+ protected abstract String getDispatcherName();
+
+ protected abstract Creator<A> creator(T entityId);
+
+ protected abstract PageDataIterable.FetchFunction<M> getFetchEntitiesFunction();
+
+ public void init(ActorContext context) {
+ for (M entity : new PageDataIterable<>(getFetchEntitiesFunction(), ContextAwareActor.ENTITY_PACK_LIMIT)) {
+ T entityId = (T) entity.getId();
+ log.debug("[{}|{}] Creating entity actor", entityId.getEntityType(), entityId.getId());
+ //TODO: remove this cast making UUIDBased subclass of EntityId an interface and vice versa.
+ ActorRef actorRef = getOrCreateActor(context, entityId);
+ visit(entity, actorRef);
+ log.debug("[{}|{}] Entity actor created.", entityId.getEntityType(), entityId.getId());
+ }
+ }
+
+ protected void visit(M entity, ActorRef actorRef) {}
+
+ public ActorRef getOrCreateActor(ActorContext context, T entityId) {
+ return actors.computeIfAbsent(entityId, eId ->
+ context.actorOf(Props.create(creator(eId))
+ .withDispatcher(getDispatcherName()), eId.toString()));
+ }
+
+ public void broadcast(Object msg) {
+ actors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+ }
+
+ public void remove(T id) {
+ actors.remove(id);
+ }
+
+}
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
index 4f5871f..3345e5f 100644
--- 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
@@ -15,63 +15,28 @@
*/
package org.thingsboard.server.actors.shared.plugin;
-import akka.actor.ActorContext;
-import akka.actor.ActorRef;
-import akka.actor.Props;
+import akka.japi.Creator;
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.shared.EntityActorsManager;
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 java.util.HashMap;
-import java.util.Map;
-
@Slf4j
-public abstract class PluginManager {
+public abstract class PluginManager extends EntityActorsManager<PluginId, PluginActor, PluginMetaData> {
- protected final ActorSystemContext systemContext;
protected final PluginService pluginService;
- protected final Map<PluginId, ActorRef> pluginActors;
public PluginManager(ActorSystemContext systemContext) {
- this.systemContext = systemContext;
+ super(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.");
- }
+ @Override
+ public Creator<PluginActor> creator(PluginId entityId){
+ return new PluginActor.ActorCreator(systemContext, getTenantId(), entityId);
}
- abstract FetchFunction<PluginMetaData> getFetchPluginsFunction();
-
- abstract TenantId getTenantId();
-
- abstract String getDispatcherName();
-
- public ActorRef getOrCreatePluginActor(ActorContext context, PluginId pluginId) {
- return pluginActors.computeIfAbsent(pluginId, pId ->
- context.actorOf(Props.create(new PluginActor.ActorCreator(systemContext, getTenantId(), pId))
- .withDispatcher(getDispatcherName()), pId.toString()));
- }
-
- public void broadcast(Object msg) {
- pluginActors.values().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
index 0888e23..88c52a6 100644
--- 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
@@ -29,12 +29,12 @@ public class SystemPluginManager extends PluginManager {
}
@Override
- FetchFunction<PluginMetaData> getFetchPluginsFunction() {
+ protected FetchFunction<PluginMetaData> getFetchEntitiesFunction() {
return pluginService::findSystemPlugins;
}
@Override
- TenantId getTenantId() {
+ protected 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
index 14ea2aa..09115f0 100644
--- 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
@@ -19,6 +19,7 @@ import akka.actor.ActorContext;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.service.DefaultActorService;
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;
@@ -39,12 +40,12 @@ public class TenantPluginManager extends PluginManager {
}
@Override
- FetchFunction<PluginMetaData> getFetchPluginsFunction() {
+ protected FetchFunction<PluginMetaData> getFetchEntitiesFunction() {
return link -> pluginService.findTenantPlugins(tenantId, link);
}
@Override
- TenantId getTenantId() {
+ protected TenantId getTenantId() {
return tenantId;
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java
new file mode 100644
index 0000000..ff0c52e
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.shared.rulechain;
+
+import akka.actor.ActorRef;
+import akka.japi.Creator;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.ruleChain.RuleChainActor;
+import org.thingsboard.server.actors.shared.EntityActorsManager;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+/**
+ * Created by ashvayka on 15.03.18.
+ */
+@Slf4j
+public abstract class RuleChainManager extends EntityActorsManager<RuleChainId, RuleChainActor, RuleChain> {
+
+ protected final RuleChainService service;
+ @Getter
+ protected RuleChain rootChain;
+ @Getter
+ protected ActorRef rootChainActor;
+
+ public RuleChainManager(ActorSystemContext systemContext) {
+ super(systemContext);
+ this.service = systemContext.getRuleChainService();
+ }
+
+ @Override
+ public Creator<RuleChainActor> creator(RuleChainId entityId) {
+ return new RuleChainActor.ActorCreator(systemContext, getTenantId(), entityId);
+ }
+
+ @Override
+ protected void visit(RuleChain entity, ActorRef actorRef) {
+ if (entity.isRoot()) {
+ rootChain = entity;
+ rootChainActor = actorRef;
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java b/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java
index ccc31cc..8623370 100644
--- a/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java
@@ -24,6 +24,7 @@ import org.thingsboard.server.actors.service.ContextAwareActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
public class StatsActor extends ContextAwareActor {
@@ -36,6 +37,12 @@ public class StatsActor extends ContextAwareActor {
}
@Override
+ protected boolean process(TbActorMsg msg) {
+ //TODO Move everything here, to work with TbActorMsg\
+ return false;
+ }
+
+ @Override
public void onReceive(Object msg) throws Exception {
logger.debug("Received message: {}", msg);
if (msg instanceof StatsPersistMsg) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
index b923fe1..d53c054 100644
--- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
@@ -15,52 +15,38 @@
*/
package org.thingsboard.server.actors.tenant;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-
+import akka.actor.ActorRef;
+import akka.actor.Props;
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
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.ruleChain.RuleChainManagerActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.actors.service.DefaultActorService;
-import org.thingsboard.server.actors.shared.plugin.PluginManager;
import org.thingsboard.server.actors.shared.plugin.TenantPluginManager;
-import org.thingsboard.server.actors.shared.rule.RuleManager;
-import org.thingsboard.server.actors.shared.rule.TenantRuleManager;
+import org.thingsboard.server.actors.shared.rulechain.TenantRuleChainManager;
import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.TbActorMsg;
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.common.msg.system.ServiceToRuleEngineMsg;
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 {
+import java.util.HashMap;
+import java.util.Map;
- private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
+public class TenantActor extends RuleChainManagerActor {
private final TenantId tenantId;
- private final RuleManager ruleManager;
- private final PluginManager pluginManager;
private final Map<DeviceId, ActorRef> deviceActors;
private TenantActor(ActorSystemContext systemContext, TenantId tenantId) {
- super(systemContext);
+ super(systemContext, new TenantRuleChainManager(systemContext, tenantId), new TenantPluginManager(systemContext, tenantId));
this.tenantId = tenantId;
- this.ruleManager = new TenantRuleManager(systemContext, tenantId);
- this.pluginManager = new TenantPluginManager(systemContext, tenantId);
this.deviceActors = new HashMap<>();
}
@@ -68,8 +54,7 @@ public class TenantActor extends ContextAwareActor {
public void preStart() {
logger.info("[{}] Starting tenant actor.", tenantId);
try {
- ruleManager.init(this.context());
- pluginManager.init(this.context());
+ initRuleChains();
logger.info("[{}] Tenant actor started.", tenantId);
} catch (Exception e) {
logger.error(e, "[{}] Unknown failure", tenantId);
@@ -77,29 +62,45 @@ public class TenantActor extends ContextAwareActor {
}
@Override
- public void onReceive(Object msg) throws Exception {
- logger.debug("[{}] Received message: {}", tenantId, msg);
- if (msg instanceof RuleChainDeviceMsg) {
- process((RuleChainDeviceMsg) msg);
- } else if (msg instanceof ToDeviceActorMsg) {
- onToDeviceActorMsg((ToDeviceActorMsg) msg);
- } else if (msg instanceof ToPluginActorMsg) {
- onToPluginMsg((ToPluginActorMsg) msg);
- } else if (msg instanceof ToRuleActorMsg) {
- onToRuleMsg((ToRuleActorMsg) msg);
- } else if (msg instanceof ToDeviceActorNotificationMsg) {
- onToDeviceActorMsg((ToDeviceActorNotificationMsg) msg);
- } else if (msg instanceof ClusterEventMsg) {
- broadcast(msg);
- } else if (msg instanceof ComponentLifecycleMsg) {
- onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
- } else if (msg instanceof PluginTerminationMsg) {
- onPluginTerminated((PluginTerminationMsg) msg);
- } else {
- logger.warning("[{}] Unknown message: {}!", tenantId, msg);
+ protected boolean process(TbActorMsg msg) {
+ switch (msg.getMsgType()) {
+ case COMPONENT_LIFE_CYCLE_MSG:
+ onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+ break;
+ case SERVICE_TO_RULE_ENGINE_MSG:
+ onServiceToRuleEngineMsg((ServiceToRuleEngineMsg) msg);
+ break;
+ default:
+ return false;
}
+ return true;
}
+ private void onServiceToRuleEngineMsg(ServiceToRuleEngineMsg msg) {
+ ruleChainManager.getRootChainActor().tell(msg, self());
+ }
+
+
+// @Override
+// public void onReceive(Object msg) throws Exception {
+// logger.debug("[{}] Received message: {}", tenantId, msg);
+// if (msg instanceof ToDeviceActorMsg) {
+// onToDeviceActorMsg((ToDeviceActorMsg) msg);
+// } else if (msg instanceof ToPluginActorMsg) {
+// onToPluginMsg((ToPluginActorMsg) 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().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
@@ -113,14 +114,9 @@ public class TenantActor extends ContextAwareActor {
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());
+ ActorRef pluginActor = pluginManager.getOrCreateActor(this.context(), msg.getPluginId());
pluginActor.tell(msg, ActorRef.noSender());
} else {
context().parent().tell(msg, ActorRef.noSender());
@@ -128,23 +124,11 @@ public class TenantActor extends ContextAwareActor {
}
private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
- Optional<PluginId> pluginId = msg.getPluginId();
- Optional<RuleId> ruleId = msg.getRuleId();
- if (pluginId.isPresent()) {
- ActorRef pluginActor = pluginManager.getOrCreatePluginActor(this.context(), pluginId.get());
- pluginActor.tell(msg, ActorRef.noSender());
- } else if (ruleId.isPresent()) {
- ActorRef target;
- Optional<ActorRef> ref = ruleManager.update(this.context(), ruleId.get(), msg.getEvent());
- if (ref.isPresent()) {
- target = ref.get();
- } else {
- logger.debug("Failed to find actor for rule: [{}]", ruleId);
- return;
- }
+ ActorRef target = getEntityActorRef(msg.getEntityId());
+ if (target != null) {
target.tell(msg, ActorRef.noSender());
} else {
- logger.debug("[{}] Invalid component lifecycle msg.", tenantId);
+ logger.debug("Invalid component lifecycle msg: {}", msg);
}
}
@@ -152,13 +136,6 @@ public class TenantActor extends ContextAwareActor {
pluginManager.remove(msg.getId());
}
- private void process(RuleChainDeviceMsg msg) {
- ToDeviceActorMsg toDeviceActorMsg = msg.getToDeviceActorMsg();
- ActorRef deviceActor = getOrCreateDeviceActor(toDeviceActorMsg.getDeviceId());
- RuleActorChain tenantChain = ruleManager.getRuleChain(this.context());
- RuleActorChain chain = new ComplexRuleActorChain(msg.getRuleChain(), tenantChain);
- deviceActor.tell(new RuleChainDeviceMsg(toDeviceActorMsg, chain), context().self());
- }
private ActorRef getOrCreateDeviceActor(DeviceId deviceId) {
return deviceActors.computeIfAbsent(deviceId, k -> context().actorOf(Props.create(new DeviceActor.ActorCreator(systemContext, tenantId, deviceId))
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
index 2952529..24c533c 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
@@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
@@ -37,7 +36,6 @@ import org.springframework.security.web.authentication.AuthenticationFailureHand
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
-import org.springframework.web.cors.CorsUtils;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
diff --git a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
index a75ecb1..2e5050a 100644
--- a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
@@ -17,9 +17,9 @@ package org.thingsboard.server.config;
import java.util.Map;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.controller.plugin.PluginWebSocketHandler;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.controller.plugin.TbWebSocketHandler;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -54,7 +54,7 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
- registry.addHandler(pluginWsHandler(), WS_PLUGIN_MAPPING).setAllowedOrigins("*")
+ registry.addHandler(wsHandler(), WS_PLUGIN_MAPPING).setAllowedOrigins("*")
.addInterceptors(new HttpSessionHandshakeInterceptor(), new HandshakeInterceptor() {
@Override
@@ -82,8 +82,8 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
}
@Bean
- public WebSocketHandler pluginWsHandler() {
- return new PluginWebSocketHandler();
+ public WebSocketHandler wsHandler() {
+ return new TbWebSocketHandler();
}
protected SecurityUser getCurrentUser() throws ThingsboardException {
diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
index e9a6ba3..5a43125 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
@@ -20,8 +20,8 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.dao.settings.AdminSettingsService;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.service.mail.MailService;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.service.update.UpdateService;
import org.thingsboard.server.service.update.model.UpdateMessage;
diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
index 1959f4e..81bcf7e 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
@@ -23,8 +23,8 @@ import org.thingsboard.server.common.data.alarm.*;
import org.thingsboard.server.common.data.id.*;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
@RestController
@RequestMapping("/api")
diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
index 9b43913..0e348f9 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
@@ -33,8 +33,8 @@ import org.thingsboard.server.common.data.asset.AssetSearchQuery;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.ArrayList;
diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
index 75bcf2a..e8685c7 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
@@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import java.util.UUID;
diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
index ef38d80..96ff516 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
@@ -28,9 +28,9 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.security.UserCredentials;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.service.mail.MailService;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index 83b304f..29436c1 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -15,12 +15,9 @@
*/
package org.thingsboard.server.controller;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -30,7 +27,6 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmId;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.asset.Asset;
-import org.thingsboard.server.common.data.audit.ActionStatus;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.*;
import org.thingsboard.server.common.data.page.TextPageLink;
@@ -49,6 +45,7 @@ import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
@@ -57,12 +54,13 @@ import org.thingsboard.server.dao.plugin.PluginService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.security.model.SecurityUser;
@@ -71,6 +69,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import java.util.UUID;
import static org.thingsboard.server.dao.service.Validator.validateId;
@@ -85,6 +84,9 @@ public abstract class BaseController {
private ThingsboardErrorResponseHandler errorResponseHandler;
@Autowired
+ protected TenantService tenantService;
+
+ @Autowired
protected CustomerService customerService;
@Autowired
@@ -132,6 +134,9 @@ public abstract class BaseController {
@Autowired
protected AuditLogService auditLogService;
+ @Autowired
+ protected DeviceOfflineService offlineService;
+
@ExceptionHandler(ThingsboardException.class)
public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
errorResponseHandler.handle(ex, response);
@@ -480,6 +485,15 @@ public abstract class BaseController {
}
}
+ List<ComponentDescriptor> checkComponentDescriptorsByTypes(Set<ComponentType> types) throws ThingsboardException {
+ try {
+ log.debug("[{}] Lookup component descriptors", types);
+ return componentDescriptorService.getComponents(types);
+ } catch (Exception e) {
+ throw handleException(e, false);
+ }
+ }
+
List<ComponentDescriptor> checkPluginActionsByPluginClazz(String pluginClazz) throws ThingsboardException {
try {
checkComponentDescriptorByClazz(pluginClazz);
@@ -550,6 +564,8 @@ public abstract class BaseController {
throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION,
ThingsboardErrorCode.PERMISSION_DENIED);
+ } else if (tenantId.getId().equals(ModelConstants.NULL_UUID)) {
+ ruleChain.setConfiguration(null);
}
}
return ruleChain;
@@ -590,5 +606,8 @@ public abstract class BaseController {
auditLogService.logEntityAction(user.getTenantId(), customerId, user.getId(), user.getName(), entityId, entity, actionType, e, additionalInfo);
}
+ protected static Exception toException(Throwable error) {
+ return Exception.class.isInstance(error) ? (Exception) error : new Exception(error);
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
index e63a443..6313d61 100644
--- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
@@ -19,9 +19,11 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentType;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
@RestController
@RequestMapping("/api")
@@ -52,6 +54,22 @@ public class ComponentDescriptorController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
+ @RequestMapping(value = "/components", params = {"componentTypes"}, method = RequestMethod.GET)
+ @ResponseBody
+ public List<ComponentDescriptor> getComponentDescriptorsByTypes(@RequestParam("componentTypes") String[] strComponentTypes) throws ThingsboardException {
+ checkArrayParameter("componentTypes", strComponentTypes);
+ try {
+ Set<ComponentType> componentTypes = new HashSet<>();
+ for (String strComponentType : strComponentTypes) {
+ componentTypes.add(ComponentType.valueOf(strComponentType));
+ }
+ return checkComponentDescriptorsByTypes(componentTypes);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
@RequestMapping(value = "/components/actions/{pluginClazz:.+}", method = RequestMethod.GET)
@ResponseBody
public List<ComponentDescriptor> getPluginActionsByPluginClazz(@PathVariable("pluginClazz") String pluginClazz) throws ThingsboardException {
diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
index b164702..7763f3c 100644
--- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
@@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
@RestController
@RequestMapping("/api")
diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
index d2952a1..4ec2bba 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
@@ -27,9 +27,7 @@ import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
-import org.thingsboard.server.dao.exception.IncorrectParameterException;
-import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import java.util.HashSet;
import java.util.Set;
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
index bceea54..f97603e 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -23,9 +23,11 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
-import org.thingsboard.server.common.data.audit.ActionStatus;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
+import org.thingsboard.server.common.data.device.DeviceStatusQuery;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -35,8 +37,6 @@ import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.ArrayList;
@@ -70,7 +70,7 @@ public class DeviceController extends BaseController {
device.setTenantId(getCurrentUser().getTenantId());
if (getCurrentUser().getAuthority() == Authority.CUSTOMER_USER) {
if (device.getId() == null || device.getId().isNullUid() ||
- device.getCustomerId() == null || device.getCustomerId().isNullUid()) {
+ device.getCustomerId() == null || device.getCustomerId().isNullUid()) {
throw new ThingsboardException("You don't have permission to perform this operation!",
ThingsboardErrorCode.PERMISSION_DENIED);
} else {
@@ -368,4 +368,32 @@ public class DeviceController extends BaseController {
throw handleException(e);
}
}
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/device/offline", method = RequestMethod.GET)
+ @ResponseBody
+ public List<Device> getOfflineDevices(@RequestParam("contactType") DeviceStatusQuery.ContactType contactType,
+ @RequestParam("threshold") long threshold) throws ThingsboardException {
+ try {
+ TenantId tenantId = getCurrentUser().getTenantId();
+ ListenableFuture<List<Device>> offlineDevices = offlineService.findOfflineDevices(tenantId.getId(), contactType, threshold);
+ return checkNotNull(offlineDevices.get());
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/device/online", method = RequestMethod.GET)
+ @ResponseBody
+ public List<Device> getOnlineDevices(@RequestParam("contactType") DeviceStatusQuery.ContactType contactType,
+ @RequestParam("threshold") long threshold) throws ThingsboardException {
+ try {
+ TenantId tenantId = getCurrentUser().getTenantId();
+ ListenableFuture<List<Device>> offlineDevices = offlineService.findOnlineDevices(tenantId.getId(), contactType, threshold);
+ return checkNotNull(offlineDevices.get());
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
index 03054df..3a3b78b 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
@@ -24,8 +24,8 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationInfo;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import java.util.List;
diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java
index 331b15e..f67f113 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EventController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java
@@ -24,8 +24,8 @@ import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
@RestController
@RequestMapping("/api")
diff --git a/application/src/main/java/org/thingsboard/server/controller/HttpValidationCallback.java b/application/src/main/java/org/thingsboard/server/controller/HttpValidationCallback.java
new file mode 100644
index 0000000..fb1f3e7
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/HttpValidationCallback.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.google.common.util.concurrent.FutureCallback;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.service.security.ValidationCallback;
+
+/**
+ * Created by ashvayka on 21.02.17.
+ */
+public class HttpValidationCallback extends ValidationCallback<DeferredResult<ResponseEntity>> {
+
+ public HttpValidationCallback(DeferredResult<ResponseEntity> response, FutureCallback<DeferredResult<ResponseEntity>> action) {
+ super(response, action);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java
index 8e3cee4..8d25db3 100644
--- a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java
@@ -17,90 +17,69 @@ package org.thingsboard.server.controller.plugin;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.RequestEntity;
-import org.springframework.http.ResponseEntity;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.context.request.async.DeferredResult;
-import org.thingsboard.server.actors.service.ActorService;
-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.plugin.PluginMetaData;
import org.thingsboard.server.controller.BaseController;
-import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.dao.plugin.PluginService;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
import org.thingsboard.server.extensions.api.plugins.PluginConstants;
-import org.thingsboard.server.extensions.api.plugins.rest.BasicPluginRestMsg;
-import org.thingsboard.server.extensions.api.plugins.rest.RestRequest;
-
-import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping(PluginConstants.PLUGIN_URL_PREFIX)
@Slf4j
public class PluginApiController extends BaseController {
- @SuppressWarnings("rawtypes")
- @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
- @RequestMapping(value = "/{pluginToken}/**")
- @ResponseStatus(value = HttpStatus.OK)
- public DeferredResult<ResponseEntity> processRequest(
- @PathVariable("pluginToken") String pluginToken,
- RequestEntity<byte[]> requestEntity,
- HttpServletRequest request)
- throws ThingsboardException {
- log.debug("[{}] Going to process requst uri: {}", pluginToken, requestEntity.getUrl());
- DeferredResult<ResponseEntity> result = new DeferredResult<ResponseEntity>();
- PluginMetaData pluginMd = pluginService.findPluginByApiToken(pluginToken);
- if (pluginMd == null) {
- result.setErrorResult(new PluginNotFoundException("Plugin with token: " + pluginToken + " not found!"));
- } else {
- TenantId tenantId = getCurrentUser().getTenantId();
- CustomerId customerId = getCurrentUser().getCustomerId();
- if (validatePluginAccess(pluginMd, tenantId, customerId)) {
- if(tenantId != null && ModelConstants.NULL_UUID.equals(tenantId.getId())){
- tenantId = null;
- }
- UserId userId = getCurrentUser().getId();
- String userName = getCurrentUser().getName();
- PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(),
- tenantId, customerId, userId, userName);
- actorService.process(new BasicPluginRestMsg(securityCtx, new RestRequest(requestEntity, request), result));
- } else {
- result.setResult(new ResponseEntity<>(HttpStatus.FORBIDDEN));
- }
-
- }
- return result;
- }
-
- public static boolean validatePluginAccess(PluginMetaData pluginMd, TenantId tenantId, CustomerId customerId) {
- boolean systemAdministrator = tenantId == null || ModelConstants.NULL_UUID.equals(tenantId.getId());
- boolean tenantAdministrator = !systemAdministrator && (customerId == null || ModelConstants.NULL_UUID.equals(customerId.getId()));
- boolean systemPlugin = ModelConstants.NULL_UUID.equals(pluginMd.getTenantId().getId());
-
- boolean validUser = false;
- if (systemPlugin) {
- if (pluginMd.isPublicAccess() || systemAdministrator) {
- // All users can access public system plugins. Only system
- // users can access private system plugins
- validUser = true;
- }
- } else {
- if ((pluginMd.isPublicAccess() || tenantAdministrator) && tenantId != null && tenantId.equals(pluginMd.getTenantId())) {
- // All tenant users can access public tenant plugins. Only tenant
- // administrator can access private tenant plugins
- validUser = true;
- }
- }
- return validUser;
- }
+// @SuppressWarnings("rawtypes")
+// @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+// @RequestMapping(value = "/{pluginToken}/**")
+// @ResponseStatus(value = HttpStatus.OK)
+// public DeferredResult<ResponseEntity> processRequest(
+// @PathVariable("pluginToken") String pluginToken,
+// RequestEntity<byte[]> requestEntity,
+// HttpServletRequest request)
+// throws ThingsboardException {
+// log.debug("[{}] Going to process requst uri: {}", pluginToken, requestEntity.getUrl());
+// DeferredResult<ResponseEntity> result = new DeferredResult<ResponseEntity>();
+// PluginMetaData pluginMd = pluginService.findPluginByApiToken(pluginToken);
+// if (pluginMd == null) {
+// result.setErrorResult(new PluginNotFoundException("Plugin with token: " + pluginToken + " not found!"));
+// } else {
+// TenantId tenantId = getCurrentUser().getTenantId();
+// CustomerId customerId = getCurrentUser().getCustomerId();
+// if (validatePluginAccess(pluginMd, tenantId, customerId)) {
+// if(tenantId != null && ModelConstants.NULL_UUID.equals(tenantId.getId())){
+// tenantId = null;
+// }
+// UserId userId = getCurrentUser().getId();
+// String userName = getCurrentUser().getName();
+// PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(),
+// tenantId, customerId, userId, userName);
+// actorService.process(new BasicPluginRestMsg(securityCtx, new RestRequest(requestEntity, request), result));
+// } else {
+// result.setResult(new ResponseEntity<>(HttpStatus.FORBIDDEN));
+// }
+//
+// }
+// return result;
+// }
+//
+// public static boolean validatePluginAccess(PluginMetaData pluginMd, TenantId tenantId, CustomerId customerId) {
+// boolean systemAdministrator = tenantId == null || ModelConstants.NULL_UUID.equals(tenantId.getId());
+// boolean tenantAdministrator = !systemAdministrator && (customerId == null || ModelConstants.NULL_UUID.equals(customerId.getId()));
+// boolean systemPlugin = ModelConstants.NULL_UUID.equals(pluginMd.getTenantId().getId());
+//
+// boolean validUser = false;
+// if (systemPlugin) {
+// if (pluginMd.isPublicAccess() || systemAdministrator) {
+// // All users can access public system plugins. Only system
+// // users can access private system plugins
+// validUser = true;
+// }
+// } else {
+// if ((pluginMd.isPublicAccess() || tenantAdministrator) && tenantId != null && tenantId.equals(pluginMd.getTenantId())) {
+// // All tenant users can access public tenant plugins. Only tenant
+// // administrator can access private tenant plugins
+// validUser = true;
+// }
+// }
+// return validUser;
+// }
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/PluginController.java b/application/src/main/java/org/thingsboard/server/controller/PluginController.java
index 2c69248..3bc385d 100644
--- a/application/src/main/java/org/thingsboard/server/controller/PluginController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/PluginController.java
@@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.plugin.PluginMetaData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import java.util.List;
@@ -71,7 +71,7 @@ public class PluginController extends BaseController {
boolean created = source.getId() == null;
source.setTenantId(getCurrentUser().getTenantId());
PluginMetaData plugin = checkNotNull(pluginService.savePlugin(source));
- actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(),
+ actorService.onEntityStateChange(plugin.getTenantId(), plugin.getId(),
created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
logEntityAction(plugin.getId(), plugin,
@@ -97,7 +97,7 @@ public class PluginController extends BaseController {
PluginId pluginId = new PluginId(toUUID(strPluginId));
PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
pluginService.activatePluginById(pluginId);
- actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.ACTIVATED);
+ actorService.onEntityStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.ACTIVATED);
logEntityAction(plugin.getId(), plugin,
null,
@@ -123,7 +123,7 @@ public class PluginController extends BaseController {
PluginId pluginId = new PluginId(toUUID(strPluginId));
PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
pluginService.suspendPluginById(pluginId);
- actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.SUSPENDED);
+ actorService.onEntityStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.SUSPENDED);
logEntityAction(plugin.getId(), plugin,
null,
@@ -221,7 +221,7 @@ public class PluginController extends BaseController {
PluginId pluginId = new PluginId(toUUID(strPluginId));
PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
pluginService.deletePluginById(pluginId);
- actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.DELETED);
+ actorService.onEntityStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.DELETED);
logEntityAction(pluginId, plugin,
null,
diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
index 012e077..48c9cd5 100644
--- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
@@ -15,31 +15,46 @@
*/
package org.thingsboard.server.controller;
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
+import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
-import org.thingsboard.server.common.data.id.PluginId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
-import org.thingsboard.server.common.data.plugin.PluginMetaData;
import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.service.script.NashornJsEngine;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
+@Slf4j
@RestController
@RequestMapping("/api")
public class RuleChainController extends BaseController {
public static final String RULE_CHAIN_ID = "ruleChainId";
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET)
@ResponseBody
@@ -54,6 +69,21 @@ public class RuleChainController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/ruleChain/{ruleChainId}/metadata", method = RequestMethod.GET)
+ @ResponseBody
+ public RuleChainMetaData getRuleChainMetaData(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
+ checkParameter(RULE_CHAIN_ID, strRuleChainId);
+ try {
+ RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
+ checkRuleChain(ruleChainId);
+ return ruleChainService.loadRuleChainMetaData(ruleChainId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain", method = RequestMethod.POST)
@ResponseBody
public RuleChain saveRuleChain(@RequestBody RuleChain ruleChain) throws ThingsboardException {
@@ -62,6 +92,9 @@ public class RuleChainController extends BaseController {
ruleChain.setTenantId(getCurrentUser().getTenantId());
RuleChain savedRuleChain = checkNotNull(ruleChainService.saveRuleChain(ruleChain));
+ actorService.onEntityStateChange(ruleChain.getTenantId(), savedRuleChain.getId(),
+ created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
+
logEntityAction(savedRuleChain.getId(), savedRuleChain,
null,
created ? ActionType.ADDED : ActionType.UPDATED, null);
@@ -77,6 +110,30 @@ public class RuleChainController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/ruleChain/metadata", method = RequestMethod.POST)
+ @ResponseBody
+ public RuleChainMetaData saveRuleChainMetaData(@RequestBody RuleChainMetaData ruleChainMetaData) throws ThingsboardException {
+ try {
+ RuleChain ruleChain = checkRuleChain(ruleChainMetaData.getRuleChainId());
+ RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.saveRuleChainMetaData(ruleChainMetaData));
+
+ actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.UPDATED);
+
+ logEntityAction(ruleChain.getId(), ruleChain,
+ null,
+ ActionType.UPDATED, null, ruleChainMetaData);
+
+ return savedRuleChainMetaData;
+ } catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.RULE_CHAIN), null,
+ null, ActionType.UPDATED, e, ruleChainMetaData);
+
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/ruleChains", params = {"limit"}, method = RequestMethod.GET)
@ResponseBody
public TextPageData<RuleChain> getRuleChains(
@@ -145,6 +202,8 @@ public class RuleChainController extends BaseController {
RuleChain ruleChain = checkRuleChain(ruleChainId);
ruleChainService.deleteRuleChainById(ruleChainId);
+ actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.DELETED);
+
logEntityAction(ruleChainId, ruleChain,
null,
ActionType.DELETED, null, strRuleChainId);
@@ -158,4 +217,78 @@ public class RuleChainController extends BaseController {
}
}
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST)
+ @ResponseBody
+ public JsonNode testScript(@RequestBody JsonNode inputParams) throws ThingsboardException {
+ try {
+ String script = inputParams.get("script").asText();
+ String scriptType = inputParams.get("scriptType").asText();
+ String functionName = inputParams.get("functionName").asText();
+ JsonNode argNamesJson = inputParams.get("argNames");
+ String[] argNames = objectMapper.treeToValue(argNamesJson, String[].class);
+
+ String data = inputParams.get("msg").asText();
+ JsonNode metadataJson = inputParams.get("metadata");
+ Map<String, String> metadata = objectMapper.convertValue(metadataJson, new TypeReference<Map<String, String>>() {});
+ String msgType = inputParams.get("msgType").asText();
+ String output = "";
+ String errorText = "";
+ ScriptEngine engine = null;
+ try {
+ engine = new NashornJsEngine(script, functionName, argNames);
+ TbMsg inMsg = new TbMsg(UUIDs.timeBased(), msgType, null, new TbMsgMetaData(metadata), data);
+ switch (scriptType) {
+ case "update":
+ output = msgToOutput(engine.executeUpdate(inMsg));
+ break;
+ case "generate":
+ output = msgToOutput(engine.executeGenerate(inMsg));
+ break;
+ case "filter":
+ boolean result = engine.executeFilter(inMsg);
+ output = Boolean.toString(result);
+ break;
+ case "switch":
+ Set<String> states = engine.executeSwitch(inMsg);
+ output = objectMapper.writeValueAsString(states);
+ break;
+ case "json":
+ JsonNode json = engine.executeJson(inMsg);
+ output = objectMapper.writeValueAsString(json);
+ break;
+ case "string":
+ output = engine.executeToString(inMsg);
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported script type: " + scriptType);
+ }
+ } catch (Exception e) {
+ log.error("Error evaluating JS function", e);
+ errorText = e.getMessage();
+ } finally {
+ if (engine != null) {
+ engine.destroy();
+ }
+ }
+ ObjectNode result = objectMapper.createObjectNode();
+ result.put("output", output);
+ result.put("error", errorText);
+ return result;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ private String msgToOutput(TbMsg msg) throws Exception {
+ ObjectNode msgData = objectMapper.createObjectNode();
+ if (!StringUtils.isEmpty(msg.getData())) {
+ msgData.set("msg", objectMapper.readTree(msg.getData()));
+ }
+ Map<String, String> metadata = msg.getMetaData().getData();
+ msgData.set("metadata", objectMapper.valueToTree(metadata));
+ msgData.put("msgType", msg.getType());
+ return objectMapper.writeValueAsString(msgData);
+ }
+
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleController.java b/application/src/main/java/org/thingsboard/server/controller/RuleController.java
index e498c8f..4528d81 100644
--- a/application/src/main/java/org/thingsboard/server/controller/RuleController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/RuleController.java
@@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
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.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import java.util.List;
@@ -73,7 +73,7 @@ public class RuleController extends BaseController {
boolean created = source.getId() == null;
source.setTenantId(getCurrentUser().getTenantId());
RuleMetaData rule = checkNotNull(ruleService.saveRule(source));
- actorService.onRuleStateChange(rule.getTenantId(), rule.getId(),
+ actorService.onEntityStateChange(rule.getTenantId(), rule.getId(),
created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
logEntityAction(rule.getId(), rule,
@@ -99,7 +99,7 @@ public class RuleController extends BaseController {
RuleId ruleId = new RuleId(toUUID(strRuleId));
RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
ruleService.activateRuleById(ruleId);
- actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.ACTIVATED);
+ actorService.onEntityStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.ACTIVATED);
logEntityAction(rule.getId(), rule,
null,
@@ -125,7 +125,7 @@ public class RuleController extends BaseController {
RuleId ruleId = new RuleId(toUUID(strRuleId));
RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
ruleService.suspendRuleById(ruleId);
- actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.SUSPENDED);
+ actorService.onEntityStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.SUSPENDED);
logEntityAction(rule.getId(), rule,
null,
@@ -219,7 +219,7 @@ public class RuleController extends BaseController {
RuleId ruleId = new RuleId(toUUID(strRuleId));
RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
ruleService.deleteRuleById(ruleId);
- actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.DELETED);
+ actorService.onEntityStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.DELETED);
logEntityAction(ruleId, rule,
null,
diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
new file mode 100644
index 0000000..80ddc2b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
@@ -0,0 +1,586 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+import org.thingsboard.server.common.data.kv.DoubleDataEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.extensions.api.exception.InvalidParametersException;
+import org.thingsboard.server.extensions.api.exception.UncheckedApiException;
+import org.thingsboard.server.extensions.api.plugins.PluginConstants;
+import org.thingsboard.server.extensions.core.plugin.telemetry.AttributeData;
+import org.thingsboard.server.extensions.core.plugin.telemetry.TsData;
+import org.thingsboard.server.service.security.AccessValidator;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/**
+ * Created by ashvayka on 22.03.18.
+ */
+@RestController
+@RequestMapping(PluginConstants.TELEMETRY_URL_PREFIX)
+@Slf4j
+public class TelemetryController extends BaseController {
+
+ @Autowired
+ private AttributesService attributesService;
+
+ @Autowired
+ private TimeseriesService tsService;
+
+ @Autowired
+ private TelemetrySubscriptionService tsSubService;
+
+ @Autowired
+ private AccessValidator accessValidator;
+
+ private ExecutorService executor;
+
+ @PostConstruct
+ public void initExecutor() {
+ executor = Executors.newSingleThreadExecutor();
+ }
+
+ @PreDestroy
+ public void shutdownExecutor() {
+ if (executor != null) {
+ executor.shutdownNow();
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/keys/attributes", method = RequestMethod.GET)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> getAttributeKeys(
+ @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr) throws ThingsboardException {
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, this::getAttributeKeysCallback);
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/keys/attributes/{scope}", method = RequestMethod.GET)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> getAttributeKeysByScope(
+ @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr
+ , @PathVariable("scope") String scope) throws ThingsboardException {
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+ (result, entityId) -> getAttributeKeysCallback(result, entityId, scope));
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/values/attributes", method = RequestMethod.GET)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> getAttributes(
+ @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
+ SecurityUser user = getCurrentUser();
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+ (result, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr));
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/values/attributes/{scope}", method = RequestMethod.GET)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> getAttributesByScope(
+ @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @PathVariable("scope") String scope,
+ @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
+ SecurityUser user = getCurrentUser();
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+ (result, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr));
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/keys/timeseries", method = RequestMethod.GET)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> getTimeseriesKeys(
+ @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr) throws ThingsboardException {
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+ (result, entityId) -> {
+ Futures.addCallback(tsService.findAllLatest(entityId), getTsKeysToResponseCallback(result));
+ });
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> getLatestTimeseries(
+ @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
+ SecurityUser user = getCurrentUser();
+
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+ (result, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr));
+ }
+
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET, params = {"keys", "startTs", "endTs"})
+ @ResponseBody
+ public DeferredResult<ResponseEntity> getTimeseries(
+ @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @RequestParam(name = "keys") String keys,
+ @RequestParam(name = "startTs") Long startTs,
+ @RequestParam(name = "endTs") Long endTs,
+ @RequestParam(name = "interval", defaultValue = "0") Long interval,
+ @RequestParam(name = "limit", defaultValue = "100") Integer limit,
+ @RequestParam(name = "agg", defaultValue = "NONE") String aggStr
+ ) throws ThingsboardException {
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+ (result, entityId) -> {
+ // If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted
+ Aggregation agg = interval == 0L ? Aggregation.valueOf(Aggregation.NONE.name()) : Aggregation.valueOf(aggStr);
+ List<TsKvQuery> queries = toKeysList(keys).stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, interval, limit, agg))
+ .collect(Collectors.toList());
+
+ Futures.addCallback(tsService.findAll(entityId, queries), getTsKvListCallback(result));
+ });
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.POST)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> saveDeviceAttributes(@PathVariable("deviceId") String deviceIdStr, @PathVariable("scope") String scope,
+ @RequestBody JsonNode request) throws ThingsboardException {
+ EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
+ return saveAttributes(entityId, scope, request);
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.POST)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> saveEntityAttributesV1(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @PathVariable("scope") String scope,
+ @RequestBody JsonNode request) throws ThingsboardException {
+ EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+ return saveAttributes(entityId, scope, request);
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/attributes/{scope}", method = RequestMethod.POST)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> saveEntityAttributesV2(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @PathVariable("scope") String scope,
+ @RequestBody JsonNode request) throws ThingsboardException {
+ EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+ return saveAttributes(entityId, scope, request);
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}", method = RequestMethod.POST)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> saveEntityTelemetry(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @PathVariable("scope") String scope,
+ @RequestBody String requestBody) throws ThingsboardException {
+ EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+ return saveTelemetry(entityId, requestBody, 0L);
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}/{ttl}", method = RequestMethod.POST)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> saveEntityTelemetryWithTTL(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @PathVariable("scope") String scope, @PathVariable("ttl") Long ttl,
+ @RequestBody String requestBody) throws ThingsboardException {
+ EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+ return saveTelemetry(entityId, requestBody, ttl);
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> deleteEntityAttributes(@PathVariable("deviceId") String deviceIdStr,
+ @PathVariable("scope") String scope,
+ @RequestParam(name = "keys") String keysStr) throws ThingsboardException {
+ EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
+ return deleteAttributes(entityId, scope, keysStr);
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE)
+ @ResponseBody
+ public DeferredResult<ResponseEntity> deleteEntityAttributes(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @PathVariable("scope") String scope,
+ @RequestParam(name = "keys") String keysStr) throws ThingsboardException {
+ EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+ return deleteAttributes(entityId, scope, keysStr);
+ }
+
+ private DeferredResult<ResponseEntity> deleteAttributes(EntityId entityIdStr, String scope, String keysStr) throws ThingsboardException {
+ List<String> keys = toKeysList(keysStr);
+ if (keys.isEmpty()) {
+ return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST);
+ }
+ SecurityUser user = getCurrentUser();
+ if (DataConstants.SERVER_SCOPE.equals(scope) ||
+ DataConstants.SHARED_SCOPE.equals(scope) ||
+ DataConstants.CLIENT_SCOPE.equals(scope)) {
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdStr, (result, entityId) -> {
+ ListenableFuture<List<Void>> future = attributesService.removeAll(entityId, scope, keys);
+ Futures.addCallback(future, new FutureCallback<List<Void>>() {
+ @Override
+ public void onSuccess(@Nullable List<Void> tmp) {
+ logAttributesDeleted(user, entityId, scope, keys, null);
+ result.setResult(new ResponseEntity<>(HttpStatus.OK));
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ logAttributesDeleted(user, entityId, scope, keys, t);
+ result.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+ }, executor);
+ });
+ } else {
+ return getImmediateDeferredResult("Invalid attribute scope: " + scope, HttpStatus.BAD_REQUEST);
+ }
+ }
+
+ private DeferredResult<ResponseEntity> saveAttributes(EntityId entityIdSrc, String scope, JsonNode json) throws ThingsboardException {
+ if (!DataConstants.SERVER_SCOPE.equals(scope) && !DataConstants.SHARED_SCOPE.equals(scope)) {
+ return getImmediateDeferredResult("Invalid scope: " + scope, HttpStatus.BAD_REQUEST);
+ }
+ if (json.isObject()) {
+ List<AttributeKvEntry> attributes = extractRequestAttributes(json);
+ if (attributes.isEmpty()) {
+ return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST);
+ }
+ SecurityUser user = getCurrentUser();
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdSrc, (result, entityId) -> {
+ tsSubService.saveAndNotify(entityId, scope, attributes, new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(@Nullable Void tmp) {
+ logAttributesUpdated(user, entityId, scope, attributes, null);
+ result.setResult(new ResponseEntity(HttpStatus.OK));
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ logAttributesUpdated(user, entityId, scope, attributes, t);
+ AccessValidator.handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ });
+ });
+ } else {
+ return getImmediateDeferredResult("Request is not a JSON object", HttpStatus.BAD_REQUEST);
+ }
+ }
+
+ private DeferredResult<ResponseEntity> saveTelemetry(EntityId entityIdSrc, String requestBody, long ttl) throws ThingsboardException {
+ TelemetryUploadRequest telemetryRequest;
+ JsonElement telemetryJson;
+ try {
+ telemetryJson = new JsonParser().parse(requestBody);
+ } catch (Exception e) {
+ return getImmediateDeferredResult("Unable to parse timeseries payload: Invalid JSON body!", HttpStatus.BAD_REQUEST);
+ }
+ try {
+ telemetryRequest = JsonConverter.convertToTelemetry(telemetryJson);
+ } catch (Exception e) {
+ return getImmediateDeferredResult("Unable to parse timeseries payload. Invalid JSON body: " + e.getMessage(), HttpStatus.BAD_REQUEST);
+ }
+ List<TsKvEntry> entries = new ArrayList<>();
+ for (Map.Entry<Long, List<KvEntry>> entry : telemetryRequest.getData().entrySet()) {
+ for (KvEntry kv : entry.getValue()) {
+ entries.add(new BasicTsKvEntry(entry.getKey(), kv));
+ }
+ }
+ if (entries.isEmpty()) {
+ return getImmediateDeferredResult("No timeseries data found in request body!", HttpStatus.BAD_REQUEST);
+ }
+ SecurityUser user = getCurrentUser();
+ return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdSrc, (result, entityId) -> {
+ tsSubService.saveAndNotify(entityId, entries, ttl, new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(@Nullable Void tmp) {
+ result.setResult(new ResponseEntity(HttpStatus.OK));
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ AccessValidator.handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ });
+ });
+ }
+
+ private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, String keys) {
+ ListenableFuture<List<TsKvEntry>> future;
+ if (StringUtils.isEmpty(keys)) {
+ future = tsService.findAllLatest(entityId);
+ } else {
+ future = tsService.findLatest(entityId, toKeysList(keys));
+ }
+ Futures.addCallback(future, getTsKvListCallback(result));
+ }
+
+ private void getAttributeValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, String scope, String keys) {
+ List<String> keyList = toKeysList(keys);
+ FutureCallback<List<AttributeKvEntry>> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keyList);
+ if (!StringUtils.isEmpty(scope)) {
+ if (keyList != null && !keyList.isEmpty()) {
+ Futures.addCallback(attributesService.find(entityId, scope, keyList), callback);
+ } else {
+ Futures.addCallback(attributesService.findAll(entityId, scope), callback);
+ }
+ } else {
+ List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
+ for (String tmpScope : DataConstants.allScopes()) {
+ if (keyList != null && !keyList.isEmpty()) {
+ futures.add(attributesService.find(entityId, tmpScope, keyList));
+ } else {
+ futures.add(attributesService.findAll(entityId, tmpScope));
+ }
+ }
+
+ ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures);
+
+ Futures.addCallback(future, callback);
+ }
+ }
+
+ private void getAttributeKeysCallback(@Nullable DeferredResult<ResponseEntity> result, EntityId entityId, String scope) {
+ Futures.addCallback(attributesService.findAll(entityId, scope), getAttributeKeysToResponseCallback(result));
+ }
+
+ private void getAttributeKeysCallback(@Nullable DeferredResult<ResponseEntity> result, EntityId entityId) {
+ List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
+ for (String scope : DataConstants.allScopes()) {
+ futures.add(attributesService.findAll(entityId, scope));
+ }
+
+ ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures);
+
+ Futures.addCallback(future, getAttributeKeysToResponseCallback(result));
+ }
+
+ private FutureCallback<List<TsKvEntry>> getTsKeysToResponseCallback(final DeferredResult<ResponseEntity> response) {
+ return new FutureCallback<List<TsKvEntry>>() {
+ @Override
+ public void onSuccess(List<TsKvEntry> values) {
+ List<String> keys = values.stream().map(KvEntry::getKey).collect(Collectors.toList());
+ response.setResult(new ResponseEntity<>(keys, HttpStatus.OK));
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ log.error("Failed to fetch attributes", e);
+ AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ };
+ }
+
+ private FutureCallback<List<AttributeKvEntry>> getAttributeKeysToResponseCallback(final DeferredResult<ResponseEntity> response) {
+ return new FutureCallback<List<AttributeKvEntry>>() {
+
+ @Override
+ public void onSuccess(List<AttributeKvEntry> attributes) {
+ List<String> keys = attributes.stream().map(KvEntry::getKey).collect(Collectors.toList());
+ response.setResult(new ResponseEntity<>(keys, HttpStatus.OK));
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ log.error("Failed to fetch attributes", e);
+ AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ };
+ }
+
+ private FutureCallback<List<AttributeKvEntry>> getAttributeValuesToResponseCallback(final DeferredResult<ResponseEntity> response,
+ final SecurityUser user, final String scope,
+ final EntityId entityId, final List<String> keyList) {
+ return new FutureCallback<List<AttributeKvEntry>>() {
+ @Override
+ public void onSuccess(List<AttributeKvEntry> attributes) {
+ List<AttributeData> values = attributes.stream().map(attribute -> new AttributeData(attribute.getLastUpdateTs(),
+ attribute.getKey(), attribute.getValue())).collect(Collectors.toList());
+ logAttributesRead(user, entityId, scope, keyList, null);
+ response.setResult(new ResponseEntity<>(values, HttpStatus.OK));
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ log.error("Failed to fetch attributes", e);
+ logAttributesRead(user, entityId, scope, keyList, e);
+ AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ };
+ }
+
+ private FutureCallback<List<TsKvEntry>> getTsKvListCallback(final DeferredResult<ResponseEntity> response) {
+ return new FutureCallback<List<TsKvEntry>>() {
+ @Override
+ public void onSuccess(List<TsKvEntry> data) {
+ Map<String, List<TsData>> result = new LinkedHashMap<>();
+ for (TsKvEntry entry : data) {
+ result.computeIfAbsent(entry.getKey(), k -> new ArrayList<>())
+ .add(new TsData(entry.getTs(), entry.getValueAsString()));
+ }
+ response.setResult(new ResponseEntity<>(result, HttpStatus.OK));
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ log.error("Failed to fetch historical data", e);
+ AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ };
+ }
+
+ private void logAttributesDeleted(SecurityUser user, EntityId entityId, String scope, List<String> keys, Throwable e) {
+ auditLogService.logEntityAction(
+ user.getTenantId(),
+ user.getCustomerId(),
+ user.getId(),
+ user.getName(),
+ (UUIDBased & EntityId) entityId,
+ null,
+ ActionType.ATTRIBUTES_DELETED,
+ toException(e),
+ scope,
+ keys);
+ }
+
+ private void logAttributesUpdated(SecurityUser user, EntityId entityId, String scope, List<AttributeKvEntry> attributes, Throwable e) {
+ auditLogService.logEntityAction(
+ user.getTenantId(),
+ user.getCustomerId(),
+ user.getId(),
+ user.getName(),
+ (UUIDBased & EntityId) entityId,
+ null,
+ ActionType.ATTRIBUTES_UPDATED,
+ toException(e),
+ scope,
+ attributes);
+ }
+
+
+ private void logAttributesRead(SecurityUser user, EntityId entityId, String scope, List<String> keys, Throwable e) {
+ auditLogService.logEntityAction(
+ user.getTenantId(),
+ user.getCustomerId(),
+ user.getId(),
+ user.getName(),
+ (UUIDBased & EntityId) entityId,
+ null,
+ ActionType.ATTRIBUTES_READ,
+ toException(e),
+ scope,
+ keys);
+ }
+
+ private ListenableFuture<List<AttributeKvEntry>> mergeAllAttributesFutures(List<ListenableFuture<List<AttributeKvEntry>>> futures) {
+ return Futures.transform(Futures.successfulAsList(futures),
+ (Function<? super List<List<AttributeKvEntry>>, ? extends List<AttributeKvEntry>>) input -> {
+ List<AttributeKvEntry> tmp = new ArrayList<>();
+ if (input != null) {
+ input.forEach(tmp::addAll);
+ }
+ return tmp;
+ }, executor);
+ }
+
+ private List<String> toKeysList(String keys) {
+ List<String> keyList = null;
+ if (!StringUtils.isEmpty(keys)) {
+ keyList = Arrays.asList(keys.split(","));
+ }
+ return keyList;
+ }
+
+ private DeferredResult<ResponseEntity> getImmediateDeferredResult(String message, HttpStatus status) {
+ DeferredResult<ResponseEntity> result = new DeferredResult<>();
+ result.setResult(new ResponseEntity<>(message, status));
+ return result;
+ }
+
+ private List<AttributeKvEntry> extractRequestAttributes(JsonNode jsonNode) {
+ long ts = System.currentTimeMillis();
+ List<AttributeKvEntry> attributes = new ArrayList<>();
+ jsonNode.fields().forEachRemaining(entry -> {
+ String key = entry.getKey();
+ JsonNode value = entry.getValue();
+ if (entry.getValue().isTextual()) {
+ attributes.add(new BaseAttributeKvEntry(new StringDataEntry(key, value.textValue()), ts));
+ } else if (entry.getValue().isBoolean()) {
+ attributes.add(new BaseAttributeKvEntry(new BooleanDataEntry(key, value.booleanValue()), ts));
+ } else if (entry.getValue().isDouble()) {
+ attributes.add(new BaseAttributeKvEntry(new DoubleDataEntry(key, value.doubleValue()), ts));
+ } else if (entry.getValue().isNumber()) {
+ if (entry.getValue().isBigInteger()) {
+ throw new UncheckedApiException(new InvalidParametersException("Big integer values are not supported!"));
+ } else {
+ attributes.add(new BaseAttributeKvEntry(new LongDataEntry(key, value.longValue()), ts));
+ }
+ }
+ });
+ return attributes;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java
index 5acb4eb..bf49074 100644
--- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java
@@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.tenant.TenantService;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
@RestController
@RequestMapping("/api")
diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java
index 2a1531a..52b207d 100644
--- a/application/src/main/java/org/thingsboard/server/controller/UserController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java
@@ -29,9 +29,9 @@ import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.service.mail.MailService;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.service.security.model.SecurityUser;
import javax.servlet.http.HttpServletRequest;
diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
index 757f765..bf89f13 100644
--- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
@@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import java.util.List;
diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
index 44c7d94..43ece89 100644
--- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
@@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import java.util.List;
diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java
index 3b897d6..031073f 100644
--- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java
+++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.exception;
import org.springframework.http.HttpStatus;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import java.util.Date;
diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
index c70c561..63fe17a 100644
--- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
+++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
@@ -17,8 +17,6 @@ package org.thingsboard.server.exception;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -27,6 +25,8 @@ import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
index 0a6081d..9377756 100644
--- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
@@ -16,6 +16,8 @@
package org.thingsboard.server.service.component;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import lombok.extern.slf4j.Slf4j;
@@ -26,12 +28,14 @@ import org.springframework.context.annotation.ClassPathScanningCandidateComponen
import org.springframework.core.env.Environment;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.stereotype.Service;
+import org.thingsboard.rule.engine.api.*;
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;
@@ -66,6 +70,24 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
}
}
+ private void registerRuleNodeComponents() {
+ Set<BeanDefinition> ruleNodeBeanDefinitions = getBeanDefinitions(RuleNode.class);
+ for (BeanDefinition def : ruleNodeBeanDefinitions) {
+ try {
+ String clazzName = def.getBeanClassName();
+ Class<?> clazz = Class.forName(clazzName);
+ RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
+ ComponentType type = ruleNodeAnnotation.type();
+ ComponentDescriptor component = scanAndPersistComponent(def, type);
+ components.put(component.getClazz(), component);
+ componentsMap.computeIfAbsent(type, k -> new ArrayList<>()).add(component);
+ } catch (Exception e) {
+ log.error("Can't initialize component {}, due to {}", def.getBeanClassName(), e.getMessage(), e);
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
private void registerComponents(ComponentType type, Class<? extends Annotation> annotation) {
List<ComponentDescriptor> components = persist(getBeanDefinitions(annotation), type);
componentsMap.put(type, components);
@@ -79,8 +101,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
private List<ComponentDescriptor> persist(Set<BeanDefinition> filterDefs, ComponentType type) {
List<ComponentDescriptor> result = new ArrayList<>();
for (BeanDefinition def : filterDefs) {
- ComponentDescriptor scannedComponent = scanAndPersistComponent(def, type);
- result.add(scannedComponent);
+ result.add(scanAndPersistComponent(def, type));
}
return result;
}
@@ -93,23 +114,26 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
Class<?> clazz = Class.forName(clazzName);
String descriptorResourceName;
switch (type) {
+ case ENRICHMENT:
case FILTER:
- Filter filterAnnotation = clazz.getAnnotation(Filter.class);
- scannedComponent.setName(filterAnnotation.name());
- scannedComponent.setScope(filterAnnotation.scope());
- descriptorResourceName = filterAnnotation.descriptor();
- break;
- case PROCESSOR:
- Processor processorAnnotation = clazz.getAnnotation(Processor.class);
- scannedComponent.setName(processorAnnotation.name());
- scannedComponent.setScope(processorAnnotation.scope());
- descriptorResourceName = processorAnnotation.descriptor();
- break;
+ case TRANSFORMATION:
case ACTION:
- Action actionAnnotation = clazz.getAnnotation(Action.class);
- scannedComponent.setName(actionAnnotation.name());
- scannedComponent.setScope(actionAnnotation.scope());
- descriptorResourceName = actionAnnotation.descriptor();
+ RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
+ scannedComponent.setName(ruleNodeAnnotation.name());
+ scannedComponent.setScope(ruleNodeAnnotation.scope());
+ NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation);
+ ObjectNode configurationDescriptor = mapper.createObjectNode();
+ JsonNode node = mapper.valueToTree(nodeDefinition);
+ configurationDescriptor.set("nodeDefinition", node);
+ scannedComponent.setConfigurationDescriptor(configurationDescriptor);
+ break;
+ case OLD_ACTION:
+ Action oldActionAnnotation = clazz.getAnnotation(Action.class);
+ scannedComponent.setName(oldActionAnnotation.name());
+ scannedComponent.setScope(oldActionAnnotation.scope());
+ descriptorResourceName = oldActionAnnotation.descriptor();
+ scannedComponent.setConfigurationDescriptor(mapper.readTree(
+ Resources.toString(Resources.getResource(descriptorResourceName), Charsets.UTF_8)));
break;
case PLUGIN:
Plugin pluginAnnotation = clazz.getAnnotation(Plugin.class);
@@ -122,18 +146,18 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
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) {
+ if (actionComponent.getType() != ComponentType.OLD_ACTION) {
log.error("Plugin {} action {} has wrong component type!", def.getBeanClassName(), actionClazz.getName(), actionComponent.getType());
throw new RuntimeException("Plugin " + def.getBeanClassName() + "action " + actionClazz.getName() + " has wrong component type!");
}
}
- scannedComponent.setActions(Arrays.stream(pluginAnnotation.actions()).map(action -> action.getName()).collect(Collectors.joining(",")));
+ scannedComponent.setActions(Arrays.stream(pluginAnnotation.actions()).map(Class::getName).collect(Collectors.joining(",")));
+ scannedComponent.setConfigurationDescriptor(mapper.readTree(
+ Resources.toString(Resources.getResource(descriptorResourceName), Charsets.UTF_8)));
break;
default:
throw new RuntimeException(type + " is not supported yet!");
}
- scannedComponent.setConfigurationDescriptor(mapper.readTree(
- Resources.toString(Resources.getResource(descriptorResourceName), Charsets.UTF_8)));
scannedComponent.setClazz(clazzName);
log.info("Processing scanned component: {}", scannedComponent);
} catch (Exception e) {
@@ -156,6 +180,23 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
return scannedComponent;
}
+ private NodeDefinition prepareNodeDefinition(RuleNode nodeAnnotation) throws Exception {
+ NodeDefinition nodeDefinition = new NodeDefinition();
+ nodeDefinition.setDetails(nodeAnnotation.nodeDetails());
+ nodeDefinition.setDescription(nodeAnnotation.nodeDescription());
+ nodeDefinition.setInEnabled(nodeAnnotation.inEnabled());
+ nodeDefinition.setOutEnabled(nodeAnnotation.outEnabled());
+ nodeDefinition.setRelationTypes(nodeAnnotation.relationTypes());
+ nodeDefinition.setCustomRelations(nodeAnnotation.customRelations());
+ Class<? extends NodeConfiguration> configClazz = nodeAnnotation.configClazz();
+ NodeConfiguration config = configClazz.newInstance();
+ NodeConfiguration defaultConfiguration = config.defaultConfiguration();
+ nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration));
+ nodeDefinition.setUiResources(nodeAnnotation.uiResources());
+ nodeDefinition.setConfigDirective(nodeAnnotation.configDirective());
+ return nodeDefinition;
+ }
+
private Set<BeanDefinition> getBeanDefinitions(Class<? extends Annotation> componentType) {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(componentType));
@@ -168,11 +209,10 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
@Override
public void discoverComponents() {
- registerComponents(ComponentType.FILTER, Filter.class);
- registerComponents(ComponentType.PROCESSOR, Processor.class);
+ registerRuleNodeComponents();
- registerComponents(ComponentType.ACTION, Action.class);
+ registerComponents(ComponentType.OLD_ACTION, Action.class);
registerComponents(ComponentType.PLUGIN, Plugin.class);
@@ -181,7 +221,20 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
@Override
public List<ComponentDescriptor> getComponents(ComponentType type) {
- return Collections.unmodifiableList(componentsMap.get(type));
+ if (componentsMap.containsKey(type)) {
+ return Collections.unmodifiableList(componentsMap.get(type));
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ @Override
+ public List<ComponentDescriptor> getComponents(Set<ComponentType> types) {
+ List<ComponentDescriptor> result = new ArrayList<>();
+ types.stream().filter(type -> componentsMap.containsKey(type)).forEach(type -> {
+ result.addAll(componentsMap.get(type));
+ });
+ return Collections.unmodifiableList(result);
}
@Override
@@ -199,7 +252,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
}
List<ComponentDescriptor> result = new ArrayList<>();
for (String action : plugin.getActions().split(",")) {
- getComponent(action).ifPresent(v -> result.add(v));
+ getComponent(action).ifPresent(result::add);
}
return result;
} else {
diff --git a/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
index ea27e60..7a15a1b 100644
--- a/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
@@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
/**
* @author Andrew Shvayka
@@ -30,6 +31,8 @@ public interface ComponentDiscoveryService {
List<ComponentDescriptor> getComponents(ComponentType type);
+ List<ComponentDescriptor> getComponents(Set<ComponentType> types);
+
Optional<ComponentDescriptor> getComponent(String clazz);
List<ComponentDescriptor> getPluginActions(String pluginClazz);
diff --git a/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java b/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java
new file mode 100644
index 0000000..91ef9de
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.executors;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import org.thingsboard.rule.engine.api.ListeningExecutor;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+
+/**
+ * Created by igor on 4/13/18.
+ */
+public abstract class AbstractListeningExecutor implements ListeningExecutor {
+
+ private ListeningExecutorService service;
+
+ @PostConstruct
+ public void init() {
+ this.service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(getThreadPollSize()));
+ }
+
+ @PreDestroy
+ public void destroy() {
+ if (this.service != null) {
+ this.service.shutdown();
+ }
+ }
+
+ @Override
+ public <T> ListenableFuture<T> executeAsync(Callable<T> task) {
+ return service.submit(task);
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ service.execute(command);
+ }
+
+ protected abstract int getThreadPollSize();
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java b/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java
new file mode 100644
index 0000000..2eec3ed
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.executors;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Component
+public class DbCallbackExecutorService extends AbstractListeningExecutor {
+
+ @Value("${actors.rule.db_callback_thread_pool_size}")
+ private int dbCallbackExecutorThreadPoolSize;
+
+ @Override
+ protected int getThreadPollSize() {
+ return dbCallbackExecutorThreadPoolSize;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
index 1ef805f..908faf2 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
@@ -187,7 +187,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
@Override
public void loadSystemRules() throws Exception {
- loadRules(Paths.get(dataDir, JSON_DIR, SYSTEM_DIR, RULES_DIR), null);
+// loadRules(Paths.get(dataDir, JSON_DIR, SYSTEM_DIR, RULES_DIR), null);
}
@Override
@@ -228,7 +228,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
"Raspberry Pi GPIO control sample application");
loadPlugins(Paths.get(dataDir, JSON_DIR, DEMO_DIR, PLUGINS_DIR), demoTenant.getId());
- loadRules(Paths.get(dataDir, JSON_DIR, DEMO_DIR, RULES_DIR), demoTenant.getId());
+// loadRules(Paths.get(dataDir, JSON_DIR, DEMO_DIR, RULES_DIR), demoTenant.getId());
loadDashboards(Paths.get(dataDir, JSON_DIR, DEMO_DIR, DASHBOARDS_DIR), demoTenant.getId(), null);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
index 25b911c..818c935 100644
--- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
+++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
@@ -27,13 +27,15 @@ import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.ui.velocity.VelocityEngineUtils;
+import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.settings.AdminSettingsService;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
import javax.annotation.PostConstruct;
+import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.util.HashMap;
import java.util.Locale;
@@ -49,18 +51,18 @@ public class DefaultMailService implements MailService {
public static final String UTF_8 = "UTF-8";
@Autowired
private MessageSource messages;
-
+
@Autowired
@Qualifier("velocityEngine")
private VelocityEngine engine;
-
+
private JavaMailSenderImpl mailSender;
-
+
private String mailFrom;
-
+
@Autowired
- private AdminSettingsService adminSettingsService;
-
+ private AdminSettingsService adminSettingsService;
+
@PostConstruct
private void init() {
updateMailConfiguration();
@@ -77,7 +79,7 @@ public class DefaultMailService implements MailService {
throw new IncorrectParameterException("Failed to date mail configuration. Settings not found!");
}
}
-
+
private JavaMailSenderImpl createMailSender(JsonNode jsonConfig) {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(jsonConfig.get("smtpHost").asText());
@@ -99,7 +101,7 @@ public class DefaultMailService implements MailService {
javaMailProperties.put(MAIL_PROP + protocol + ".starttls.enable", jsonConfig.has("enableTls") ? jsonConfig.get("enableTls").asText() : "false");
return javaMailProperties;
}
-
+
private int parsePort(String strPort) {
try {
return Integer.valueOf(strPort);
@@ -112,86 +114,102 @@ public class DefaultMailService implements MailService {
public void sendEmail(String email, String subject, String message) throws ThingsboardException {
sendMail(mailSender, mailFrom, email, subject, message);
}
-
+
@Override
public void sendTestMail(JsonNode jsonConfig, String email) throws ThingsboardException {
JavaMailSenderImpl testMailSender = createMailSender(jsonConfig);
String mailFrom = jsonConfig.get("mailFrom").asText();
String subject = messages.getMessage("test.message.subject", null, Locale.US);
-
+
Map<String, Object> model = new HashMap<String, Object>();
model.put(TARGET_EMAIL, email);
-
+
String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
"test.vm", UTF_8, model);
-
- sendMail(testMailSender, mailFrom, email, subject, message);
+
+ sendMail(testMailSender, mailFrom, email, subject, message);
}
@Override
public void sendActivationEmail(String activationLink, String email) throws ThingsboardException {
-
+
String subject = messages.getMessage("activation.subject", null, Locale.US);
-
+
Map<String, Object> model = new HashMap<String, Object>();
model.put("activationLink", activationLink);
model.put(TARGET_EMAIL, email);
-
+
String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
"activation.vm", UTF_8, model);
-
- sendMail(mailSender, mailFrom, email, subject, message);
+
+ sendMail(mailSender, mailFrom, email, subject, message);
}
-
+
@Override
public void sendAccountActivatedEmail(String loginLink, String email) throws ThingsboardException {
-
+
String subject = messages.getMessage("account.activated.subject", null, Locale.US);
-
+
Map<String, Object> model = new HashMap<String, Object>();
model.put("loginLink", loginLink);
model.put(TARGET_EMAIL, email);
-
+
String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
"account.activated.vm", UTF_8, model);
-
- sendMail(mailSender, mailFrom, email, subject, message);
+
+ sendMail(mailSender, mailFrom, email, subject, message);
}
@Override
public void sendResetPasswordEmail(String passwordResetLink, String email) throws ThingsboardException {
-
+
String subject = messages.getMessage("reset.password.subject", null, Locale.US);
-
+
Map<String, Object> model = new HashMap<String, Object>();
model.put("passwordResetLink", passwordResetLink);
model.put(TARGET_EMAIL, email);
-
+
String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
"reset.password.vm", UTF_8, model);
-
- sendMail(mailSender, mailFrom, email, subject, message);
+
+ sendMail(mailSender, mailFrom, email, subject, message);
}
-
+
@Override
public void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException {
-
+
String subject = messages.getMessage("password.was.reset.subject", null, Locale.US);
-
+
Map<String, Object> model = new HashMap<String, Object>();
model.put("loginLink", loginLink);
model.put(TARGET_EMAIL, email);
-
+
String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
"password.was.reset.vm", UTF_8, model);
-
- sendMail(mailSender, mailFrom, email, subject, message);
+
+ sendMail(mailSender, mailFrom, email, subject, message);
}
+ @Override
+ public void send(String from, String to, String cc, String bcc, String subject, String body) throws MessagingException {
+ MimeMessage mailMsg = mailSender.createMimeMessage();
+ MimeMessageHelper helper = new MimeMessageHelper(mailMsg, "UTF-8");
+ helper.setFrom(StringUtils.isBlank(from) ? mailFrom : from);
+ helper.setTo(to.split("\\s*,\\s*"));
+ if (!StringUtils.isBlank(cc)) {
+ helper.setCc(cc.split("\\s*,\\s*"));
+ }
+ if (!StringUtils.isBlank(bcc)) {
+ helper.setBcc(bcc.split("\\s*,\\s*"));
+ }
+ helper.setSubject(subject);
+ helper.setText(body);
+ mailSender.send(helper.getMimeMessage());
+ }
- private void sendMail(JavaMailSenderImpl mailSender,
- String mailFrom, String email,
- String subject, String message) throws ThingsboardException {
+ private void sendMail(JavaMailSenderImpl mailSender,
+ String mailFrom, String email,
+ String subject, String message) throws ThingsboardException {
try {
MimeMessage mimeMsg = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMsg, UTF_8);
@@ -208,7 +226,7 @@ public class DefaultMailService implements MailService {
protected ThingsboardException handleException(Exception exception) {
String message;
if (exception instanceof NestedRuntimeException) {
- message = ((NestedRuntimeException)exception).getMostSpecificCause().getMessage();
+ message = ((NestedRuntimeException) exception).getMostSpecificCause().getMessage();
} else {
message = exception.getMessage();
}
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java b/application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java
new file mode 100644
index 0000000..e8caab6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.mail;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.service.executors.AbstractListeningExecutor;
+
+@Component
+public class MailExecutorService extends AbstractListeningExecutor {
+
+ @Value("${actors.rule.mail_thread_pool_size}")
+ private int mailExecutorThreadPoolSize;
+
+ @Override
+ protected int getThreadPollSize() {
+ return mailExecutorThreadPoolSize;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java b/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java
new file mode 100644
index 0000000..c60f455
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.script;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.service.executors.AbstractListeningExecutor;
+
+@Component
+public class JsExecutorService extends AbstractListeningExecutor {
+
+ @Value("${actors.rule.js_thread_pool_size}")
+ private int jsExecutorThreadPoolSize;
+
+ @Override
+ protected int getThreadPollSize() {
+ return jsExecutorThreadPoolSize;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/script/NashornJsEngine.java b/application/src/main/java/org/thingsboard/server/service/script/NashornJsEngine.java
new file mode 100644
index 0000000..d68f6fe
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/script/NashornJsEngine.java
@@ -0,0 +1,211 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.script;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Sets;
+import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.Invocable;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+
+@Slf4j
+public class NashornJsEngine implements org.thingsboard.rule.engine.api.ScriptEngine {
+
+ public static final String MSG = "msg";
+ public static final String METADATA = "metadata";
+ public static final String MSG_TYPE = "msgType";
+
+ private static final String JS_WRAPPER_PREFIX_TEMPLATE = "function %s(msgStr, metadataStr, msgType) { " +
+ " var msg = JSON.parse(msgStr); " +
+ " var metadata = JSON.parse(metadataStr); " +
+ " return JSON.stringify(%s(msg, metadata, msgType));" +
+ " function %s(%s, %s, %s) {";
+ private static final String JS_WRAPPER_SUFFIX = "}" +
+ "\n}";
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+ private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
+ private ScriptEngine engine = factory.getScriptEngine(new String[]{"--no-java"});
+
+ private final String invokeFunctionName;
+
+ public NashornJsEngine(String script, String functionName, String... argNames) {
+ this.invokeFunctionName = "invokeInternal" + this.hashCode();
+ String msgArg;
+ String metadataArg;
+ String msgTypeArg;
+ if (argNames != null && argNames.length == 3) {
+ msgArg = argNames[0];
+ metadataArg = argNames[1];
+ msgTypeArg = argNames[2];
+ } else {
+ msgArg = MSG;
+ metadataArg = METADATA;
+ msgTypeArg = MSG_TYPE;
+ }
+ String jsWrapperPrefix = String.format(JS_WRAPPER_PREFIX_TEMPLATE, this.invokeFunctionName,
+ functionName, functionName, msgArg, metadataArg, msgTypeArg);
+ compileScript(jsWrapperPrefix + script + JS_WRAPPER_SUFFIX);
+ }
+
+ private void compileScript(String script) {
+ try {
+ engine.eval(script);
+ } catch (ScriptException e) {
+ log.warn("Failed to compile JS script: {}", e.getMessage(), e);
+ throw new IllegalArgumentException("Can't compile script: " + e.getMessage());
+ }
+ }
+
+ private static String[] prepareArgs(TbMsg msg) {
+ try {
+ String[] args = new String[3];
+ if (msg.getData() != null) {
+ args[0] = msg.getData();
+ } else {
+ args[0] = "";
+ }
+ args[1] = mapper.writeValueAsString(msg.getMetaData().getData());
+ args[2] = msg.getType();
+ return args;
+ } catch (Throwable th) {
+ throw new IllegalArgumentException("Cannot bind js args", th);
+ }
+ }
+
+ private static TbMsg unbindMsg(JsonNode msgData, TbMsg msg) {
+ try {
+ String data = null;
+ Map<String, String> metadata = null;
+ String messageType = null;
+ if (msgData.has(MSG)) {
+ JsonNode msgPayload = msgData.get(MSG);
+ data = mapper.writeValueAsString(msgPayload);
+ }
+ if (msgData.has(METADATA)) {
+ JsonNode msgMetadata = msgData.get(METADATA);
+ metadata = mapper.convertValue(msgMetadata, new TypeReference<Map<String, String>>() {
+ });
+ }
+ if (msgData.has(MSG_TYPE)) {
+ messageType = msgData.get(MSG_TYPE).asText();
+ }
+ String newData = data != null ? data : msg.getData();
+ TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData();
+ String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType();
+ return new TbMsg(msg.getId(), newMessageType, msg.getOriginator(), newMetadata, newData);
+ } catch (Throwable th) {
+ th.printStackTrace();
+ throw new RuntimeException("Failed to unbind message data from javascript result", th);
+ }
+ }
+
+ @Override
+ public TbMsg executeUpdate(TbMsg msg) throws ScriptException {
+ JsonNode result = executeScript(msg);
+ if (!result.isObject()) {
+ log.warn("Wrong result type: {}", result.getNodeType());
+ throw new ScriptException("Wrong result type: " + result.getNodeType());
+ }
+ return unbindMsg(result, msg);
+ }
+
+ @Override
+ public TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException {
+ JsonNode result = executeScript(prevMsg);
+ if (!result.isObject()) {
+ log.warn("Wrong result type: {}", result.getNodeType());
+ throw new ScriptException("Wrong result type: " + result.getNodeType());
+ }
+ return unbindMsg(result, prevMsg);
+ }
+
+ @Override
+ public JsonNode executeJson(TbMsg msg) throws ScriptException {
+ return executeScript(msg);
+ }
+
+ @Override
+ public String executeToString(TbMsg msg) throws ScriptException {
+ JsonNode result = executeScript(msg);
+ if (!result.isTextual()) {
+ log.warn("Wrong result type: {}", result.getNodeType());
+ throw new ScriptException("Wrong result type: " + result.getNodeType());
+ }
+ return result.asText();
+ }
+
+ @Override
+ public boolean executeFilter(TbMsg msg) throws ScriptException {
+ JsonNode result = executeScript(msg);
+ if (!result.isBoolean()) {
+ log.warn("Wrong result type: {}", result.getNodeType());
+ throw new ScriptException("Wrong result type: " + result.getNodeType());
+ }
+ return result.asBoolean();
+ }
+
+ @Override
+ public Set<String> executeSwitch(TbMsg msg) throws ScriptException {
+ JsonNode result = executeScript(msg);
+ if (result.isTextual()) {
+ return Collections.singleton(result.asText());
+ } else if (result.isArray()) {
+ Set<String> nextStates = Sets.newHashSet();
+ for (JsonNode val : result) {
+ if (!val.isTextual()) {
+ log.warn("Wrong result type: {}", val.getNodeType());
+ throw new ScriptException("Wrong result type: " + val.getNodeType());
+ } else {
+ nextStates.add(val.asText());
+ }
+ }
+ return nextStates;
+ } else {
+ log.warn("Wrong result type: {}", result.getNodeType());
+ throw new ScriptException("Wrong result type: " + result.getNodeType());
+ }
+ }
+
+ private JsonNode executeScript(TbMsg msg) throws ScriptException {
+ try {
+ String[] inArgs = prepareArgs(msg);
+ String eval = ((Invocable)engine).invokeFunction(this.invokeFunctionName, inArgs[0], inArgs[1], inArgs[2]).toString();
+ return mapper.readTree(eval);
+ } catch (ScriptException | IllegalArgumentException th) {
+ throw th;
+ } catch (Throwable th) {
+ th.printStackTrace();
+ throw new RuntimeException("Failed to execute js script", th);
+ }
+ }
+
+ public void destroy() {
+ engine = null;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
new file mode 100644
index 0000000..c1f3688
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
@@ -0,0 +1,295 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security;
+
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.actors.plugin.ValidationResult;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.controller.HttpValidationCallback;
+import org.thingsboard.server.dao.alarm.AlarmService;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.dao.user.UserService;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.extensions.api.exception.ToErrorResponseEntity;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.BiConsumer;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+@Component
+public class AccessValidator {
+
+ public static final String CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "Customer user is not allowed to perform this operation!";
+ public static final String SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "System administrator is not allowed to perform this operation!";
+ public static final String DEVICE_WITH_REQUESTED_ID_NOT_FOUND = "Device with requested id wasn't found!";
+
+ @Autowired
+ protected TenantService tenantService;
+
+ @Autowired
+ protected CustomerService customerService;
+
+ @Autowired
+ protected UserService userService;
+
+ @Autowired
+ protected DeviceService deviceService;
+
+ @Autowired
+ protected AssetService assetService;
+
+ @Autowired
+ protected AlarmService alarmService;
+
+ @Autowired
+ protected RuleChainService ruleChainService;
+
+ private ExecutorService executor;
+
+ @PostConstruct
+ public void initExecutor() {
+ executor = Executors.newSingleThreadExecutor();
+ }
+
+ @PreDestroy
+ public void shutdownExecutor() {
+ if (executor != null) {
+ executor.shutdownNow();
+ }
+ }
+
+ public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, String entityType, String entityIdStr,
+ BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess) throws ThingsboardException {
+ return validateEntityAndCallback(currentUser, entityType, entityIdStr, onSuccess, (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+
+ public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, String entityType, String entityIdStr,
+ BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess,
+ BiConsumer<DeferredResult<ResponseEntity>, Throwable> onFailure) throws ThingsboardException {
+ return validateEntityAndCallback(currentUser, EntityIdFactory.getByTypeAndId(entityType, entityIdStr),
+ onSuccess, onFailure);
+ }
+
+ public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, EntityId entityId,
+ BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess) throws ThingsboardException {
+ return validateEntityAndCallback(currentUser, entityId, onSuccess, (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+
+ public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, EntityId entityId,
+ BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess,
+ BiConsumer<DeferredResult<ResponseEntity>, Throwable> onFailure) throws ThingsboardException {
+
+ final DeferredResult<ResponseEntity> response = new DeferredResult<>();
+
+ validate(currentUser, entityId, new HttpValidationCallback(response,
+ new FutureCallback<DeferredResult<ResponseEntity>>() {
+ @Override
+ public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) {
+ onSuccess.accept(response, entityId);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ onFailure.accept(response, t);
+ }
+ }));
+
+ return response;
+ }
+
+ public <T> void validate(SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+ switch (entityId.getEntityType()) {
+ case DEVICE:
+ validateDevice(currentUser, entityId, callback);
+ return;
+ case ASSET:
+ validateAsset(currentUser, entityId, callback);
+ return;
+ case RULE_CHAIN:
+ validateRuleChain(currentUser, entityId, callback);
+ return;
+ case CUSTOMER:
+ validateCustomer(currentUser, entityId, callback);
+ return;
+ case TENANT:
+ validateTenant(currentUser, entityId, callback);
+ return;
+ default:
+ //TODO: add support of other entities
+ throw new IllegalStateException("Not Implemented!");
+ }
+ }
+
+ private void validateDevice(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+ if (currentUser.isSystemAdmin()) {
+ callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+ } else {
+ ListenableFuture<Device> deviceFuture = deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId()));
+ Futures.addCallback(deviceFuture, getCallback(callback, device -> {
+ if (device == null) {
+ return ValidationResult.entityNotFound(DEVICE_WITH_REQUESTED_ID_NOT_FOUND);
+ } else {
+ if (!device.getTenantId().equals(currentUser.getTenantId())) {
+ return ValidationResult.accessDenied("Device doesn't belong to the current Tenant!");
+ } else if (currentUser.isCustomerUser() && !device.getCustomerId().equals(currentUser.getCustomerId())) {
+ return ValidationResult.accessDenied("Device doesn't belong to the current Customer!");
+ } else {
+ return ValidationResult.ok();
+ }
+ }
+ }), executor);
+ }
+ }
+
+ private <T> void validateAsset(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+ if (currentUser.isSystemAdmin()) {
+ callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+ } else {
+ ListenableFuture<Asset> assetFuture = assetService.findAssetByIdAsync(new AssetId(entityId.getId()));
+ Futures.addCallback(assetFuture, getCallback(callback, asset -> {
+ if (asset == null) {
+ return ValidationResult.entityNotFound("Asset with requested id wasn't found!");
+ } else {
+ if (!asset.getTenantId().equals(currentUser.getTenantId())) {
+ return ValidationResult.accessDenied("Asset doesn't belong to the current Tenant!");
+ } else if (currentUser.isCustomerUser() && !asset.getCustomerId().equals(currentUser.getCustomerId())) {
+ return ValidationResult.accessDenied("Asset doesn't belong to the current Customer!");
+ } else {
+ return ValidationResult.ok();
+ }
+ }
+ }), executor);
+ }
+ }
+
+
+ private <T> void validateRuleChain(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+ if (currentUser.isCustomerUser()) {
+ callback.onSuccess(ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+ } else {
+ ListenableFuture<RuleChain> ruleChainFuture = ruleChainService.findRuleChainByIdAsync(new RuleChainId(entityId.getId()));
+ Futures.addCallback(ruleChainFuture, getCallback(callback, ruleChain -> {
+ if (ruleChain == null) {
+ return ValidationResult.entityNotFound("Rule chain with requested id wasn't found!");
+ } else {
+ if (currentUser.isTenantAdmin() && !ruleChain.getTenantId().equals(currentUser.getTenantId())) {
+ return ValidationResult.accessDenied("Rule chain doesn't belong to the current Tenant!");
+ } else if (currentUser.isSystemAdmin() && !ruleChain.getTenantId().isNullUid()) {
+ return ValidationResult.accessDenied("Rule chain is not in system scope!");
+ } else {
+ return ValidationResult.ok();
+ }
+ }
+ }), executor);
+ }
+ }
+
+ private <T> void validateCustomer(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+ if (currentUser.isSystemAdmin()) {
+ callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+ } else {
+ ListenableFuture<Customer> customerFuture = customerService.findCustomerByIdAsync(new CustomerId(entityId.getId()));
+ Futures.addCallback(customerFuture, getCallback(callback, customer -> {
+ if (customer == null) {
+ return ValidationResult.entityNotFound("Customer with requested id wasn't found!");
+ } else {
+ if (!customer.getTenantId().equals(currentUser.getTenantId())) {
+ return ValidationResult.accessDenied("Customer doesn't belong to the current Tenant!");
+ } else if (currentUser.isCustomerUser() && !customer.getId().equals(currentUser.getCustomerId())) {
+ return ValidationResult.accessDenied("Customer doesn't relate to the currently authorized customer user!");
+ } else {
+ return ValidationResult.ok();
+ }
+ }
+ }), executor);
+ }
+ }
+
+ private <T> void validateTenant(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+ if (currentUser.isCustomerUser()) {
+ callback.onSuccess(ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+ } else if (currentUser.isSystemAdmin()) {
+ callback.onSuccess(ValidationResult.ok());
+ } else {
+ ListenableFuture<Tenant> tenantFuture = tenantService.findTenantByIdAsync(new TenantId(entityId.getId()));
+ Futures.addCallback(tenantFuture, getCallback(callback, tenant -> {
+ if (tenant == null) {
+ return ValidationResult.entityNotFound("Tenant with requested id wasn't found!");
+ } else if (!tenant.getId().equals(currentUser.getTenantId())) {
+ return ValidationResult.accessDenied("Tenant doesn't relate to the currently authorized user!");
+ } else {
+ return ValidationResult.ok();
+ }
+ }), executor);
+ }
+ }
+
+ private <T> FutureCallback<T> getCallback(FutureCallback<ValidationResult> callback, Function<T, ValidationResult> transformer) {
+ return new FutureCallback<T>() {
+ @Override
+ public void onSuccess(@Nullable T result) {
+ callback.onSuccess(transformer.apply(result));
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ callback.onFailure(t);
+ }
+ };
+ }
+
+ public static void handleError(Throwable e, final DeferredResult<ResponseEntity> response, HttpStatus defaultErrorStatus) {
+ ResponseEntity responseEntity;
+ if (e != null && e instanceof ToErrorResponseEntity) {
+ responseEntity = ((ToErrorResponseEntity) e).toErrorResponseEntity();
+ } else if (e != null && e instanceof IllegalArgumentException) {
+ responseEntity = new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
+ } else {
+ responseEntity = new ResponseEntity<>(defaultErrorStatus);
+ }
+ response.setResult(responseEntity);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
index f4a28a0..91ee7bf 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.service.security.model;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UserId;
import java.util.Collection;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/ValidationCallback.java b/application/src/main/java/org/thingsboard/server/service/security/ValidationCallback.java
new file mode 100644
index 0000000..2b91c60
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/ValidationCallback.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security;
+
+import com.google.common.util.concurrent.FutureCallback;
+import org.thingsboard.server.actors.plugin.ValidationResult;
+import org.thingsboard.server.actors.plugin.ValidationResultCode;
+import org.thingsboard.server.extensions.api.exception.AccessDeniedException;
+import org.thingsboard.server.extensions.api.exception.EntityNotFoundException;
+import org.thingsboard.server.extensions.api.exception.InternalErrorException;
+import org.thingsboard.server.extensions.api.exception.UnauthorizedException;
+
+/**
+ * Created by ashvayka on 31.03.18.
+ */
+public class ValidationCallback<T> implements FutureCallback<ValidationResult> {
+
+ private final T response;
+ private final FutureCallback<T> action;
+
+ public ValidationCallback(T response, FutureCallback<T> action) {
+ this.response = response;
+ this.action = action;
+ }
+
+ @Override
+ public void onSuccess(ValidationResult result) {
+ ValidationResultCode resultCode = result.getResultCode();
+ if (resultCode == ValidationResultCode.OK) {
+ action.onSuccess(response);
+ } else {
+ Exception e;
+ switch (resultCode) {
+ case ENTITY_NOT_FOUND:
+ e = new EntityNotFoundException(result.getMessage());
+ break;
+ case UNAUTHORIZED:
+ e = new UnauthorizedException(result.getMessage());
+ break;
+ case ACCESS_DENIED:
+ e = new AccessDeniedException(result.getMessage());
+ break;
+ case INTERNAL_ERROR:
+ e = new InternalErrorException(result.getMessage());
+ break;
+ default:
+ e = new UnauthorizedException("Permission denied.");
+ break;
+ }
+ onFailure(e);
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ action.onFailure(e);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
new file mode 100644
index 0000000..58bbec5
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
@@ -0,0 +1,335 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature;
+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.SubscriptionUpdate;
+import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+@Service
+@Slf4j
+public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptionService {
+
+ @Autowired
+ private TelemetryWebSocketService wsService;
+
+ @Autowired
+ private AttributesService attrService;
+
+ @Autowired
+ private TimeseriesService tsService;
+
+ @Autowired
+ private ClusterRoutingService routingService;
+
+ private ExecutorService tsCallBackExecutor;
+ private ExecutorService wsCallBackExecutor;
+
+ @PostConstruct
+ public void initExecutor() {
+ tsCallBackExecutor = Executors.newSingleThreadExecutor();
+ wsCallBackExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ @PreDestroy
+ public void shutdownExecutor() {
+ if (tsCallBackExecutor != null) {
+ tsCallBackExecutor.shutdownNow();
+ }
+ if (wsCallBackExecutor != null) {
+ wsCallBackExecutor.shutdownNow();
+ }
+ }
+
+ private final Map<EntityId, Set<Subscription>> subscriptionsByEntityId = new HashMap<>();
+
+ private final Map<String, Map<Integer, Subscription>> subscriptionsByWsSessionId = new HashMap<>();
+
+ @Override
+ public void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub) {
+ Optional<ServerAddress> server = routingService.resolveById(entityId);
+ Subscription subscription;
+ if (server.isPresent()) {
+ ServerAddress address = server.get();
+ log.trace("[{}] Forwarding subscription [{}] for device [{}] to [{}]", sessionId, sub.getSubscriptionId(), entityId, address);
+ subscription = new Subscription(sub, true, address);
+// rpcHandler.onNewSubscription(ctx, address, sessionId, subscription);
+ } else {
+ log.trace("[{}] Registering local subscription [{}] for device [{}]", sessionId, sub.getSubscriptionId(), entityId);
+ subscription = new Subscription(sub, true);
+ }
+ registerSubscription(sessionId, entityId, subscription);
+ }
+
+ @Override
+ public void cleanupLocalWsSessionSubscriptions(TelemetryWebSocketSessionRef sessionRef, String sessionId) {
+ cleanupLocalWsSessionSubscriptions(sessionId);
+ }
+
+ @Override
+ public void removeSubscription(String sessionId, int subscriptionId) {
+ log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId);
+ Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId);
+ if (sessionSubscriptions != null) {
+ Subscription subscription = sessionSubscriptions.remove(subscriptionId);
+ if (subscription != null) {
+ processSubscriptionRemoval(sessionId, sessionSubscriptions, subscription);
+ } else {
+ log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId);
+ }
+ } else {
+ log.debug("[{}] No session subscriptions found!", sessionId);
+ }
+ }
+
+ @Override
+ public void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback) {
+ saveAndNotify(entityId, ts, 0L, callback);
+ }
+
+ @Override
+ public void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, long ttl, FutureCallback<Void> callback) {
+ ListenableFuture<List<Void>> saveFuture = tsService.save(entityId, ts, ttl);
+ addMainCallback(saveFuture, callback);
+ addWsCallback(saveFuture, success -> onTimeseriesUpdate(entityId, ts));
+ }
+
+ @Override
+ public void saveAndNotify(EntityId entityId, String scope, List<AttributeKvEntry> attributes, FutureCallback<Void> callback) {
+ ListenableFuture<List<Void>> saveFuture = attrService.save(entityId, scope, attributes);
+ addMainCallback(saveFuture, callback);
+ addWsCallback(saveFuture, success -> onAttributesUpdate(entityId, scope, attributes));
+ }
+
+ private void onAttributesUpdate(EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
+ Optional<ServerAddress> serverAddress = routingService.resolveById(entityId);
+ if (!serverAddress.isPresent()) {
+ onLocalAttributesUpdate(entityId, scope, attributes);
+ } else {
+// rpcHandler.onAttributesUpdate(ctx, serverAddress.get(), entityId, entries);
+ }
+ }
+
+ private void onTimeseriesUpdate(EntityId entityId, List<TsKvEntry> ts) {
+ Optional<ServerAddress> serverAddress = routingService.resolveById(entityId);
+ if (!serverAddress.isPresent()) {
+ onLocalTimeseriesUpdate(entityId, ts);
+ } else {
+// rpcHandler.onTimeseriesUpdate(ctx, serverAddress.get(), entityId, entries);
+ }
+ }
+
+ private void onLocalAttributesUpdate(EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
+ onLocalSubUpdate(entityId, s -> TelemetryFeature.ATTRIBUTES == s.getType() && (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope())), s -> {
+ List<TsKvEntry> subscriptionUpdate = null;
+ for (AttributeKvEntry kv : attributes) {
+ if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) {
+ if (subscriptionUpdate == null) {
+ subscriptionUpdate = new ArrayList<>();
+ }
+ subscriptionUpdate.add(new BasicTsKvEntry(kv.getLastUpdateTs(), kv));
+ }
+ }
+ return subscriptionUpdate;
+ });
+ }
+
+ private void onLocalTimeseriesUpdate(EntityId entityId, List<TsKvEntry> ts) {
+ onLocalSubUpdate(entityId, s -> TelemetryFeature.TIMESERIES == s.getType(), s -> {
+ List<TsKvEntry> subscriptionUpdate = null;
+ for (TsKvEntry kv : ts) {
+ if (s.isAllKeys() || s.getKeyStates().containsKey((kv.getKey()))) {
+ if (subscriptionUpdate == null) {
+ subscriptionUpdate = new ArrayList<>();
+ }
+ subscriptionUpdate.add(kv);
+ }
+ }
+ return subscriptionUpdate;
+ });
+ }
+
+ private void onLocalSubUpdate(EntityId entityId, Predicate<Subscription> filter, Function<Subscription, List<TsKvEntry>> f) {
+ Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId);
+ if (deviceSubscriptions != null) {
+ deviceSubscriptions.stream().filter(filter).forEach(s -> {
+ String sessionId = s.getWsSessionId();
+ List<TsKvEntry> subscriptionUpdate = f.apply(s);
+ if (subscriptionUpdate == null || !subscriptionUpdate.isEmpty()) {
+ SubscriptionUpdate update = new SubscriptionUpdate(s.getSubscriptionId(), subscriptionUpdate);
+ if (s.isLocal()) {
+ updateSubscriptionState(sessionId, s, update);
+ wsService.sendWsMsg(sessionId, update);
+ } else {
+ //TODO: ashvayka
+// rpcHandler.onSubscriptionUpdate(ctx, s.getServer(), sessionId, update);
+ }
+ }
+ });
+ } else {
+ log.debug("[{}] No device subscriptions to process!", entityId);
+ }
+ }
+
+ private void updateSubscriptionState(String sessionId, Subscription subState, SubscriptionUpdate update) {
+ log.trace("[{}] updating subscription state {} using onUpdate {}", sessionId, subState, update);
+ update.getLatestValues().entrySet().forEach(e -> subState.setKeyState(e.getKey(), e.getValue()));
+ }
+
+ private void registerSubscription(String sessionId, EntityId entityId, Subscription subscription) {
+ Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.computeIfAbsent(entityId, k -> new HashSet<>());
+ deviceSubscriptions.add(subscription);
+ Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.computeIfAbsent(sessionId, k -> new HashMap<>());
+ sessionSubscriptions.put(subscription.getSubscriptionId(), subscription);
+ }
+
+ public void cleanupLocalWsSessionSubscriptions(String sessionId) {
+ cleanupWsSessionSubscriptions(sessionId, true);
+ }
+
+ public void cleanupRemoteWsSessionSubscriptions(String sessionId) {
+ cleanupWsSessionSubscriptions(sessionId, false);
+ }
+
+ private void cleanupWsSessionSubscriptions(String sessionId, boolean localSession) {
+ log.debug("[{}] Removing all subscriptions for particular session.", sessionId);
+ Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId);
+ if (sessionSubscriptions != null) {
+ int sessionSubscriptionSize = sessionSubscriptions.size();
+
+ for (Subscription subscription : sessionSubscriptions.values()) {
+ EntityId entityId = subscription.getEntityId();
+ Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId);
+ deviceSubscriptions.remove(subscription);
+ if (deviceSubscriptions.isEmpty()) {
+ subscriptionsByEntityId.remove(entityId);
+ }
+ }
+ subscriptionsByWsSessionId.remove(sessionId);
+ log.debug("[{}] Removed {} subscriptions for particular session.", sessionId, sessionSubscriptionSize);
+
+ if (localSession) {
+ notifyWsSubscriptionClosed(sessionId, sessionSubscriptions);
+ }
+ } else {
+ log.debug("[{}] No subscriptions found!", sessionId);
+ }
+ }
+
+ private void notifyWsSubscriptionClosed(String sessionId, Map<Integer, Subscription> sessionSubscriptions) {
+ Set<ServerAddress> affectedServers = new HashSet<>();
+ for (Subscription subscription : sessionSubscriptions.values()) {
+ if (subscription.getServer() != null) {
+ affectedServers.add(subscription.getServer());
+ }
+ }
+ for (ServerAddress address : affectedServers) {
+ log.debug("[{}] Going to onSubscriptionUpdate [{}] server about session close event", sessionId, address);
+// rpcHandler.onSessionClose(ctx, address, sessionId);
+ }
+ }
+
+ private void processSubscriptionRemoval(String sessionId, Map<Integer, Subscription> sessionSubscriptions, Subscription subscription) {
+ EntityId entityId = subscription.getEntityId();
+ 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 = subscriptionsByEntityId.get(entityId);
+ if (deviceSubscriptions != null) {
+ boolean result = deviceSubscriptions.remove(subscription);
+ if (result) {
+ if (deviceSubscriptions.size() == 0) {
+ log.debug("[{}] Removed last subscription for particular device.", sessionId);
+ subscriptionsByEntityId.remove(entityId);
+ } else {
+ log.debug("[{}] Removed device subscription.", sessionId);
+ }
+ } else {
+ log.debug("[{}] Subscription not found!", sessionId);
+ }
+ } else {
+ log.debug("[{}] No device subscriptions found!", sessionId);
+ }
+ }
+
+ private void addMainCallback(ListenableFuture<List<Void>> saveFuture, final FutureCallback<Void> callback) {
+ Futures.addCallback(saveFuture, new FutureCallback<List<Void>>() {
+ @Override
+ public void onSuccess(@Nullable List<Void> result) {
+ callback.onSuccess(null);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ callback.onFailure(t);
+ }
+ }, tsCallBackExecutor);
+ }
+
+ private void addWsCallback(ListenableFuture<List<Void>> saveFuture, Consumer<Void> callback) {
+ Futures.addCallback(saveFuture, new FutureCallback<List<Void>>() {
+ @Override
+ public void onSuccess(@Nullable List<Void> result) {
+ callback.accept(null);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ }
+ }, wsCallBackExecutor);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
new file mode 100644
index 0000000..57f3876
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
@@ -0,0 +1,563 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.hazelcast.util.function.Consumer;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.actors.plugin.ValidationResult;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.extensions.api.exception.UnauthorizedException;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.ws.SessionEvent;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.AttributesSubscriptionCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.GetHistoryCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.SubscriptionCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.TelemetryPluginCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.TelemetryPluginCmdsWrapper;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.TimeseriesSubscriptionCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature;
+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.SubscriptionUpdate;
+import org.thingsboard.server.service.security.AccessValidator;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+@Service
+@Slf4j
+public class DefaultTelemetryWebSocketService implements TelemetryWebSocketService {
+
+ public static final int DEFAULT_LIMIT = 100;
+ public static final Aggregation DEFAULT_AGGREGATION = Aggregation.NONE;
+ private static final int UNKNOWN_SUBSCRIPTION_ID = 0;
+ private static final String PROCESSING_MSG = "[{}] Processing: {}";
+ private static final ObjectMapper jsonMapper = new ObjectMapper();
+ private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!";
+ private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!";
+ private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!";
+
+ private final ConcurrentMap<String, WsSessionMetaData> wsSessionsMap = new ConcurrentHashMap<>();
+
+ @Autowired
+ private TelemetrySubscriptionService subscriptionManager;
+
+ @Autowired
+ private TelemetryWebSocketMsgEndpoint msgEndpoint;
+
+ @Autowired
+ private AccessValidator accessValidator;
+
+ @Autowired
+ private AttributesService attributesService;
+
+ @Autowired
+ private TimeseriesService tsService;
+
+ private ExecutorService executor;
+
+ @PostConstruct
+ public void initExecutor() {
+ executor = Executors.newSingleThreadExecutor();
+ }
+
+ @PreDestroy
+ public void shutdownExecutor() {
+ if (executor != null) {
+ executor.shutdownNow();
+ }
+ }
+
+ @Override
+ public void handleWebSocketSessionEvent(TelemetryWebSocketSessionRef sessionRef, SessionEvent event) {
+ String sessionId = sessionRef.getSessionId();
+ log.debug(PROCESSING_MSG, sessionId, event);
+ switch (event.getEventType()) {
+ case ESTABLISHED:
+ wsSessionsMap.put(sessionId, new WsSessionMetaData(sessionRef));
+ break;
+ case ERROR:
+ log.debug("[{}] Unknown websocket session error: {}. ", sessionId, event.getError().orElse(null));
+ break;
+ case CLOSED:
+ wsSessionsMap.remove(sessionId);
+ subscriptionManager.cleanupLocalWsSessionSubscriptions(sessionRef, sessionId);
+ break;
+ }
+ }
+
+ @Override
+ public void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg) {
+ if (log.isTraceEnabled()) {
+ log.trace("[{}] Processing: {}", sessionRef.getSessionId(), msg);
+ }
+
+ try {
+ TelemetryPluginCmdsWrapper cmdsWrapper = jsonMapper.readValue(msg, TelemetryPluginCmdsWrapper.class);
+ if (cmdsWrapper != null) {
+ if (cmdsWrapper.getAttrSubCmds() != null) {
+ cmdsWrapper.getAttrSubCmds().forEach(cmd -> handleWsAttributesSubscriptionCmd(sessionRef, cmd));
+ }
+ if (cmdsWrapper.getTsSubCmds() != null) {
+ cmdsWrapper.getTsSubCmds().forEach(cmd -> handleWsTimeseriesSubscriptionCmd(sessionRef, cmd));
+ }
+ if (cmdsWrapper.getHistoryCmds() != null) {
+ cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(sessionRef, cmd));
+ }
+ }
+ } catch (IOException e) {
+ log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e);
+ SubscriptionUpdate update = new SubscriptionUpdate(UNKNOWN_SUBSCRIPTION_ID, SubscriptionErrorCode.INTERNAL_ERROR, SESSION_META_DATA_NOT_FOUND);
+ sendWsMsg(sessionRef, update);
+ }
+ }
+
+ @Override
+ public void sendWsMsg(String sessionId, SubscriptionUpdate update) {
+ WsSessionMetaData md = wsSessionsMap.get(sessionId);
+ if (md != null) {
+ sendWsMsg(md.getSessionRef(), update);
+ }
+ }
+
+ private void handleWsAttributesSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, AttributesSubscriptionCmd cmd) {
+ String sessionId = sessionRef.getSessionId();
+ log.debug("[{}] Processing: {}", sessionId, cmd);
+
+ if (validateSessionMetadata(sessionRef, cmd, sessionId)) {
+ if (cmd.isUnsubscribe()) {
+ unsubscribe(sessionRef, cmd, sessionId);
+ } else if (validateSubscriptionCmd(sessionRef, cmd)) {
+ EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
+ log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), entityId);
+ Optional<Set<String>> keysOptional = getKeys(cmd);
+ if (keysOptional.isPresent()) {
+ List<String> keys = new ArrayList<>(keysOptional.get());
+ handleWsAttributesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId, keys);
+ } else {
+ handleWsAttributesSubscription(sessionRef, cmd, sessionId, entityId);
+ }
+ }
+ }
+ }
+
+ private void handleWsAttributesSubscriptionByKeys(TelemetryWebSocketSessionRef sessionRef,
+ AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId,
+ List<String> keys) {
+ FutureCallback<List<AttributeKvEntry>> callback = new FutureCallback<List<AttributeKvEntry>>() {
+ @Override
+ public void onSuccess(List<AttributeKvEntry> data) {
+ List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
+ sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
+
+ Map<String, Long> subState = new HashMap<>(keys.size());
+ keys.forEach(key -> subState.put(key, 0L));
+ attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
+
+ SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.ATTRIBUTES, false, subState, cmd.getScope());
+ subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub);
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ log.error(FAILED_TO_FETCH_ATTRIBUTES, e);
+ SubscriptionUpdate update;
+ if (UnauthorizedException.class.isInstance(e)) {
+ update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
+ SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
+ } else {
+ update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+ FAILED_TO_FETCH_ATTRIBUTES);
+ }
+ sendWsMsg(sessionRef, update);
+ }
+ };
+
+ if (StringUtils.isEmpty(cmd.getScope())) {
+ accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, keys, callback));
+ } else {
+ accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, cmd.getScope(), keys, callback));
+ }
+ }
+
+ private void handleWsHistoryCmd(TelemetryWebSocketSessionRef sessionRef, GetHistoryCmd cmd) {
+ String sessionId = sessionRef.getSessionId();
+ WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
+ if (sessionMD == null) {
+ log.warn("[{}] Session meta data not found. ", sessionId);
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+ SESSION_META_DATA_NOT_FOUND);
+ sendWsMsg(sessionRef, update);
+ return;
+ }
+ if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty() || cmd.getEntityType() == null || cmd.getEntityType().isEmpty()) {
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+ "Device id is empty!");
+ sendWsMsg(sessionRef, update);
+ return;
+ }
+ if (cmd.getKeys() == null || cmd.getKeys().isEmpty()) {
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+ "Keys are empty!");
+ sendWsMsg(sessionRef, update);
+ return;
+ }
+ EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
+ List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
+ List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg())))
+ .collect(Collectors.toList());
+
+ FutureCallback<List<TsKvEntry>> callback = new FutureCallback<List<TsKvEntry>>() {
+ @Override
+ public void onSuccess(List<TsKvEntry> data) {
+ sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ SubscriptionUpdate update;
+ if (UnauthorizedException.class.isInstance(e)) {
+ update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
+ SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
+ } else {
+ update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+ FAILED_TO_FETCH_DATA);
+ }
+ sendWsMsg(sessionRef, update);
+ }
+ };
+ accessValidator.validate(sessionRef.getSecurityCtx(), entityId,
+ on(r -> Futures.addCallback(tsService.findAll(entityId, queries), callback, executor), callback::onFailure));
+ }
+
+ private void handleWsAttributesSubscription(TelemetryWebSocketSessionRef sessionRef,
+ AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId) {
+ FutureCallback<List<AttributeKvEntry>> callback = new FutureCallback<List<AttributeKvEntry>>() {
+ @Override
+ public void onSuccess(List<AttributeKvEntry> data) {
+ List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
+ sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
+
+ Map<String, Long> subState = new HashMap<>(attributesData.size());
+ attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
+
+ SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.ATTRIBUTES, true, subState, cmd.getScope());
+ subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub);
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ log.error(FAILED_TO_FETCH_ATTRIBUTES, e);
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+ FAILED_TO_FETCH_ATTRIBUTES);
+ sendWsMsg(sessionRef, update);
+ }
+ };
+
+
+ if (StringUtils.isEmpty(cmd.getScope())) {
+ accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, callback));
+ } else {
+ accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, cmd.getScope(), callback));
+ }
+ }
+
+ private void handleWsTimeseriesSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd) {
+ String sessionId = sessionRef.getSessionId();
+ log.debug("[{}] Processing: {}", sessionId, cmd);
+
+ if (validateSessionMetadata(sessionRef, cmd, sessionId)) {
+ if (cmd.isUnsubscribe()) {
+ unsubscribe(sessionRef, cmd, sessionId);
+ } else if (validateSubscriptionCmd(sessionRef, cmd)) {
+ EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
+ Optional<Set<String>> keysOptional = getKeys(cmd);
+
+ if (keysOptional.isPresent()) {
+ handleWsTimeseriesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId);
+ } else {
+ handleWsTimeseriesSubscription(sessionRef, cmd, sessionId, entityId);
+ }
+ }
+ }
+ }
+
+ private void handleWsTimeseriesSubscriptionByKeys(TelemetryWebSocketSessionRef sessionRef,
+ TimeseriesSubscriptionCmd cmd, String sessionId, EntityId entityId) {
+ long startTs;
+ if (cmd.getTimeWindow() > 0) {
+ List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
+ log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), entityId);
+ startTs = cmd.getStartTs();
+ long endTs = cmd.getStartTs() + cmd.getTimeWindow();
+ List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, cmd.getInterval(),
+ getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
+
+ final FutureCallback<List<TsKvEntry>> callback = getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys);
+ accessValidator.validate(sessionRef.getSecurityCtx(), entityId,
+ on(r -> Futures.addCallback(tsService.findAll(entityId, queries), callback, executor), callback::onFailure));
+ } else {
+ List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
+ startTs = System.currentTimeMillis();
+ log.debug("[{}] fetching latest timeseries data for keys: ({}) for device : {}", sessionId, cmd.getKeys(), entityId);
+ final FutureCallback<List<TsKvEntry>> callback = getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys);
+ accessValidator.validate(sessionRef.getSecurityCtx(), entityId,
+ on(r -> Futures.addCallback(tsService.findLatest(entityId, keys), callback, executor), callback::onFailure));
+ }
+ }
+
+ private void handleWsTimeseriesSubscription(TelemetryWebSocketSessionRef sessionRef,
+ TimeseriesSubscriptionCmd cmd, String sessionId, EntityId entityId) {
+ FutureCallback<List<TsKvEntry>> callback = new FutureCallback<List<TsKvEntry>>() {
+ @Override
+ public void onSuccess(List<TsKvEntry> data) {
+ sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+ Map<String, Long> subState = new HashMap<>(data.size());
+ data.forEach(v -> subState.put(v.getKey(), v.getTs()));
+ SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.TIMESERIES, true, subState, cmd.getScope());
+ subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub);
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ SubscriptionUpdate update;
+ if (UnauthorizedException.class.isInstance(e)) {
+ update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
+ SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
+ } else {
+ update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+ FAILED_TO_FETCH_DATA);
+ }
+ sendWsMsg(sessionRef, update);
+ }
+ };
+ accessValidator.validate(sessionRef.getSecurityCtx(), entityId,
+ on(r -> Futures.addCallback(tsService.findAllLatest(entityId), callback, executor), callback::onFailure));
+ }
+
+ private FutureCallback<List<TsKvEntry>> getSubscriptionCallback(final TelemetryWebSocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final EntityId entityId, final long startTs, final List<String> keys) {
+ return new FutureCallback<List<TsKvEntry>>() {
+ @Override
+ public void onSuccess(List<TsKvEntry> data) {
+ sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+
+ Map<String, Long> subState = new HashMap<>(keys.size());
+ keys.forEach(key -> subState.put(key, startTs));
+ data.forEach(v -> subState.put(v.getKey(), v.getTs()));
+ SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.TIMESERIES, false, subState, cmd.getScope());
+ subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub);
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ log.error(FAILED_TO_FETCH_DATA, e);
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+ FAILED_TO_FETCH_DATA);
+ sendWsMsg(sessionRef, update);
+ }
+ };
+ }
+
+ private void unsubscribe(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
+ if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
+ subscriptionManager.cleanupLocalWsSessionSubscriptions(sessionRef, sessionId);
+ } else {
+ subscriptionManager.removeSubscription(sessionId, cmd.getCmdId());
+ }
+ }
+
+ private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) {
+ if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+ "Device id is empty!");
+ sendWsMsg(sessionRef, update);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
+ WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
+ if (sessionMD == null) {
+ log.warn("[{}] Session meta data not found. ", sessionId);
+ SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+ SESSION_META_DATA_NOT_FOUND);
+ sendWsMsg(sessionRef, update);
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, SubscriptionUpdate update) {
+ try {
+ msgEndpoint.send(sessionRef, jsonMapper.writeValueAsString(update));
+ } catch (JsonProcessingException e) {
+ log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e);
+ } catch (IOException e) {
+ log.warn("[{}] Failed to send reply: {}", sessionRef.getSessionId(), update, e);
+ }
+ }
+
+ private static Optional<Set<String>> getKeys(TelemetryPluginCmd cmd) {
+ if (!StringUtils.isEmpty(cmd.getKeys())) {
+ Set<String> keys = new HashSet<>();
+ Collections.addAll(keys, cmd.getKeys().split(","));
+ return Optional.of(keys);
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ private ListenableFuture<List<AttributeKvEntry>> mergeAllAttributesFutures(List<ListenableFuture<List<AttributeKvEntry>>> futures) {
+ return Futures.transform(Futures.successfulAsList(futures),
+ (Function<? super List<List<AttributeKvEntry>>, ? extends List<AttributeKvEntry>>) input -> {
+ List<AttributeKvEntry> tmp = new ArrayList<>();
+ if (input != null) {
+ input.forEach(tmp::addAll);
+ }
+ return tmp;
+ }, executor);
+ }
+
+ private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final List<String> keys, final FutureCallback<List<AttributeKvEntry>> callback) {
+ return new FutureCallback<ValidationResult>() {
+ @Override
+ public void onSuccess(@Nullable ValidationResult result) {
+ List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
+ for (String scope : DataConstants.allScopes()) {
+ futures.add(attributesService.find(entityId, scope, keys));
+ }
+
+ ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures);
+ Futures.addCallback(future, callback);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ callback.onFailure(t);
+ }
+ };
+ }
+
+ private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final String scope, final List<String> keys, final FutureCallback<List<AttributeKvEntry>> callback) {
+ return new FutureCallback<ValidationResult>() {
+ @Override
+ public void onSuccess(@Nullable ValidationResult result) {
+ Futures.addCallback(attributesService.find(entityId, scope, keys), callback);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ callback.onFailure(t);
+ }
+ };
+ }
+
+ private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final FutureCallback<List<AttributeKvEntry>> callback) {
+ return new FutureCallback<ValidationResult>() {
+ @Override
+ public void onSuccess(@Nullable ValidationResult result) {
+ List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
+ for (String scope : DataConstants.allScopes()) {
+ futures.add(attributesService.findAll(entityId, scope));
+ }
+
+ ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures);
+ Futures.addCallback(future, callback);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ callback.onFailure(t);
+ }
+ };
+ }
+
+ private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final String scope, final FutureCallback<List<AttributeKvEntry>> callback) {
+ return new FutureCallback<ValidationResult>() {
+ @Override
+ public void onSuccess(@Nullable ValidationResult result) {
+ Futures.addCallback(attributesService.findAll(entityId, scope), callback);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ callback.onFailure(t);
+ }
+ };
+ }
+
+ private FutureCallback<ValidationResult> on(Consumer<ValidationResult> success, Consumer<Throwable> failure) {
+ return new FutureCallback<ValidationResult>() {
+ @Override
+ public void onSuccess(@Nullable ValidationResult result) {
+ success.accept(result);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ failure.accept(t);
+ }
+ };
+ }
+
+
+ private static Aggregation getAggregation(String agg) {
+ return StringUtils.isEmpty(agg) ? DEFAULT_AGGREGATION : Aggregation.valueOf(agg);
+ }
+
+ private int getLimit(int limit) {
+ return limit == 0 ? DEFAULT_LIMIT : limit;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java
new file mode 100644
index 0000000..923d06b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry;
+
+import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+public interface TelemetrySubscriptionService extends RuleEngineTelemetryService {
+
+ void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub);
+
+ void cleanupLocalWsSessionSubscriptions(TelemetryWebSocketSessionRef sessionRef, String sessionId);
+
+ void removeSubscription(String sessionId, int cmdId);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java
new file mode 100644
index 0000000..be6fc56
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry;
+
+import org.thingsboard.server.extensions.api.plugins.ws.SessionEvent;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionUpdate;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+public interface TelemetryWebSocketService {
+
+ void handleWebSocketSessionEvent(TelemetryWebSocketSessionRef sessionRef, SessionEvent sessionEvent);
+
+ void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg);
+
+ void sendWsMsg(String sessionId, SubscriptionUpdate update);
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java
new file mode 100644
index 0000000..53438c5
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry;
+
+import lombok.Getter;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+import java.net.InetSocketAddress;
+import java.util.Objects;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+public class TelemetryWebSocketSessionRef {
+
+ private static final long serialVersionUID = 1L;
+
+ @Getter
+ private final String sessionId;
+ @Getter
+ private final SecurityUser securityCtx;
+ @Getter
+ private final InetSocketAddress localAddress;
+ @Getter
+ private final InetSocketAddress remoteAddress;
+
+ public TelemetryWebSocketSessionRef(String sessionId, SecurityUser securityCtx, InetSocketAddress localAddress, InetSocketAddress remoteAddress) {
+ this.sessionId = sessionId;
+ this.securityCtx = securityCtx;
+ this.localAddress = localAddress;
+ this.remoteAddress = remoteAddress;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TelemetryWebSocketSessionRef that = (TelemetryWebSocketSessionRef) o;
+ return Objects.equals(sessionId, that.sessionId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(sessionId);
+ }
+
+ @Override
+ public String toString() {
+ return "TelemetryWebSocketSessionRef{" +
+ "sessionId='" + sessionId + '\'' +
+ ", localAddress=" + localAddress +
+ ", remoteAddress=" + remoteAddress +
+ '}';
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketTextMsg.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketTextMsg.java
new file mode 100644
index 0000000..5d4630c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketTextMsg.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry;
+
+import lombok.Data;
+import lombok.Getter;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+import java.net.InetSocketAddress;
+import java.util.Objects;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+@Data
+public class TelemetryWebSocketTextMsg {
+
+ private final TelemetryWebSocketSessionRef sessionRef;
+ private final String payload;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/WsSessionMetaData.java b/application/src/main/java/org/thingsboard/server/service/telemetry/WsSessionMetaData.java
new file mode 100644
index 0000000..dd15ed3
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/WsSessionMetaData.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry;
+
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+public class WsSessionMetaData {
+ private TelemetryWebSocketSessionRef sessionRef;
+ private long lastActivityTime;
+
+ public WsSessionMetaData(TelemetryWebSocketSessionRef sessionRef) {
+ super();
+ this.sessionRef = sessionRef;
+ this.lastActivityTime = System.currentTimeMillis();
+ }
+
+ public TelemetryWebSocketSessionRef getSessionRef() {
+ return sessionRef;
+ }
+
+ public void setSessionRef(TelemetryWebSocketSessionRef 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/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java b/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java
index c3444d4..751bde6 100644
--- a/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java
+++ b/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java
@@ -19,6 +19,7 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.Arrays;
@@ -26,6 +27,7 @@ import java.util.Arrays;
@SpringBootConfiguration
@EnableAsync
@EnableSwagger2
+@EnableScheduling
@ComponentScan({"org.thingsboard.server"})
public class ThingsboardServerApplication {
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 1c842c8..a9e47de 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -62,7 +62,7 @@ cluster:
# Plugins configuration parameters
plugins:
# Comma seperated package list used during classpath scanning for plugins
- scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions}"
+ scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions,org.thingsboard.rule.engine}"
# JWT Token parameters
security.jwt:
@@ -181,6 +181,10 @@ cassandra:
default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
# Specify partitioning size for timestamp key-value storage. Example MINUTES, HOURS, DAYS, MONTHS
ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
+ buffer_size: "${CASSANDRA_QUERY_BUFFER_SIZE:200000}"
+ concurrent_limit: "${CASSANDRA_QUERY_CONCURRENT_LIMIT:1000}"
+ permit_max_wait_time: "${PERMIT_MAX_WAIT_TIME:120000}"
+ rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:30000}"
queue:
msg.ttl: 604800 # 7 days
@@ -215,6 +219,18 @@ actors:
termination.delay: "${ACTORS_RULE_TERMINATION_DELAY:30000}"
# Errors for particular actor are persisted once per specified amount of milliseconds
error_persist_frequency: "${ACTORS_RULE_ERROR_FREQUENCY:3000}"
+ # Specify thread pool size for database request callbacks executor service
+ db_callback_thread_pool_size: "${ACTORS_RULE_DB_CALLBACK_THREAD_POOL_SIZE:1}"
+ # Specify thread pool size for javascript executor service
+ js_thread_pool_size: "${ACTORS_RULE_JS_THREAD_POOL_SIZE:10}"
+ # Specify thread pool size for mail sender executor service
+ mail_thread_pool_size: "${ACTORS_RULE_MAIL_THREAD_POOL_SIZE:10}"
+ chain:
+ # Errors for particular actor are persisted once per specified amount of milliseconds
+ error_persist_frequency: "${ACTORS_RULE_CHAIN_ERROR_FREQUENCY:3000}"
+ node:
+ # Errors for particular actor are persisted once per specified amount of milliseconds
+ error_persist_frequency: "${ACTORS_RULE_NODE_ERROR_FREQUENCY:3000}"
statistics:
# Enable/disable actor statistics
enabled: "${ACTORS_STATISTICS_ENABLED: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
index b92e464..3ec4dc8 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
@@ -96,6 +96,8 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC
@Slf4j
public abstract class AbstractControllerTest {
+ protected ObjectMapper mapper = new ObjectMapper();
+
protected static final String TEST_TENANT_NAME = "TEST TENANT";
protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org";
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java
new file mode 100644
index 0000000..93fe767
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+
+/**
+ * Created by ashvayka on 20.03.18.
+ */
+public class AbstractRuleEngineControllerTest extends AbstractControllerTest {
+
+ protected RuleChain saveRuleChain(RuleChain ruleChain) throws Exception {
+ return doPost("/api/ruleChain", ruleChain, RuleChain.class);
+ }
+
+ protected RuleChain getRuleChain(RuleChainId ruleChainId) throws Exception {
+ return doGet("/api/ruleChain/" + ruleChainId.getId().toString(), RuleChain.class);
+ }
+
+ protected RuleChainMetaData saveRuleChainMetaData(RuleChainMetaData ruleChainMD) throws Exception {
+ return doPost("/api/ruleChain/metadata", ruleChainMD, RuleChainMetaData.class);
+ }
+
+ protected RuleChainMetaData getRuleChainMetaData(RuleChainId ruleChainId) throws Exception {
+ return doGet("/api/ruleChain/metadata/" + ruleChainId.getId().toString(), RuleChainMetaData.class);
+ }
+
+ protected TimePageData<Event> getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception {
+ TimePageLink pageLink = new TimePageLink(limit);
+ return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&",
+ new TypeReference<TimePageData<Event>>() {
+ }, pageLink, entityId.getEntityType(), entityId.getId(), DataConstants.DEBUG_RULE_NODE, tenantId.getId());
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
new file mode 100644
index 0000000..f88eb24
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
@@ -0,0 +1,190 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.rules.flow;
+
+import com.datastax.driver.core.utils.UUIDs;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.*;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import org.thingsboard.server.controller.AbstractRuleEngineControllerTest;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author Valerii Sosliuk
+ */
+@Slf4j
+public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRuleEngineControllerTest {
+
+ protected Tenant savedTenant;
+ protected User tenantAdmin;
+
+ @Autowired
+ protected ActorService actorService;
+
+ @Autowired
+ protected AttributesService attributesService;
+
+ @Autowired
+ protected RuleChainService ruleChainService;
+
+ @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");
+
+ createUserAndLogin(tenantAdmin, "testPassword1");
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ loginSysAdmin();
+ if (savedTenant != null) {
+ doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk());
+ }
+ }
+
+ @Test
+ public void testRuleChainWithTwoRules() throws Exception {
+ // Creating Rule Chain
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setName("Simple Rule Chain");
+ ruleChain.setTenantId(savedTenant.getId());
+ ruleChain.setRoot(true);
+ ruleChain.setDebugMode(true);
+ ruleChain = saveRuleChain(ruleChain);
+ Assert.assertNull(ruleChain.getFirstRuleNodeId());
+
+ RuleChainMetaData metaData = new RuleChainMetaData();
+ metaData.setRuleChainId(ruleChain.getId());
+
+ RuleNode ruleNode1 = new RuleNode();
+ ruleNode1.setName("Simple Rule Node 1");
+ ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+ ruleNode1.setDebugMode(true);
+ TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
+ configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
+ ruleNode1.setConfiguration(mapper.valueToTree(configuration1));
+
+ RuleNode ruleNode2 = new RuleNode();
+ ruleNode2.setName("Simple Rule Node 2");
+ ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+ ruleNode2.setDebugMode(true);
+ TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
+ configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
+ ruleNode2.setConfiguration(mapper.valueToTree(configuration2));
+
+
+ metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2));
+ metaData.setFirstNodeIndex(0);
+ metaData.addConnectionInfo(0, 1, "Success");
+ metaData = saveRuleChainMetaData(metaData);
+ Assert.assertNotNull(metaData);
+
+ ruleChain = getRuleChain(ruleChain.getId());
+ Assert.assertNotNull(ruleChain.getFirstRuleNodeId());
+
+ // Saving the device
+ Device device = new Device();
+ device.setName("My device");
+ device.setType("default");
+ device = doPost("/api/device", device, Device.class);
+
+ attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+ Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey1", "serverAttributeValue1"), System.currentTimeMillis())));
+ attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+ Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey2", "serverAttributeValue2"), System.currentTimeMillis())));
+
+
+ Thread.sleep(1000);
+
+ // Pushing Message to the system
+ TbMsg tbMsg = new TbMsg(UUIDs.timeBased(),
+ "CUSTOM",
+ device.getId(),
+ new TbMsgMetaData(),
+ "{}");
+ actorService.onMsg(new ServiceToRuleEngineMsg(savedTenant.getId(), tbMsg));
+
+ Thread.sleep(3000);
+
+ TimePageData<Event> events = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000);
+
+ Assert.assertEquals(2, events.getData().size());
+
+ Event inEvent = events.getData().stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+ Assert.assertEquals(ruleChain.getFirstRuleNodeId(), inEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+ Event outEvent = events.getData().stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+ Assert.assertEquals(ruleChain.getFirstRuleNodeId(), outEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+ Assert.assertEquals("serverAttributeValue1", outEvent.getBody().get("metadata").get("ss.serverAttributeKey1").asText());
+
+ RuleChain finalRuleChain = ruleChain;
+ RuleNode lastRuleNode = metaData.getNodes().stream().filter(node -> !node.getId().equals(finalRuleChain.getFirstRuleNodeId())).findFirst().get();
+
+ events = getDebugEvents(savedTenant.getId(), lastRuleNode.getId(), 1000);
+
+ Assert.assertEquals(2, events.getData().size());
+
+ inEvent = events.getData().stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+ Assert.assertEquals(lastRuleNode.getId(), inEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+ outEvent = events.getData().stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+ Assert.assertEquals(lastRuleNode.getId(), outEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+ Assert.assertEquals("serverAttributeValue1", outEvent.getBody().get("metadata").get("ss.serverAttributeKey1").asText());
+ Assert.assertEquals("serverAttributeValue2", outEvent.getBody().get("metadata").get("ss.serverAttributeKey2").asText());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
new file mode 100644
index 0000000..22d79f0
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
@@ -0,0 +1,158 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.rules.lifecycle;
+
+import com.datastax.driver.core.utils.UUIDs;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import org.thingsboard.server.controller.AbstractRuleEngineControllerTest;
+import org.thingsboard.server.dao.attributes.AttributesService;
+
+import java.util.Collections;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author Valerii Sosliuk
+ */
+@Slf4j
+public abstract class AbstractRuleEngineLifecycleIntegrationTest extends AbstractRuleEngineControllerTest {
+
+ protected Tenant savedTenant;
+ protected User tenantAdmin;
+
+ @Autowired
+ protected ActorService actorService;
+
+ @Autowired
+ protected AttributesService attributesService;
+
+ @Before
+ public void beforeTest() throws Exception {
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(savedTenant.getId());
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ createUserAndLogin(tenantAdmin, "testPassword1");
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ loginSysAdmin();
+ if (savedTenant != null) {
+ doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk());
+ }
+ }
+
+ @Test
+ public void testRuleChainWithOneRule() throws Exception {
+ // Creating Rule Chain
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setName("Simple Rule Chain");
+ ruleChain.setTenantId(savedTenant.getId());
+ ruleChain.setRoot(true);
+ ruleChain.setDebugMode(true);
+ ruleChain = saveRuleChain(ruleChain);
+ Assert.assertNull(ruleChain.getFirstRuleNodeId());
+
+ RuleChainMetaData metaData = new RuleChainMetaData();
+ metaData.setRuleChainId(ruleChain.getId());
+
+ RuleNode ruleNode = new RuleNode();
+ ruleNode.setName("Simple Rule Node");
+ ruleNode.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+ ruleNode.setDebugMode(true);
+ TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
+ configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey"));
+ ruleNode.setConfiguration(mapper.valueToTree(configuration));
+
+ metaData.setNodes(Collections.singletonList(ruleNode));
+ metaData.setFirstNodeIndex(0);
+
+ metaData = saveRuleChainMetaData(metaData);
+ Assert.assertNotNull(metaData);
+
+ ruleChain = getRuleChain(ruleChain.getId());
+ Assert.assertNotNull(ruleChain.getFirstRuleNodeId());
+
+ // Saving the device
+ Device device = new Device();
+ device.setName("My device");
+ device.setType("default");
+ device = doPost("/api/device", device, Device.class);
+
+ attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+ Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey", "serverAttributeValue"), System.currentTimeMillis())));
+
+ Thread.sleep(1000);
+
+ // Pushing Message to the system
+ TbMsg tbMsg = new TbMsg(UUIDs.timeBased(),
+ "CUSTOM",
+ device.getId(),
+ new TbMsgMetaData(),
+ "{}");
+ actorService.onMsg(new ServiceToRuleEngineMsg(savedTenant.getId(), tbMsg));
+
+ Thread.sleep(3000);
+
+ TimePageData<Event> events = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000);
+
+ Assert.assertEquals(2, events.getData().size());
+
+ Event inEvent = events.getData().stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+ Assert.assertEquals(ruleChain.getFirstRuleNodeId(), inEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+ Event outEvent = events.getData().stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+ Assert.assertEquals(ruleChain.getFirstRuleNodeId(), outEvent.getEntityId());
+ Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+ Assert.assertEquals("serverAttributeValue", outEvent.getBody().get("metadata").get("ss.serverAttributeKey").asText());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/RuleEngineLifecycleSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/RuleEngineLifecycleSqlIntegrationTest.java
new file mode 100644
index 0000000..004958b
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/RuleEngineLifecycleSqlIntegrationTest.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.rules.lifecycle;
+
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.rules.flow.AbstractRuleEngineFlowIntegrationTest;
+
+/**
+ * Created by Valerii Sosliuk on 8/22/2017.
+ */
+@DaoSqlTest
+public class RuleEngineLifecycleSqlIntegrationTest extends AbstractRuleEngineLifecycleIntegrationTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java
new file mode 100644
index 0000000..65b4293
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.rules;
+
+import org.junit.ClassRule;
+import org.junit.extensions.cpsuite.ClasspathSuite;
+import org.junit.runner.RunWith;
+import org.thingsboard.server.dao.CustomSqlUnit;
+
+import java.util.Arrays;
+
+@RunWith(ClasspathSuite.class)
+@ClasspathSuite.ClassnameFilters({
+ "org.thingsboard.server.rules.flow.*Test"})
+public class RuleEngineSqlTestSuite {
+
+ @ClassRule
+ public static CustomSqlUnit sqlUnit = new CustomSqlUnit(
+ Arrays.asList("sql/schema.sql", "sql/system-data.sql"),
+ "sql/drop-all-tables.sql",
+ "sql-test.properties");
+}
diff --git a/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java b/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
index ed3750d..ba2bb65 100644
--- a/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
+++ b/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
@@ -22,7 +22,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.rule.engine.api.MailService;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
@Profile("test")
@Configuration
diff --git a/application/src/test/java/org/thingsboard/server/service/script/NashornJsEngineTest.java b/application/src/test/java/org/thingsboard/server/service/script/NashornJsEngineTest.java
new file mode 100644
index 0000000..e6a48e2
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/script/NashornJsEngineTest.java
@@ -0,0 +1,151 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.script;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.collect.Sets;
+import org.junit.Test;
+import org.thingsboard.rule.engine.api.ScriptEngine;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+public class NashornJsEngineTest {
+
+ private ScriptEngine scriptEngine;
+
+ @Test
+ public void msgCanBeUpdated() throws ScriptException {
+ String function = "metadata.temp = metadata.temp * 10; return {metadata: metadata};";
+ scriptEngine = new NashornJsEngine(function, "Transform");
+
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "7");
+ metaData.putValue("humidity", "99");
+ String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+
+ TbMsg actual = scriptEngine.executeUpdate(msg);
+ assertEquals("70", actual.getMetaData().getValue("temp"));
+ }
+
+ @Test
+ public void newAttributesCanBeAddedInMsg() throws ScriptException {
+ String function = "metadata.newAttr = metadata.humidity - msg.passed; return {metadata: metadata};";
+ scriptEngine = new NashornJsEngine(function, "Transform");
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "7");
+ metaData.putValue("humidity", "99");
+ String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+
+ TbMsg actual = scriptEngine.executeUpdate(msg);
+ assertEquals("94", actual.getMetaData().getValue("newAttr"));
+ }
+
+ @Test
+ public void payloadCanBeUpdated() throws ScriptException {
+ String function = "msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine'; return {msg: msg};";
+ scriptEngine = new NashornJsEngine(function, "Transform");
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "7");
+ metaData.putValue("humidity", "99");
+ String rawJson = "{\"name\":\"Vit\",\"passed\": 5,\"bigObj\":{\"prop\":42}}";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+
+ TbMsg actual = scriptEngine.executeUpdate(msg);
+
+ String expectedJson = "{\"name\":\"Vit\",\"passed\":35,\"bigObj\":{\"prop\":42,\"newProp\":\"Ukraine\"}}";
+ assertEquals(expectedJson, actual.getData());
+ }
+
+ @Test
+ public void metadataAccessibleForFilter() throws ScriptException {
+ String function = "return metadata.humidity < 15;";
+ scriptEngine = new NashornJsEngine(function, "Filter");
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "7");
+ metaData.putValue("humidity", "99");
+ String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+ assertFalse(scriptEngine.executeFilter(msg));
+ }
+
+ @Test
+ public void dataAccessibleForFilter() throws ScriptException {
+ String function = "return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 7 && msg.bigObj.prop == 42;";
+ scriptEngine = new NashornJsEngine(function, "Filter");
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "7");
+ metaData.putValue("humidity", "99");
+ String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+ assertTrue(scriptEngine.executeFilter(msg));
+ }
+
+ @Test
+ public void dataAccessibleForSwitch() throws ScriptException {
+ String jsCode = "function nextRelation(metadata, msg) {\n" +
+ " if(msg.passed == 5 && metadata.temp == 10)\n" +
+ " return 'one'\n" +
+ " else\n" +
+ " return 'two';\n" +
+ "};\n" +
+ "\n" +
+ "return nextRelation(metadata, msg);";
+ scriptEngine = new NashornJsEngine(jsCode, "Switch");
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "10");
+ metaData.putValue("humidity", "99");
+ String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+ Set<String> actual = scriptEngine.executeSwitch(msg);
+ assertEquals(Sets.newHashSet("one"), actual);
+ }
+
+ @Test
+ public void multipleRelationsReturnedFromSwitch() throws ScriptException {
+ String jsCode = "function nextRelation(metadata, msg) {\n" +
+ " if(msg.passed == 5 && metadata.temp == 10)\n" +
+ " return ['three', 'one']\n" +
+ " else\n" +
+ " return 'two';\n" +
+ "};\n" +
+ "\n" +
+ "return nextRelation(metadata, msg);";
+ scriptEngine = new NashornJsEngine(jsCode, "Switch");
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "10");
+ metaData.putValue("humidity", "99");
+ String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+ Set<String> actual = scriptEngine.executeSwitch(msg);
+ assertEquals(Sets.newHashSet("one", "three"), actual);
+ }
+
+}
\ No newline at end of file
diff --git a/application/src/test/java/org/thingsboard/server/system/BaseDeviceOfflineTest.java b/application/src/test/java/org/thingsboard/server/system/BaseDeviceOfflineTest.java
new file mode 100644
index 0000000..5a09a68
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/system/BaseDeviceOfflineTest.java
@@ -0,0 +1,123 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.system;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.google.common.collect.ImmutableMap;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.controller.AbstractControllerTest;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+
+public class BaseDeviceOfflineTest extends AbstractControllerTest {
+
+ private Device deviceA;
+ private Device deviceB;
+ private DeviceCredentials credA;
+ private DeviceCredentials credB;
+
+ @Before
+ public void before() throws Exception {
+ loginTenantAdmin();
+ deviceA = createDevice("DevA", "VMS");
+ credA = getCredentials(deviceA.getUuidId());
+ deviceB = createDevice("DevB", "SOLAR");
+ credB = getCredentials(deviceB.getUuidId());
+ }
+
+ @Test
+ public void offlineDevicesCanBeFoundByLastConnectField() throws Exception {
+ makeDeviceContact(credA);
+ Thread.sleep(1000);
+ makeDeviceContact(credB);
+ Thread.sleep(100);
+ List<Device> devices = doGetTyped("/api/device/offline?contactType=CONNECT&threshold=700", new TypeReference<List<Device>>() {
+ });
+
+ assertEquals(devices.toString(),1, devices.size());
+ assertEquals("DevA", devices.get(0).getName());
+ }
+
+ @Test
+ public void offlineDevicesCanBeFoundByLastUpdateField() throws Exception {
+ makeDeviceUpdate(credA);
+ Thread.sleep(1000);
+ makeDeviceUpdate(credB);
+ makeDeviceContact(credA);
+ Thread.sleep(100);
+ List<Device> devices = doGetTyped("/api/device/offline?contactType=UPLOAD&threshold=700", new TypeReference<List<Device>>() {
+ });
+
+ assertEquals(devices.toString(),1, devices.size());
+ assertEquals("DevA", devices.get(0).getName());
+ }
+
+ @Test
+ public void onlineDevicesCanBeFoundByLastConnectField() throws Exception {
+ makeDeviceContact(credB);
+ Thread.sleep(1000);
+ makeDeviceContact(credA);
+ Thread.sleep(100);
+ List<Device> devices = doGetTyped("/api/device/online?contactType=CONNECT&threshold=700", new TypeReference<List<Device>>() {
+ });
+
+ assertEquals(devices.toString(),1, devices.size());
+ assertEquals("DevA", devices.get(0).getName());
+ }
+
+ @Test
+ public void onlineDevicesCanBeFoundByLastUpdateField() throws Exception {
+ makeDeviceUpdate(credB);
+ Thread.sleep(1000);
+ makeDeviceUpdate(credA);
+ makeDeviceContact(credB);
+ Thread.sleep(100);
+ List<Device> devices = doGetTyped("/api/device/online?contactType=UPLOAD&threshold=700", new TypeReference<List<Device>>() {
+ });
+
+ assertEquals(devices.toString(),1, devices.size());
+ assertEquals("DevA", devices.get(0).getName());
+ }
+
+ private Device createDevice(String name, String type) throws Exception {
+ Device device = new Device();
+ device.setName(name);
+ device.setType(type);
+ long currentTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(10);
+ device.setLastConnectTs(currentTime);
+ device.setLastUpdateTs(currentTime);
+ return doPost("/api/device", device, Device.class);
+ }
+
+ private DeviceCredentials getCredentials(UUID deviceId) throws Exception {
+ return doGet("/api/device/" + deviceId.toString() + "/credentials", DeviceCredentials.class);
+ }
+
+ private void makeDeviceUpdate(DeviceCredentials credentials) throws Exception {
+ doPost("/api/v1/" + credentials.getCredentialsId() + "/attributes", ImmutableMap.of("keyA", "valueA"), new String[]{});
+ }
+
+ private void makeDeviceContact(DeviceCredentials credentials) throws Exception {
+ doGet("/api/v1/" + credentials.getCredentialsId() + "/attributes?clientKeys=keyA,keyB,keyC");
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java b/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java
index 4fa6162..c3e87c2 100644
--- a/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java
+++ b/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.system;
+import com.google.common.collect.ImmutableMap;
import org.junit.Before;
import org.junit.Test;
import org.springframework.test.web.servlet.ResultActions;
@@ -28,6 +29,9 @@ import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
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;
@@ -48,6 +52,9 @@ public abstract class BaseHttpDeviceApiTest extends AbstractControllerTest {
device = new Device();
device.setName("My device");
device.setType("default");
+ long currentTime = System.currentTimeMillis();
+ device.setLastConnectTs(currentTime);
+ device.setLastUpdateTs(currentTime);
device = doPost("/api/device", device, Device.class);
deviceCredentials =
@@ -67,6 +74,34 @@ public abstract class BaseHttpDeviceApiTest extends AbstractControllerTest {
doGetAsync("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes?clientKeys=keyA,keyB,keyC").andExpect(status().isOk());
}
+ @Test
+ public void deviceLastContactAndUpdateFieldsAreUpdated() throws Exception {
+ Device actualDevice = doGet("/api/device/" + this.device.getId(), Device.class);
+ Long initConnectTs = actualDevice.getLastConnectTs();
+ Long initUpdateTs = actualDevice.getLastUpdateTs();
+ assertNotNull(initConnectTs);
+ assertNotNull(initUpdateTs);
+ Thread.sleep(50);
+
+ doPost("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes", ImmutableMap.of("keyA", "valueA"), new String[]{});
+ actualDevice = doGet("/api/device/" + this.device.getId(), Device.class);
+ Long postConnectTs = actualDevice.getLastConnectTs();
+ Long postUpdateTs = actualDevice.getLastUpdateTs();
+ System.out.println(postConnectTs + " - " + postUpdateTs + " -> " + (postConnectTs - initConnectTs) + " : " + (postUpdateTs - initUpdateTs));
+ assertTrue(postConnectTs > initConnectTs);
+ assertEquals(postConnectTs, postUpdateTs);
+ Thread.sleep(50);
+
+ doGet("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes?clientKeys=keyA,keyB,keyC");
+ Thread.sleep(50);
+ actualDevice = doGet("/api/device/" + this.device.getId(), Device.class);
+ Long getConnectTs = actualDevice.getLastConnectTs();
+ Long getUpdateTs = actualDevice.getLastUpdateTs();
+ assertTrue(getConnectTs > postConnectTs);
+ assertEquals(getUpdateTs, postUpdateTs);
+
+ }
+
protected ResultActions doGetAsync(String urlTemplate, Object... urlVariables) throws Exception {
MockHttpServletRequestBuilder getRequest;
getRequest = get(urlTemplate, urlVariables);
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
index 70f5042..125406c 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
@@ -22,6 +22,7 @@ import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasName;
+import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -31,7 +32,7 @@ import org.thingsboard.server.common.data.id.TenantId;
@Data
@Builder
@AllArgsConstructor
-public class Alarm extends BaseData<AlarmId> implements HasName {
+public class Alarm extends BaseData<AlarmId> implements HasName, HasTenantId {
private TenantId tenantId;
private String type;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
index cc3c111..c7b246c 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
@@ -17,16 +17,13 @@ package org.thingsboard.server.common.data.asset;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.EqualsAndHashCode;
-import org.thingsboard.server.common.data.HasAdditionalInfo;
-import org.thingsboard.server.common.data.HasName;
-import org.thingsboard.server.common.data.SearchTextBased;
-import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
+import org.thingsboard.server.common.data.*;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
@EqualsAndHashCode(callSuper = true)
-public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements HasName {
+public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements HasName, HasTenantId, HasCustomerId {
private static final long serialVersionUID = 2807343040519543363L;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
index 03115a9..078c97b 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
@@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import com.fasterxml.jackson.databind.JsonNode;
-public class Customer extends ContactBased<CustomerId> implements HasName {
+public class Customer extends ContactBased<CustomerId> implements HasName, HasTenantId {
private static final long serialVersionUID = -1599722990298929275L;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
index a776d7b..7d4e480 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
@@ -37,7 +37,12 @@ public class DataConstants {
public static final String ERROR = "ERROR";
public static final String LC_EVENT = "LC_EVENT";
public static final String STATS = "STATS";
+ public static final String DEBUG_RULE_NODE = "DEBUG_RULE_NODE";
public static final String ONEWAY = "ONEWAY";
public static final String TWOWAY = "TWOWAY";
+
+ public static final String IN = "IN";
+ public static final String OUT = "OUT";
+
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
index 13fa011..6d257fc 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
@@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import com.fasterxml.jackson.databind.JsonNode;
@EqualsAndHashCode(callSuper = true)
-public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implements HasName {
+public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implements HasName, HasTenantId, HasCustomerId {
private static final long serialVersionUID = 2807343040519543363L;
@@ -31,6 +31,8 @@ public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implemen
private CustomerId customerId;
private String name;
private String type;
+ private Long lastConnectTs;
+ private Long lastUpdateTs;
public Device() {
super();
@@ -81,6 +83,22 @@ public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implemen
this.type = type;
}
+ public Long getLastConnectTs() {
+ return lastConnectTs;
+ }
+
+ public void setLastConnectTs(Long lastConnectTs) {
+ this.lastConnectTs = lastConnectTs;
+ }
+
+ public Long getLastUpdateTs() {
+ return lastUpdateTs;
+ }
+
+ public void setLastUpdateTs(Long lastUpdateTs) {
+ this.lastUpdateTs = lastUpdateTs;
+ }
+
@Override
public String getSearchText() {
return getName();
@@ -101,6 +119,10 @@ public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implemen
builder.append(getAdditionalInfo());
builder.append(", createdTime=");
builder.append(createdTime);
+ builder.append(", lastUpdateTs=");
+ builder.append(lastUpdateTs);
+ builder.append(", lastConnectTs=");
+ builder.append(lastConnectTs);
builder.append(", id=");
builder.append(id);
builder.append("]");
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceStatusQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceStatusQuery.java
new file mode 100644
index 0000000..0d0dad1
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceStatusQuery.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.device;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.ToString;
+
+@Data
+@AllArgsConstructor
+@ToString
+public class DeviceStatusQuery {
+
+ private Status status;
+ private ContactType contactType;
+ private long threshold;
+
+
+ public enum Status {
+ ONLINE, OFFLINE
+ }
+
+ public enum ContactType {
+ CONNECT, UPLOAD
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
index 76b3e33..31c1cda 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
@@ -33,6 +33,10 @@ public class EntityIdFactory {
return getByTypeAndUuid(EntityType.valueOf(type), uuid);
}
+ public static EntityId getByTypeAndUuid(EntityType type, String uuid) {
+ return getByTypeAndUuid(type, UUID.fromString(uuid));
+ }
+
public static EntityId getByTypeAndUuid(EntityType type, UUID uuid) {
switch (type) {
case TENANT:
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java
index 34f8c3a..ffd7822 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java
@@ -20,6 +20,7 @@ import java.util.List;
import java.util.NoSuchElementException;
import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UUIDBased;
public class PageDataIterable<T extends SearchTextBased<? extends UUIDBased>> implements Iterable<T>, Iterator<T> {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java
index 45fb590..a103064 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java
@@ -20,6 +20,6 @@ package org.thingsboard.server.common.data.plugin;
*/
public enum ComponentType {
- FILTER, PROCESSOR, ACTION, PLUGIN
+ ENRICHMENT, FILTER, TRANSFORMATION, ACTION, OLD_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
index 8576264..4c33ffe 100644
--- 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
@@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.HasName;
+import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
import org.thingsboard.server.common.data.id.PluginId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -32,7 +33,7 @@ import java.io.IOException;
@EqualsAndHashCode(callSuper = true)
@Slf4j
-public class PluginMetaData extends SearchTextBasedWithAdditionalInfo<PluginId> implements HasName {
+public class PluginMetaData extends SearchTextBasedWithAdditionalInfo<PluginId> implements HasName, HasTenantId {
private static final long serialVersionUID = 1L;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java
new file mode 100644
index 0000000..0c9fd5f
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.rule;
+
+import lombok.Data;
+
+/**
+ * Created by ashvayka on 21.03.18.
+ */
+@Data
+public class NodeConnectionInfo {
+ private int fromIndex;
+ private int toIndex;
+ private String type;
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java
index e82c850..218061a 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java
@@ -21,6 +21,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.HasName;
+import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
@@ -29,7 +30,7 @@ import org.thingsboard.server.common.data.id.TenantId;
@Data
@EqualsAndHashCode(callSuper = true)
@Slf4j
-public class RuleChain extends SearchTextBasedWithAdditionalInfo<RuleChainId> implements HasName {
+public class RuleChain extends SearchTextBasedWithAdditionalInfo<RuleChainId> implements HasName, HasTenantId {
private static final long serialVersionUID = -5656679015121935465L;
@@ -37,6 +38,7 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo<RuleChainId> im
private String name;
private RuleNodeId firstRuleNodeId;
private boolean root;
+ private boolean debugMode;
private transient JsonNode configuration;
@JsonIgnore
private byte[] configurationBytes;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java
new file mode 100644
index 0000000..a537fe4
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.rule;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.RuleChainId;
+
+/**
+ * Created by ashvayka on 21.03.18.
+ */
+@Data
+public class RuleChainConnectionInfo {
+ private int fromIndex;
+ private RuleChainId targetRuleChainId;
+ private JsonNode additionalInfo;
+ private String type;
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java
index af141d6..7ecd6df 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data.rule;
+import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import org.thingsboard.server.common.data.id.RuleChainId;
@@ -47,29 +48,15 @@ public class RuleChainMetaData {
}
connections.add(connectionInfo);
}
- public void addRuleChainConnectionInfo(int fromIndex, RuleChainId targetRuleChainId, String type) {
+ public void addRuleChainConnectionInfo(int fromIndex, RuleChainId targetRuleChainId, String type, JsonNode additionalInfo) {
RuleChainConnectionInfo connectionInfo = new RuleChainConnectionInfo();
connectionInfo.setFromIndex(fromIndex);
connectionInfo.setTargetRuleChainId(targetRuleChainId);
connectionInfo.setType(type);
+ connectionInfo.setAdditionalInfo(additionalInfo);
if (ruleChainConnections == null) {
ruleChainConnections = new ArrayList<>();
}
ruleChainConnections.add(connectionInfo);
}
-
- @Data
- public class NodeConnectionInfo {
- private int fromIndex;
- private int toIndex;
- private String type;
- }
-
- @Data
- public class RuleChainConnectionInfo {
- private int fromIndex;
- private RuleChainId targetRuleChainId;
- private String type;
- }
-
}
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
index 98adeb7..953e5eb 100644
--- 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
@@ -23,6 +23,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.HasName;
+import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
import org.thingsboard.server.common.data.id.RuleId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -31,7 +32,7 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
@Data
@EqualsAndHashCode(callSuper = true)
@Slf4j
-public class RuleMetaData extends SearchTextBasedWithAdditionalInfo<RuleId> implements HasName {
+public class RuleMetaData extends SearchTextBasedWithAdditionalInfo<RuleId> implements HasName, HasTenantId {
private static final long serialVersionUID = -5656679015122935465L;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
index d044000..fbc1103 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
@@ -34,6 +34,7 @@ public class RuleNode extends SearchTextBasedWithAdditionalInfo<RuleNodeId> impl
private String type;
private String name;
+ private boolean debugMode;
private transient JsonNode configuration;
@JsonIgnore
private byte[] configurationBytes;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/User.java b/common/data/src/main/java/org/thingsboard/server/common/data/User.java
index c893d64..15c52dc 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/User.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/User.java
@@ -15,9 +15,11 @@
*/
package org.thingsboard.server.common.data;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
@@ -25,7 +27,7 @@ import org.thingsboard.server.common.data.security.Authority;
import com.fasterxml.jackson.databind.JsonNode;
@EqualsAndHashCode(callSuper = true)
-public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements HasName {
+public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements HasName, HasTenantId, HasCustomerId {
private static final long serialVersionUID = 8250339805336035966L;
@@ -138,4 +140,18 @@ public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements H
return builder.toString();
}
+ @JsonIgnore
+ public boolean isSystemAdmin() {
+ return tenantId == null || EntityId.NULL_UUID.equals(tenantId.getId());
+ }
+
+ @JsonIgnore
+ public boolean isTenantAdmin() {
+ return !isSystemAdmin() && (customerId == null || EntityId.NULL_UUID.equals(customerId.getId()));
+ }
+
+ @JsonIgnore
+ public boolean isCustomerUser() {
+ return !isSystemAdmin() && !isTenantAdmin();
+ }
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java
index ace51c0..87708a7 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java
@@ -16,16 +16,16 @@
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.FromDeviceRequestMsg;
import org.thingsboard.server.common.msg.session.MsgType;
/**
* @author Andrew Shvayka
*/
@Data
-public class ToServerRpcRequestMsg implements FromDeviceMsg {
+public class ToServerRpcRequestMsg implements FromDeviceRequestMsg {
- private final int requestId;
+ private final Integer requestId;
private final String method;
private final String params;
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
new file mode 100644
index 0000000..f8f2044
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg;
+
+/**
+ * Created by ashvayka on 15.03.18.
+ */
+public enum MsgType {
+
+ /**
+ * ADDED/UPDATED/DELETED events for main entities.
+ *
+ * @See {@link org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg}
+ */
+ COMPONENT_LIFE_CYCLE_MSG,
+
+ /**
+ * Misc messages from the REST API/SERVICE layer to the new rule engine.
+ *
+ * @See {@link org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg}
+ */
+ SERVICE_TO_RULE_ENGINE_MSG,
+
+
+ SESSION_TO_DEVICE_ACTOR_MSG,
+ DEVICE_ACTOR_TO_SESSION_MSG,
+
+
+ /**
+ * Message that is sent by RuleChainActor to RuleActor with command to process TbMsg.
+ */
+ RULE_CHAIN_TO_RULE_MSG,
+
+ /**
+ * Message that is sent by RuleActor to RuleChainActor with command to process TbMsg by next nodes in chain.
+ */
+ RULE_TO_RULE_CHAIN_TELL_NEXT_MSG,
+
+ /**
+ * Message that is sent by RuleActor implementation to RuleActor itself to log the error.
+ */
+ RULE_TO_SELF_ERROR_MSG,
+
+ /**
+ * Message that is sent by RuleActor implementation to RuleActor itself to process the message.
+ */
+ RULE_TO_SELF_MSG,
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java
index d48c3fe..c104281 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java
@@ -15,14 +15,14 @@
*/
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.EntityType;
+import org.thingsboard.server.common.data.id.*;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
-import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
@@ -32,34 +32,34 @@ import java.util.Optional;
* @author Andrew Shvayka
*/
@ToString
-public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg {
+public class ComponentLifecycleMsg implements TbActorMsg, TenantAwareMsg, ToAllNodesMsg {
@Getter
private final TenantId tenantId;
- private final PluginId pluginId;
- private final RuleId ruleId;
+ @Getter
+ private final EntityId entityId;
@Getter
private final ComponentLifecycleEvent event;
- public static ComponentLifecycleMsg forPlugin(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent event) {
- return new ComponentLifecycleMsg(tenantId, pluginId, null, event);
- }
-
- public static ComponentLifecycleMsg forRule(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent event) {
- return new ComponentLifecycleMsg(tenantId, null, ruleId, event);
- }
-
- private ComponentLifecycleMsg(TenantId tenantId, PluginId pluginId, RuleId ruleId, ComponentLifecycleEvent event) {
+ public ComponentLifecycleMsg(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent event) {
this.tenantId = tenantId;
- this.pluginId = pluginId;
- this.ruleId = ruleId;
+ this.entityId = entityId;
this.event = event;
}
public Optional<PluginId> getPluginId() {
- return Optional.ofNullable(pluginId);
+ return entityId.getEntityType() == EntityType.PLUGIN ? Optional.of((PluginId) entityId) : Optional.empty();
}
public Optional<RuleId> getRuleId() {
- return Optional.ofNullable(ruleId);
+ return entityId.getEntityType() == EntityType.RULE ? Optional.of((RuleId) entityId) : Optional.empty();
+ }
+
+ public Optional<RuleChainId> getRuleChainId() {
+ return entityId.getEntityType() == EntityType.RULE_CHAIN ? Optional.of((RuleChainId) entityId) : Optional.empty();
+ }
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.COMPONENT_LIFE_CYCLE_MSG;
}
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/system/ServiceToRuleEngineMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/system/ServiceToRuleEngineMsg.java
new file mode 100644
index 0000000..0792b63
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/system/ServiceToRuleEngineMsg.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.system;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 15.03.18.
+ */
+@Data
+public final class ServiceToRuleEngineMsg implements TbActorMsg {
+
+ private final TenantId tenantId;
+ private final TbMsg tbMsg;
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.SERVICE_TO_RULE_ENGINE_MSG;
+ }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
index 5163b6c..1c7de3b 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.common.msg;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
+import lombok.AllArgsConstructor;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
@@ -30,14 +31,24 @@ import java.util.UUID;
* Created by ashvayka on 13.01.18.
*/
@Data
+@AllArgsConstructor
public final class TbMsg implements Serializable {
private final UUID id;
private final String type;
private final EntityId originator;
private final TbMsgMetaData metaData;
+ private final TbMsgDataType dataType;
+ private final String data;
- private final byte[] data;
+ public TbMsg(UUID id, String type, EntityId originator, TbMsgMetaData metaData, String data) {
+ this.id = id;
+ this.type = type;
+ this.originator = originator;
+ this.metaData = metaData;
+ this.dataType = TbMsgDataType.JSON;
+ this.data = data;
+ }
public static ByteBuffer toBytes(TbMsg msg) {
MsgProtos.TbMsgProto.Builder builder = MsgProtos.TbMsgProto.newBuilder();
@@ -49,12 +60,11 @@ public final class TbMsg implements Serializable {
}
if (msg.getMetaData() != null) {
- MsgProtos.TbMsgProto.TbMsgMetaDataProto.Builder metadataBuilder = MsgProtos.TbMsgProto.TbMsgMetaDataProto.newBuilder();
- metadataBuilder.putAllData(msg.getMetaData().getData());
- builder.addMetaData(metadataBuilder.build());
+ builder.setMetaData(MsgProtos.TbMsgMetaDataProto.newBuilder().putAllData(msg.getMetaData().getData()).build());
}
- builder.setData(ByteString.copyFrom(msg.getData()));
+ builder.setDataType(msg.getDataType().ordinal());
+ builder.setData(msg.getData());
byte[] bytes = builder.build().toByteArray();
return ByteBuffer.wrap(bytes);
}
@@ -62,19 +72,16 @@ public final class TbMsg implements Serializable {
public static TbMsg fromBytes(ByteBuffer buffer) {
try {
MsgProtos.TbMsgProto proto = MsgProtos.TbMsgProto.parseFrom(buffer.array());
- TbMsgMetaData metaData = new TbMsgMetaData();
- if (proto.getMetaDataCount() > 0) {
- metaData.setData(proto.getMetaData(0).getDataMap());
- }
-
- EntityId entityId = null;
- if (proto.getEntityId() != null) {
- entityId = EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId());
- }
-
- return new TbMsg(UUID.fromString(proto.getId()), proto.getType(), entityId, metaData, proto.getData().toByteArray());
+ TbMsgMetaData metaData = new TbMsgMetaData(proto.getMetaData().getDataMap());
+ EntityId entityId = EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId());
+ TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()];
+ return new TbMsg(UUID.fromString(proto.getId()), proto.getType(), entityId, metaData, dataType, proto.getData());
} catch (InvalidProtocolBufferException e) {
throw new IllegalStateException("Could not parse protobuf for TbMsg", e);
}
}
+
+ public TbMsg copy() {
+ return new TbMsg(id, type, originator, metaData.copy(), dataType, data);
+ }
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java
index 1bbc792..4b7314c 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java
@@ -15,9 +15,12 @@
*/
package org.thingsboard.server.common.msg;
+import lombok.AllArgsConstructor;
import lombok.Data;
+import lombok.NoArgsConstructor;
import java.io.Serializable;
+import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -25,10 +28,15 @@ import java.util.concurrent.ConcurrentHashMap;
* Created by ashvayka on 13.01.18.
*/
@Data
+@NoArgsConstructor
public final class TbMsgMetaData implements Serializable {
private Map<String, String> data = new ConcurrentHashMap<>();
+ public TbMsgMetaData(Map<String, String> data) {
+ this.data = data;
+ }
+
public String getValue(String key) {
return data.get(key);
}
@@ -37,4 +45,11 @@ public final class TbMsgMetaData implements Serializable {
data.put(key, value);
}
+ public Map<String, String> values() {
+ return new HashMap<>(data);
+ }
+
+ public TbMsgMetaData copy() {
+ return new TbMsgMetaData(new ConcurrentHashMap<>(data));
+ }
}
diff --git a/common/message/src/main/proto/tbmsg.proto b/common/message/src/main/proto/tbmsg.proto
index 90fa2bd..4ce1fb6 100644
--- a/common/message/src/main/proto/tbmsg.proto
+++ b/common/message/src/main/proto/tbmsg.proto
@@ -19,6 +19,9 @@ package msgqueue;
option java_package = "org.thingsboard.server.common.msg.gen";
option java_outer_classname = "MsgProtos";
+message TbMsgMetaDataProto {
+ map<string, string> data = 1;
+}
message TbMsgProto {
string id = 1;
@@ -26,11 +29,8 @@ message TbMsgProto {
string entityType = 3;
string entityId = 4;
- message TbMsgMetaDataProto {
- map<string, string> data = 1;
- }
+ TbMsgMetaDataProto metaData = 5;
- repeated TbMsgMetaDataProto metaData = 5;
-
- bytes data = 6;
+ int32 dataType = 6;
+ string data = 7;
}
\ No newline at end of file
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java
index 8d254a0..3782ed2 100644
--- a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java
@@ -61,13 +61,14 @@ public class HostRequestIntervalRegistry {
}
public long tick(String clientHostId) {
+ IntervalCount intervalCount = hostCounts.computeIfAbsent(clientHostId, s -> new IntervalCount(intervalDurationMs));
+ long currentCount = intervalCount.resetIfExpiredAndTick();
if (whiteList.contains(clientHostId)) {
return 0;
} else if (blackList.contains(clientHostId)) {
return Long.MAX_VALUE;
}
- IntervalCount intervalCount = hostCounts.computeIfAbsent(clientHostId, s -> new IntervalCount(intervalDurationMs));
- return intervalCount.resetIfExpiredAndTick();
+ return currentCount;
}
public void clean() {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java
index 4f923fe..64ec718 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java
@@ -148,7 +148,7 @@ public class CassandraAssetDao extends CassandraAbstractSearchTextDao<AssetEntit
query.and(eq(ENTITY_SUBTYPE_TENANT_ID_PROPERTY, tenantId));
query.and(eq(ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY, EntityType.ASSET));
query.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
- ResultSetFuture resultSetFuture = getSession().executeAsync(query);
+ ResultSetFuture resultSetFuture = executeAsyncRead(query);
return Futures.transform(resultSetFuture, new Function<ResultSet, List<EntitySubtype>>() {
@Nullable
@Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java
index 932d6b9..8ae9dc8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java
@@ -147,12 +147,12 @@ public class CassandraBaseAttributesDao extends CassandraAbstractAsyncDao implem
.and(eq(ATTRIBUTE_TYPE_COLUMN, attributeType))
.and(eq(ATTRIBUTE_KEY_COLUMN, key));
log.debug("Remove request: {}", delete.toString());
- return getFuture(getSession().executeAsync(delete), rs -> null);
+ return getFuture(executeAsyncWrite(delete), rs -> null);
}
private PreparedStatement getSaveStmt() {
if (saveStmt == null) {
- saveStmt = getSession().prepare("INSERT INTO " + ModelConstants.ATTRIBUTES_KV_CF +
+ saveStmt = prepare("INSERT INTO " + ModelConstants.ATTRIBUTES_KV_CF +
"(" + ENTITY_TYPE_COLUMN +
"," + ENTITY_ID_COLUMN +
"," + ATTRIBUTE_TYPE_COLUMN +
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
index 23fadeb..2a49130 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
@@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.id.*;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.audit.sink.AuditLogSink;
import org.thingsboard.server.dao.entity.EntityService;
@@ -158,11 +159,20 @@ public class AuditLogServiceImpl implements AuditLogService {
switch(actionType) {
case ADDED:
case UPDATED:
- ObjectNode entityNode = objectMapper.valueToTree(entity);
- if (entityId.getEntityType() == EntityType.DASHBOARD) {
- entityNode.put("configuration", "");
+ if (entity != null) {
+ ObjectNode entityNode = objectMapper.valueToTree(entity);
+ if (entityId.getEntityType() == EntityType.DASHBOARD) {
+ entityNode.put("configuration", "");
+ }
+ actionData.set("entity", entityNode);
+ }
+ if (entityId.getEntityType() == EntityType.RULE_CHAIN) {
+ RuleChainMetaData ruleChainMetaData = extractParameter(RuleChainMetaData.class, additionalInfo);
+ if (ruleChainMetaData != null) {
+ ObjectNode ruleChainMetaDataNode = objectMapper.valueToTree(ruleChainMetaData);
+ actionData.set("metadata", ruleChainMetaDataNode);
+ }
}
- actionData.set("entity", entityNode);
break;
case DELETED:
case ACTIVATED:
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
index 27f7adc..fd02b5f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
@@ -244,12 +244,12 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
values.add("?");
}
String statementString = INSERT_INTO + cfName + " (" + String.join(",", columnsList) + ") VALUES (" + values.toString() + ")";
- return getSession().prepare(statementString);
+ return prepare(statementString);
}
private PreparedStatement getPartitionInsertStmt() {
if (partitionInsertStmt == null) {
- partitionInsertStmt = getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF +
+ partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF +
"(" + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY +
"," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" +
" VALUES(?, ?)");
@@ -343,7 +343,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
.where(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId));
select.and(QueryBuilder.gte(ModelConstants.PARTITION_COLUMN, minPartition));
select.and(QueryBuilder.lte(ModelConstants.PARTITION_COLUMN, maxPartition));
- return getSession().execute(select);
+ return executeRead(select);
}
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/CassandraBaseComponentDescriptorDao.java b/dao/src/main/java/org/thingsboard/server/dao/component/CassandraBaseComponentDescriptorDao.java
index 5e03545..b5b9f15 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/component/CassandraBaseComponentDescriptorDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/component/CassandraBaseComponentDescriptorDao.java
@@ -130,7 +130,7 @@ public class CassandraBaseComponentDescriptorDao extends CassandraAbstractSearch
public boolean removeById(UUID key) {
Statement delete = QueryBuilder.delete().all().from(ModelConstants.COMPONENT_DESCRIPTOR_BY_ID).where(eq(ModelConstants.ID_PROPERTY, key));
log.debug("Remove request: {}", delete.toString());
- return getSession().execute(delete).wasApplied();
+ return executeWrite(delete).wasApplied();
}
@Override
@@ -145,7 +145,7 @@ public class CassandraBaseComponentDescriptorDao extends CassandraAbstractSearch
log.debug("Delete plugin meta-data entity by id [{}]", clazz);
Statement delete = QueryBuilder.delete().all().from(getColumnFamilyName()).where(eq(ModelConstants.COMPONENT_DESCRIPTOR_CLASS_PROPERTY, clazz));
log.debug("Remove request: {}", delete.toString());
- ResultSet resultSet = getSession().execute(delete);
+ ResultSet resultSet = executeWrite(delete);
log.debug("Delete result: [{}]", resultSet.wasApplied());
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java
index ac72ae8..0246da5 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java
@@ -15,9 +15,9 @@
*/
package org.thingsboard.server.dao.device;
-import com.datastax.driver.core.ResultSet;
-import com.datastax.driver.core.ResultSetFuture;
-import com.datastax.driver.core.Statement;
+import com.datastax.driver.core.*;
+import com.datastax.driver.core.querybuilder.Clause;
+import com.datastax.driver.core.querybuilder.QueryBuilder;
import com.datastax.driver.core.querybuilder.Select;
import com.datastax.driver.mapping.Result;
import com.google.common.base.Function;
@@ -28,9 +28,11 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.device.DeviceStatusQuery;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.EntitySubtypeEntity;
+import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.nosql.DeviceEntity;
import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao;
import org.thingsboard.server.dao.util.NoSqlDao;
@@ -148,7 +150,7 @@ public class CassandraDeviceDao extends CassandraAbstractSearchTextDao<DeviceEnt
query.and(eq(ENTITY_SUBTYPE_TENANT_ID_PROPERTY, tenantId));
query.and(eq(ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY, EntityType.DEVICE));
query.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
- ResultSetFuture resultSetFuture = getSession().executeAsync(query);
+ ResultSetFuture resultSetFuture = executeAsyncRead(query);
return Futures.transform(resultSetFuture, new Function<ResultSet, List<EntitySubtype>>() {
@Nullable
@Override
@@ -157,7 +159,7 @@ public class CassandraDeviceDao extends CassandraAbstractSearchTextDao<DeviceEnt
if (result != null) {
List<EntitySubtype> entitySubtypes = new ArrayList<>();
result.all().forEach((entitySubtypeEntity) ->
- entitySubtypes.add(entitySubtypeEntity.toEntitySubtype())
+ entitySubtypes.add(entitySubtypeEntity.toEntitySubtype())
);
return entitySubtypes;
} else {
@@ -167,4 +169,68 @@ public class CassandraDeviceDao extends CassandraAbstractSearchTextDao<DeviceEnt
});
}
+ @Override
+ public ListenableFuture<List<Device>> findDevicesByTenantIdAndStatus(UUID tenantId, DeviceStatusQuery statusQuery) {
+ log.debug("Try to find [{}] devices by tenantId [{}]", statusQuery.getStatus(), tenantId);
+
+ Select select = select().from(DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME).allowFiltering();
+ Select.Where query = select.where();
+ query.and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId));
+ Clause clause = statusClause(statusQuery);
+ query.and(clause);
+ return findListByStatementAsync(query);
+ }
+
+ @Override
+ public ListenableFuture<List<Device>> findDevicesByTenantIdTypeAndStatus(UUID tenantId, String type, DeviceStatusQuery statusQuery) {
+ log.debug("Try to find [{}] devices by tenantId [{}] and type [{}]", statusQuery.getStatus(), tenantId, type);
+
+ Select select = select().from(DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME).allowFiltering();
+ Select.Where query = select.where()
+ .and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId))
+ .and(eq(DEVICE_TYPE_PROPERTY, type));
+
+ query.and(statusClause(statusQuery));
+ return findListByStatementAsync(query);
+ }
+
+
+ @Override
+ public void saveDeviceStatus(Device device) {
+ PreparedStatement statement = prepare("insert into " +
+ "device (id, tenant_id, customer_id, type, last_connect, last_update) values (?, ?, ?, ?, ?, ?)");
+ BoundStatement boundStatement = statement.bind(device.getUuidId(), device.getTenantId().getId(), device.getCustomerId().getId(),
+ device.getType(), device.getLastConnectTs(), device.getLastUpdateTs());
+ ResultSetFuture resultSetFuture = executeAsyncWrite(boundStatement);
+ Futures.withFallback(resultSetFuture, t -> {
+ log.error("Can't update device status for [{}]", device, t);
+ throw new IllegalArgumentException("Can't update device status for {" + device + "}", t);
+ });
+ }
+
+ private String getStatusProperty(DeviceStatusQuery statusQuery) {
+ switch (statusQuery.getContactType()) {
+ case UPLOAD:
+ return DEVICE_LAST_UPDATE_PROPERTY;
+ case CONNECT:
+ return DEVICE_LAST_CONNECT_PROPERTY;
+ }
+ return null;
+ }
+
+ private Clause statusClause(DeviceStatusQuery statusQuery) {
+ long minTime = System.currentTimeMillis() - statusQuery.getThreshold();
+ String statusProperty = getStatusProperty(statusQuery);
+ if (statusProperty != null) {
+ switch (statusQuery.getStatus()) {
+ case ONLINE:
+ return gt(statusProperty, minTime);
+ case OFFLINE:
+ return lt(statusProperty, minTime);
+ }
+ }
+ log.error("Could not build status query from [{}]", statusQuery);
+ throw new IllegalStateException("Could not build status query for device []");
+ }
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
index dbc098e..2b9e522 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.dao.device;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype;
+import org.thingsboard.server.common.data.device.DeviceStatusQuery;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.Dao;
@@ -27,7 +28,6 @@ import java.util.UUID;
/**
* The Interface DeviceDao.
- *
*/
public interface DeviceDao extends Dao<Device> {
@@ -52,7 +52,7 @@ public interface DeviceDao extends Dao<Device> {
* Find devices by tenantId, type and page link.
*
* @param tenantId the tenantId
- * @param type the type
+ * @param type the type
* @param pageLink the page link
* @return the list of device objects
*/
@@ -61,7 +61,7 @@ public interface DeviceDao extends Dao<Device> {
/**
* Find devices by tenantId and devices Ids.
*
- * @param tenantId the tenantId
+ * @param tenantId the tenantId
* @param deviceIds the device Ids
* @return the list of device objects
*/
@@ -70,9 +70,9 @@ public interface DeviceDao extends Dao<Device> {
/**
* Find devices by tenantId, customerId and page link.
*
- * @param tenantId the tenantId
+ * @param tenantId the tenantId
* @param customerId the customerId
- * @param pageLink the page link
+ * @param pageLink the page link
* @return the list of device objects
*/
List<Device> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink);
@@ -80,10 +80,10 @@ public interface DeviceDao extends Dao<Device> {
/**
* Find devices by tenantId, customerId, type and page link.
*
- * @param tenantId the tenantId
+ * @param tenantId the tenantId
* @param customerId the customerId
- * @param type the type
- * @param pageLink the page link
+ * @param type the type
+ * @param pageLink the page link
* @return the list of device objects
*/
List<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink);
@@ -92,9 +92,9 @@ public interface DeviceDao extends Dao<Device> {
/**
* Find devices by tenantId, customerId and devices Ids.
*
- * @param tenantId the tenantId
+ * @param tenantId the tenantId
* @param customerId the customerId
- * @param deviceIds the device Ids
+ * @param deviceIds the device Ids
* @return the list of device objects
*/
ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds);
@@ -103,7 +103,7 @@ public interface DeviceDao extends Dao<Device> {
* Find devices by tenantId and device name.
*
* @param tenantId the tenantId
- * @param name the device name
+ * @param name the device name
* @return the optional device object
*/
Optional<Device> findDeviceByTenantIdAndName(UUID tenantId, String name);
@@ -114,4 +114,31 @@ public interface DeviceDao extends Dao<Device> {
* @return the list of tenant device type objects
*/
ListenableFuture<List<EntitySubtype>> findTenantDeviceTypesAsync(UUID tenantId);
+
+ /**
+ * Find devices by tenantId, statusQuery and page link.
+ *
+ * @param tenantId the tenantId
+ * @param statusQuery the page link
+ * @return the list of device objects
+ */
+ ListenableFuture<List<Device>> findDevicesByTenantIdAndStatus(UUID tenantId, DeviceStatusQuery statusQuery);
+
+ /**
+ * Find devices by tenantId, type, statusQuery and page link.
+ *
+ * @param tenantId the tenantId
+ * @param type the type
+ * @param statusQuery the page link
+ * @return the list of device objects
+ */
+ ListenableFuture<List<Device>> findDevicesByTenantIdTypeAndStatus(UUID tenantId, String type, DeviceStatusQuery statusQuery);
+
+
+ /**
+ * Update device last contact and update timestamp async
+ *
+ * @param device the device object
+ */
+ void saveDeviceStatus(Device device);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceOfflineService.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceOfflineService.java
new file mode 100644
index 0000000..3bf3662
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceOfflineService.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.device;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.device.DeviceStatusQuery;
+
+import java.util.List;
+import java.util.UUID;
+
+public interface DeviceOfflineService {
+
+ void online(Device device, boolean isUpdate);
+
+ void offline(Device device);
+
+ ListenableFuture<List<Device>> findOfflineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold);
+
+ ListenableFuture<List<Device>> findOnlineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold);
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceOfflineServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceOfflineServiceImpl.java
new file mode 100644
index 0000000..f4d8e61
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceOfflineServiceImpl.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.device;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.device.DeviceStatusQuery;
+
+import java.util.List;
+import java.util.UUID;
+
+import static org.thingsboard.server.common.data.device.DeviceStatusQuery.Status.OFFLINE;
+import static org.thingsboard.server.common.data.device.DeviceStatusQuery.Status.ONLINE;
+
+@Service
+public class DeviceOfflineServiceImpl implements DeviceOfflineService {
+
+ @Autowired
+ private DeviceDao deviceDao;
+
+ @Override
+ public void online(Device device, boolean isUpdate) {
+ long current = System.currentTimeMillis();
+ device.setLastConnectTs(current);
+ if(isUpdate) {
+ device.setLastUpdateTs(current);
+ }
+ deviceDao.saveDeviceStatus(device);
+ }
+
+ @Override
+ public void offline(Device device) {
+ online(device, false);
+ }
+
+ @Override
+ public ListenableFuture<List<Device>> findOfflineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold) {
+ DeviceStatusQuery statusQuery = new DeviceStatusQuery(OFFLINE, contactType, offlineThreshold);
+ return deviceDao.findDevicesByTenantIdAndStatus(tenantId, statusQuery);
+ }
+
+ @Override
+ public ListenableFuture<List<Device>> findOnlineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold) {
+ DeviceStatusQuery statusQuery = new DeviceStatusQuery(ONLINE, contactType, offlineThreshold);
+ return deviceDao.findDevicesByTenantIdAndStatus(tenantId, statusQuery);
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
index a159b9e..52b15ef 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
@@ -133,6 +133,8 @@ public class ModelConstants {
public static final String DEVICE_NAME_PROPERTY = "name";
public static final String DEVICE_TYPE_PROPERTY = "type";
public static final String DEVICE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
+ public static final String DEVICE_LAST_CONNECT_PROPERTY = "last_connect";
+ public static final String DEVICE_LAST_UPDATE_PROPERTY = "last_update";
public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text";
public static final String DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_by_type_and_search_text";
@@ -332,6 +334,8 @@ public class ModelConstants {
public static final String EVENT_BY_TYPE_AND_ID_VIEW_NAME = "event_by_type_and_id";
public static final String EVENT_BY_ID_VIEW_NAME = "event_by_id";
+ public static final String DEBUG_MODE = "debug_mode";
+
/**
* Cassandra rule chain constants.
*/
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
index ac90cb7..ab2e3bc 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
@@ -36,7 +36,7 @@ import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.*;
-@Table(name = AUDIT_LOG_COLUMN_FAMILY_NAME)
+@Table(name = AUDIT_LOG_BY_ENTITY_ID_CF)
@Data
@NoArgsConstructor
public class AuditLogEntity implements BaseEntity<AuditLog> {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java
index ef0c5fe..7458e56 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java
@@ -63,6 +63,12 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
@Column(name = DEVICE_ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class)
private JsonNode additionalInfo;
+ @Column(name = DEVICE_LAST_CONNECT_PROPERTY)
+ private Long lastConnectTs;
+
+ @Column(name = DEVICE_LAST_UPDATE_PROPERTY)
+ private Long lastUpdateTs;
+
public DeviceEntity() {
super();
}
@@ -80,6 +86,8 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
this.name = device.getName();
this.type = device.getType();
this.additionalInfo = device.getAdditionalInfo();
+ this.lastConnectTs = device.getLastConnectTs();
+ this.lastUpdateTs = device.getLastUpdateTs();
}
public UUID getId() {
@@ -129,7 +137,23 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
public void setAdditionalInfo(JsonNode additionalInfo) {
this.additionalInfo = additionalInfo;
}
-
+
+ public Long getLastConnectTs() {
+ return lastConnectTs;
+ }
+
+ public void setLastConnectTs(Long lastConnectTs) {
+ this.lastConnectTs = lastConnectTs;
+ }
+
+ public Long getLastUpdateTs() {
+ return lastUpdateTs;
+ }
+
+ public void setLastUpdateTs(Long lastUpdateTs) {
+ this.lastUpdateTs = lastUpdateTs;
+ }
+
@Override
public String getSearchTextSource() {
return getName();
@@ -157,6 +181,8 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
device.setName(name);
device.setType(type);
device.setAdditionalInfo(additionalInfo);
+ device.setLastConnectTs(lastConnectTs);
+ device.setLastUpdateTs(lastUpdateTs);
return device;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleChainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleChainEntity.java
index 34659a8..251a689 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleChainEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleChainEntity.java
@@ -22,6 +22,8 @@ import com.datastax.driver.mapping.annotations.PartitionKey;
import com.datastax.driver.mapping.annotations.Table;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
import lombok.ToString;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
@@ -54,6 +56,10 @@ public class RuleChainEntity implements SearchTextEntity<RuleChain> {
private UUID firstRuleNodeId;
@Column(name = RULE_CHAIN_ROOT_PROPERTY)
private boolean root;
+ @Getter
+ @Setter
+ @Column(name = DEBUG_MODE)
+ private boolean debugMode;
@Column(name = RULE_CHAIN_CONFIGURATION_PROPERTY, codec = JsonCodec.class)
private JsonNode configuration;
@Column(name = ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class)
@@ -71,6 +77,7 @@ public class RuleChainEntity implements SearchTextEntity<RuleChain> {
this.searchText = ruleChain.getName();
this.firstRuleNodeId = DaoUtil.getId(ruleChain.getFirstRuleNodeId());
this.root = ruleChain.isRoot();
+ this.debugMode = ruleChain.isDebugMode();
this.configuration = ruleChain.getConfiguration();
this.additionalInfo = ruleChain.getAdditionalInfo();
}
@@ -157,6 +164,7 @@ public class RuleChainEntity implements SearchTextEntity<RuleChain> {
ruleChain.setFirstRuleNodeId(new RuleNodeId(this.firstRuleNodeId));
}
ruleChain.setRoot(this.root);
+ ruleChain.setDebugMode(this.debugMode);
ruleChain.setConfiguration(this.configuration);
ruleChain.setAdditionalInfo(this.additionalInfo);
return ruleChain;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleNodeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleNodeEntity.java
index ba96e4b..8d3f3c3 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleNodeEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleNodeEntity.java
@@ -21,6 +21,8 @@ import com.datastax.driver.mapping.annotations.PartitionKey;
import com.datastax.driver.mapping.annotations.Table;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
import lombok.ToString;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.rule.RuleNode;
@@ -49,6 +51,11 @@ public class RuleNodeEntity implements SearchTextEntity<RuleNode> {
private JsonNode configuration;
@Column(name = ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class)
private JsonNode additionalInfo;
+ @Getter
+ @Setter
+ @Column(name = DEBUG_MODE)
+ private boolean debugMode;
+
public RuleNodeEntity() {
}
@@ -59,6 +66,7 @@ public class RuleNodeEntity implements SearchTextEntity<RuleNode> {
}
this.type = ruleNode.getType();
this.name = ruleNode.getName();
+ this.debugMode = ruleNode.isDebugMode();
this.searchText = ruleNode.getName();
this.configuration = ruleNode.getConfiguration();
this.additionalInfo = ruleNode.getAdditionalInfo();
@@ -126,6 +134,7 @@ public class RuleNodeEntity implements SearchTextEntity<RuleNode> {
ruleNode.setCreatedTime(UUIDs.unixTimestamp(id));
ruleNode.setType(this.type);
ruleNode.setName(this.name);
+ ruleNode.setDebugMode(this.debugMode);
ruleNode.setConfiguration(this.configuration);
ruleNode.setAdditionalInfo(this.additionalInfo);
return ruleNode;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java
index 7aaf0ae..e831c6e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java
@@ -34,6 +34,9 @@ import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_LAST_CONNECT_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_LAST_UPDATE_PROPERTY;
+
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@@ -60,6 +63,12 @@ public final class DeviceEntity extends BaseSqlEntity<Device> implements SearchT
@Column(name = ModelConstants.DEVICE_ADDITIONAL_INFO_PROPERTY)
private JsonNode additionalInfo;
+ @Column(name = DEVICE_LAST_CONNECT_PROPERTY)
+ private Long lastConnectTs;
+
+ @Column(name = DEVICE_LAST_UPDATE_PROPERTY)
+ private Long lastUpdateTs;
+
public DeviceEntity() {
super();
}
@@ -77,6 +86,8 @@ public final class DeviceEntity extends BaseSqlEntity<Device> implements SearchT
this.name = device.getName();
this.type = device.getType();
this.additionalInfo = device.getAdditionalInfo();
+ this.lastConnectTs = device.getLastConnectTs();
+ this.lastUpdateTs = device.getLastUpdateTs();
}
@Override
@@ -102,6 +113,8 @@ public final class DeviceEntity extends BaseSqlEntity<Device> implements SearchT
device.setName(name);
device.setType(type);
device.setAdditionalInfo(additionalInfo);
+ device.setLastConnectTs(lastConnectTs);
+ device.setLastUpdateTs(lastUpdateTs);
return device;
}
}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java
index 471ec7b..a48421a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java
@@ -58,6 +58,9 @@ public class RuleChainEntity extends BaseSqlEntity<RuleChain> implements SearchT
@Column(name = ModelConstants.RULE_CHAIN_ROOT_PROPERTY)
private boolean root;
+ @Column(name = ModelConstants.DEBUG_MODE)
+ private boolean debugMode;
+
@Type(type = "json")
@Column(name = ModelConstants.RULE_CHAIN_CONFIGURATION_PROPERTY)
private JsonNode configuration;
@@ -80,6 +83,7 @@ public class RuleChainEntity extends BaseSqlEntity<RuleChain> implements SearchT
this.firstRuleNodeId = UUIDConverter.fromTimeUUID(ruleChain.getFirstRuleNodeId().getId());
}
this.root = ruleChain.isRoot();
+ this.debugMode = ruleChain.isDebugMode();
this.configuration = ruleChain.getConfiguration();
this.additionalInfo = ruleChain.getAdditionalInfo();
}
@@ -104,6 +108,7 @@ public class RuleChainEntity extends BaseSqlEntity<RuleChain> implements SearchT
ruleChain.setFirstRuleNodeId(new RuleNodeId(UUIDConverter.fromString(firstRuleNodeId)));
}
ruleChain.setRoot(root);
+ ruleChain.setDebugMode(debugMode);
ruleChain.setConfiguration(configuration);
ruleChain.setAdditionalInfo(additionalInfo);
return ruleChain;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java
index d960487..6a888c2 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java
@@ -56,6 +56,9 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> implements SearchTex
@Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY)
private JsonNode additionalInfo;
+ @Column(name = ModelConstants.DEBUG_MODE)
+ private boolean debugMode;
+
public RuleNodeEntity() {
}
@@ -65,6 +68,7 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> implements SearchTex
}
this.type = ruleNode.getType();
this.name = ruleNode.getName();
+ this.debugMode = ruleNode.isDebugMode();
this.searchText = ruleNode.getName();
this.configuration = ruleNode.getConfiguration();
this.additionalInfo = ruleNode.getAdditionalInfo();
@@ -86,6 +90,7 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> implements SearchTex
ruleNode.setCreatedTime(UUIDs.unixTimestamp(getId()));
ruleNode.setType(type);
ruleNode.setName(name);
+ ruleNode.setDebugMode(debugMode);
ruleNode.setConfiguration(configuration);
ruleNode.setAdditionalInfo(additionalInfo);
return ruleNode;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
index c2f709f..ba186cc 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
@@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.model.type.*;
+import org.thingsboard.server.dao.util.BufferedRateLimiter;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -33,16 +34,15 @@ public abstract class CassandraAbstractDao {
private ConcurrentMap<String, PreparedStatement> preparedStatementMap = new ConcurrentHashMap<>();
- protected PreparedStatement prepare(String query) {
- return preparedStatementMap.computeIfAbsent(query, i -> getSession().prepare(i));
- }
+ @Autowired
+ private BufferedRateLimiter rateLimiter;
private Session session;
private ConsistencyLevel defaultReadLevel;
private ConsistencyLevel defaultWriteLevel;
- protected Session getSession() {
+ private Session getSession() {
if (session == null) {
session = cluster.getSession();
defaultReadLevel = cluster.getDefaultReadConsistencyLevel();
@@ -59,6 +59,10 @@ public abstract class CassandraAbstractDao {
return session;
}
+ protected PreparedStatement prepare(String query) {
+ return preparedStatementMap.computeIfAbsent(query, i -> getSession().prepare(i));
+ }
+
private void registerCodecIfNotFound(CodecRegistry registry, TypeCodec<?> codec) {
try {
registry.codecFor(codec.getCqlType(), codec.getJavaType());
@@ -85,10 +89,7 @@ public abstract class CassandraAbstractDao {
private ResultSet execute(Statement statement, ConsistencyLevel level) {
log.debug("Execute cassandra statement {}", statement);
- if (statement.getConsistencyLevel() == null) {
- statement.setConsistencyLevel(level);
- }
- return getSession().execute(statement);
+ return executeAsync(statement, level).getUninterruptibly();
}
private ResultSetFuture executeAsync(Statement statement, ConsistencyLevel level) {
@@ -96,6 +97,6 @@ public abstract class CassandraAbstractDao {
if (statement.getConsistencyLevel() == null) {
statement.setConsistencyLevel(level);
}
- return getSession().executeAsync(statement);
+ return new RateLimitedResultSetFuture(getSession(), rateLimiter, statement);
}
}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractModelDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractModelDao.java
index 7e87fa8..47d43ba 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractModelDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractModelDao.java
@@ -63,7 +63,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
List<E> list = Collections.emptyList();
if (statement != null) {
statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
- ResultSet resultSet = getSession().execute(statement);
+ ResultSet resultSet = executeRead(statement);
Result<E> result = getMapper().map(resultSet);
if (result != null) {
list = result.all();
@@ -75,7 +75,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
protected ListenableFuture<List<D>> findListByStatementAsync(Statement statement) {
if (statement != null) {
statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
- ResultSetFuture resultSetFuture = getSession().executeAsync(statement);
+ ResultSetFuture resultSetFuture = executeAsyncRead(statement);
return Futures.transform(resultSetFuture, new Function<ResultSet, List<D>>() {
@Nullable
@Override
@@ -97,7 +97,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
E object = null;
if (statement != null) {
statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
- ResultSet resultSet = getSession().execute(statement);
+ ResultSet resultSet = executeRead(statement);
Result<E> result = getMapper().map(resultSet);
if (result != null) {
object = result.one();
@@ -109,7 +109,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
protected ListenableFuture<D> findOneByStatementAsync(Statement statement) {
if (statement != null) {
statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
- ResultSetFuture resultSetFuture = getSession().executeAsync(statement);
+ ResultSetFuture resultSetFuture = executeAsyncRead(statement);
return Futures.transform(resultSetFuture, new Function<ResultSet, D>() {
@Nullable
@Override
@@ -184,7 +184,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
public boolean removeById(UUID key) {
Statement delete = QueryBuilder.delete().all().from(getColumnFamilyName()).where(eq(ModelConstants.ID_PROPERTY, key));
log.debug("Remove request: {}", delete.toString());
- return getSession().execute(delete).wasApplied();
+ return executeWrite(delete).wasApplied();
}
@Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java
new file mode 100644
index 0000000..d250563
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java
@@ -0,0 +1,152 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.nosql;
+
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.Statement;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.Uninterruptibles;
+import org.thingsboard.server.dao.exception.BufferLimitException;
+import org.thingsboard.server.dao.util.AsyncRateLimiter;
+
+import javax.annotation.Nullable;
+import java.util.concurrent.*;
+
+public class RateLimitedResultSetFuture implements ResultSetFuture {
+
+ private final ListenableFuture<ResultSetFuture> originalFuture;
+ private final ListenableFuture<Void> rateLimitFuture;
+
+ public RateLimitedResultSetFuture(Session session, AsyncRateLimiter rateLimiter, Statement statement) {
+ this.rateLimitFuture = Futures.withFallback(rateLimiter.acquireAsync(), t -> {
+ if (!(t instanceof BufferLimitException)) {
+ rateLimiter.release();
+ }
+ return Futures.immediateFailedFuture(t);
+ });
+ this.originalFuture = Futures.transform(rateLimitFuture,
+ (Function<Void, ResultSetFuture>) i -> executeAsyncWithRelease(rateLimiter, session, statement));
+
+ }
+
+ @Override
+ public ResultSet getUninterruptibly() {
+ return safeGet().getUninterruptibly();
+ }
+
+ @Override
+ public ResultSet getUninterruptibly(long timeout, TimeUnit unit) throws TimeoutException {
+ long rateLimitStart = System.nanoTime();
+ ResultSetFuture resultSetFuture = null;
+ try {
+ resultSetFuture = originalFuture.get(timeout, unit);
+ } catch (InterruptedException | ExecutionException e) {
+ throw new IllegalStateException(e);
+ }
+ long rateLimitDurationNano = System.nanoTime() - rateLimitStart;
+ long innerTimeoutNano = unit.toNanos(timeout) - rateLimitDurationNano;
+ if (innerTimeoutNano > 0) {
+ return resultSetFuture.getUninterruptibly(innerTimeoutNano, TimeUnit.NANOSECONDS);
+ }
+ throw new TimeoutException("Timeout waiting for task.");
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ if (originalFuture.isDone()) {
+ return safeGet().cancel(mayInterruptIfRunning);
+ } else {
+ return originalFuture.cancel(mayInterruptIfRunning);
+ }
+ }
+
+ @Override
+ public boolean isCancelled() {
+ if (originalFuture.isDone()) {
+ return safeGet().isCancelled();
+ }
+
+ return originalFuture.isCancelled();
+ }
+
+ @Override
+ public boolean isDone() {
+ return originalFuture.isDone() && safeGet().isDone();
+ }
+
+ @Override
+ public ResultSet get() throws InterruptedException, ExecutionException {
+ return safeGet().get();
+ }
+
+ @Override
+ public ResultSet get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ long rateLimitStart = System.nanoTime();
+ ResultSetFuture resultSetFuture = originalFuture.get(timeout, unit);
+ long rateLimitDurationNano = System.nanoTime() - rateLimitStart;
+ long innerTimeoutNano = unit.toNanos(timeout) - rateLimitDurationNano;
+ if (innerTimeoutNano > 0) {
+ return resultSetFuture.get(innerTimeoutNano, TimeUnit.NANOSECONDS);
+ }
+ throw new TimeoutException("Timeout waiting for task.");
+ }
+
+ @Override
+ public void addListener(Runnable listener, Executor executor) {
+ originalFuture.addListener(() -> {
+ try {
+ ResultSetFuture resultSetFuture = Uninterruptibles.getUninterruptibly(originalFuture);
+ resultSetFuture.addListener(listener, executor);
+ } catch (CancellationException | ExecutionException e) {
+ Futures.immediateFailedFuture(e).addListener(listener, executor);
+ }
+ }, executor);
+ }
+
+ private ResultSetFuture safeGet() {
+ try {
+ return originalFuture.get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private ResultSetFuture executeAsyncWithRelease(AsyncRateLimiter rateLimiter, Session session, Statement statement) {
+ try {
+ ResultSetFuture resultSetFuture = session.executeAsync(statement);
+ Futures.addCallback(resultSetFuture, new FutureCallback<ResultSet>() {
+ @Override
+ public void onSuccess(@Nullable ResultSet result) {
+ rateLimiter.release();
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ rateLimiter.release();
+ }
+ });
+ return resultSetFuture;
+ } catch (RuntimeException re) {
+ rateLimiter.release();
+ throw re;
+ }
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/QueueBenchmark.java b/dao/src/main/java/org/thingsboard/server/dao/queue/QueueBenchmark.java
index da991fa..85f42ae 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/queue/QueueBenchmark.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/queue/QueueBenchmark.java
@@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import javax.annotation.Nullable;
@@ -125,7 +126,7 @@ public class QueueBenchmark implements CommandLineRunner {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("key", "value");
String dataStr = "someContent";
- return new TbMsg(UUIDs.timeBased(), "type", null, metaData, dataStr.getBytes());
+ return new TbMsg(UUIDs.timeBased(), "type", null, metaData, TbMsgDataType.JSON, dataStr);
}
@Bean
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
index 9e25241..55838d6 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
@@ -242,7 +242,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
private PreparedStatement getSaveStmt() {
if (saveStmt == null) {
- saveStmt = getSession().prepare("INSERT INTO " + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
+ saveStmt = prepare("INSERT INTO " + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
"(" + ModelConstants.RELATION_FROM_ID_PROPERTY +
"," + ModelConstants.RELATION_FROM_TYPE_PROPERTY +
"," + ModelConstants.RELATION_TO_ID_PROPERTY +
@@ -257,7 +257,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
private PreparedStatement getDeleteStmt() {
if (deleteStmt == null) {
- deleteStmt = getSession().prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME +
+ deleteStmt = prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME +
WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ?" +
AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ?" +
AND + ModelConstants.RELATION_TO_ID_PROPERTY + " = ?" +
@@ -270,7 +270,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
private PreparedStatement getDeleteAllByEntityStmt() {
if (deleteAllByEntityStmt == null) {
- deleteAllByEntityStmt = getSession().prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME +
+ deleteAllByEntityStmt = prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME +
WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ?" +
AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ?");
}
@@ -279,7 +279,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
private PreparedStatement getFindAllByFromStmt() {
if (findAllByFromStmt == null) {
- findAllByFromStmt = getSession().prepare(SELECT_COLUMNS + " " +
+ findAllByFromStmt = prepare(SELECT_COLUMNS + " " +
FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + EQUAL_TO_PARAM +
AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + EQUAL_TO_PARAM +
@@ -290,7 +290,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
private PreparedStatement getFindAllByFromAndTypeStmt() {
if (findAllByFromAndTypeStmt == null) {
- findAllByFromAndTypeStmt = getSession().prepare(SELECT_COLUMNS + " " +
+ findAllByFromAndTypeStmt = prepare(SELECT_COLUMNS + " " +
FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + EQUAL_TO_PARAM +
AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + EQUAL_TO_PARAM +
@@ -303,7 +303,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
private PreparedStatement getFindAllByToStmt() {
if (findAllByToStmt == null) {
- findAllByToStmt = getSession().prepare(SELECT_COLUMNS + " " +
+ findAllByToStmt = prepare(SELECT_COLUMNS + " " +
FROM + ModelConstants.RELATION_REVERSE_VIEW_NAME + " " +
WHERE + ModelConstants.RELATION_TO_ID_PROPERTY + EQUAL_TO_PARAM +
AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + EQUAL_TO_PARAM +
@@ -314,7 +314,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
private PreparedStatement getFindAllByToAndTypeStmt() {
if (findAllByToAndTypeStmt == null) {
- findAllByToAndTypeStmt = getSession().prepare(SELECT_COLUMNS + " " +
+ findAllByToAndTypeStmt = prepare(SELECT_COLUMNS + " " +
FROM + ModelConstants.RELATION_REVERSE_VIEW_NAME + " " +
WHERE + ModelConstants.RELATION_TO_ID_PROPERTY + EQUAL_TO_PARAM +
AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + EQUAL_TO_PARAM +
@@ -327,7 +327,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
private PreparedStatement getCheckRelationStmt() {
if (checkRelationStmt == null) {
- checkRelationStmt = getSession().prepare(SELECT_COLUMNS + " " +
+ checkRelationStmt = prepare(SELECT_COLUMNS + " " +
FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + EQUAL_TO_PARAM +
AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + EQUAL_TO_PARAM +
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
index 01f60f8..836bd3d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
@@ -82,8 +82,9 @@ public class BaseRelationService implements RelationService {
}
@Caching(evict = {
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}")
})
@@ -95,8 +96,9 @@ public class BaseRelationService implements RelationService {
}
@Caching(evict = {
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}")
})
@@ -108,11 +110,11 @@ public class BaseRelationService implements RelationService {
}
@Caching(evict = {
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"),
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}"),
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}")
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}")
})
@Override
public boolean deleteRelation(EntityRelation relation) {
@@ -122,11 +124,11 @@ public class BaseRelationService implements RelationService {
}
@Caching(evict = {
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.from"),
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type}"),
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.to"),
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type}"),
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}")
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}")
})
@Override
public ListenableFuture<Boolean> deleteRelationAsync(EntityRelation relation) {
@@ -136,11 +138,11 @@ public class BaseRelationService implements RelationService {
}
@Caching(evict = {
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#from"),
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType}"),
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#to"),
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType}"),
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType}")
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType, #typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup}")
})
@Override
public boolean deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
@@ -150,11 +152,11 @@ public class BaseRelationService implements RelationService {
}
@Caching(evict = {
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#from"),
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType}"),
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#to"),
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType}"),
- @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType}")
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType, #typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup}"),
+ @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup}")
})
@Override
public ListenableFuture<Boolean> deleteRelationAsync(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
@@ -209,9 +211,9 @@ public class BaseRelationService implements RelationService {
private void checkFromDeleteSync(Cache cache, List<Boolean> results, EntityRelation relation, boolean isRemove) {
if (isRemove) {
results.add(relationDao.deleteRelation(relation));
- cacheEviction(relation, relation.getTo(), cache);
+ cacheEviction(relation, false, cache);
} else {
- cacheEviction(relation, relation.getFrom(), cache);
+ cacheEviction(relation, true, cache);
}
}
@@ -260,25 +262,43 @@ public class BaseRelationService implements RelationService {
private void checkFromDeleteAsync(Cache cache, List<ListenableFuture<Boolean>> results, EntityRelation relation, boolean isRemove) {
if (isRemove) {
results.add(relationDao.deleteRelationAsync(relation));
- cacheEviction(relation, relation.getTo(), cache);
+ cacheEviction(relation, false, cache);
} else {
- cacheEviction(relation, relation.getFrom(), cache);
+ cacheEviction(relation, true, cache);
}
}
- private void cacheEviction(EntityRelation relation, EntityId entityId, Cache cache) {
- cache.evict(entityId);
-
- List<Object> toAndType = new ArrayList<>();
- toAndType.add(entityId);
- toAndType.add(relation.getType());
- cache.evict(toAndType);
-
- List<Object> fromToAndType = new ArrayList<>();
- fromToAndType.add(relation.getFrom());
- fromToAndType.add(relation.getTo());
- fromToAndType.add(relation.getType());
- cache.evict(fromToAndType);
+ private void cacheEviction(EntityRelation relation, boolean outboundOnly, Cache cache) {
+ List<Object> fromToTypeAndTypeGroup = new ArrayList<>();
+ fromToTypeAndTypeGroup.add(relation.getFrom());
+ fromToTypeAndTypeGroup.add(relation.getTo());
+ fromToTypeAndTypeGroup.add(relation.getType());
+ fromToTypeAndTypeGroup.add(relation.getTypeGroup());
+ cache.evict(fromToTypeAndTypeGroup);
+
+ List<Object> fromTypeAndTypeGroup = new ArrayList<>();
+ fromTypeAndTypeGroup.add(relation.getFrom());
+ fromTypeAndTypeGroup.add(relation.getType());
+ fromTypeAndTypeGroup.add(relation.getTypeGroup());
+ cache.evict(fromTypeAndTypeGroup);
+
+ List<Object> fromAndTypeGroup = new ArrayList<>();
+ fromAndTypeGroup.add(relation.getFrom());
+ fromAndTypeGroup.add(relation.getTypeGroup());
+ cache.evict(fromAndTypeGroup);
+
+ if (!outboundOnly) {
+ List<Object> toAndTypeGroup = new ArrayList<>();
+ toAndTypeGroup.add(relation.getTo());
+ toAndTypeGroup.add(relation.getTypeGroup());
+ cache.evict(toAndTypeGroup);
+
+ List<Object> toTypeAndTypeGroup = new ArrayList<>();
+ fromTypeAndTypeGroup.add(relation.getTo());
+ fromTypeAndTypeGroup.add(relation.getType());
+ fromTypeAndTypeGroup.add(relation.getTypeGroup());
+ cache.evict(toTypeAndTypeGroup);
+ }
}
@Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}")
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
index 1e79163..cdb9a80 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.dao.rule;
+import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -30,7 +31,9 @@ import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.common.data.rule.NodeConnectionInfo;
import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.dao.entity.AbstractEntityService;
@@ -147,7 +150,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
ruleChainDao.save(ruleChain);
}
if (ruleChainMetaData.getConnections() != null) {
- for (RuleChainMetaData.NodeConnectionInfo nodeConnection : ruleChainMetaData.getConnections()) {
+ for (NodeConnectionInfo nodeConnection : ruleChainMetaData.getConnections()) {
EntityId from = nodes.get(nodeConnection.getFromIndex()).getId();
EntityId to = nodes.get(nodeConnection.getToIndex()).getId();
String type = nodeConnection.getType();
@@ -160,12 +163,12 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
}
}
if (ruleChainMetaData.getRuleChainConnections() != null) {
- for (RuleChainMetaData.RuleChainConnectionInfo nodeToRuleChainConnection : ruleChainMetaData.getRuleChainConnections()) {
+ for (RuleChainConnectionInfo nodeToRuleChainConnection : ruleChainMetaData.getRuleChainConnections()) {
EntityId from = nodes.get(nodeToRuleChainConnection.getFromIndex()).getId();
EntityId to = nodeToRuleChainConnection.getTargetRuleChainId();
String type = nodeToRuleChainConnection.getType();
try {
- createRelation(new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE));
+ createRelation(new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE, nodeToRuleChainConnection.getAdditionalInfo()));
} catch (ExecutionException | InterruptedException e) {
log.warn("[{}] Failed to create rule node to rule chain relation. from: [{}], to: [{}]", from, to);
throw new RuntimeException(e);
@@ -205,7 +208,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
ruleChainMetaData.addConnectionInfo(fromIndex, toIndex, type);
} else if (nodeRelation.getTo().getEntityType() == EntityType.RULE_CHAIN) {
RuleChainId targetRuleChainId = new RuleChainId(nodeRelation.getTo().getId());
- ruleChainMetaData.addRuleChainConnectionInfo(fromIndex, targetRuleChainId, type);
+ ruleChainMetaData.addRuleChainConnectionInfo(fromIndex, targetRuleChainId, type, nodeRelation.getAdditionalInfo());
}
}
}
@@ -219,6 +222,18 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
}
@Override
+ public RuleNode findRuleNodeById(RuleNodeId ruleNodeId) {
+ Validator.validateId(ruleNodeId, "Incorrect rule node id for search request.");
+ return ruleNodeDao.findById(ruleNodeId.getId());
+ }
+
+ @Override
+ public ListenableFuture<RuleChain> findRuleChainByIdAsync(RuleChainId ruleChainId) {
+ Validator.validateId(ruleChainId, "Incorrect rule chain id for search request.");
+ return ruleChainDao.findByIdAsync(ruleChainId.getId());
+ }
+
+ @Override
public RuleChain getRootTenantRuleChain(TenantId tenantId) {
Validator.validateId(tenantId, "Incorrect tenant id for search request.");
List<EntityRelation> relations = relationService.findByFrom(tenantId, RelationTypeGroup.RULE_CHAIN);
@@ -301,7 +316,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
private void createRelation(EntityRelation relation) throws ExecutionException, InterruptedException {
log.debug("Creating relation: {}", relation);
- relationService.saveRelationAsync(relation).get();
+ relationService.saveRelation(relation);
}
private DataValidator<RuleChain> ruleChainValidator =
@@ -318,7 +333,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
}
if (ruleChain.isRoot()) {
RuleChain rootRuleChain = getRootTenantRuleChain(ruleChain.getTenantId());
- if (ruleChain.getId() == null || !ruleChain.getId().equals(rootRuleChain.getId())) {
+ if (rootRuleChain != null && !rootRuleChain.getId().equals(ruleChain.getId())) {
throw new DataValidationException("Another root rule chain is present in scope of current tenant!");
}
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java
index f1df09e..fff3f6d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java
@@ -16,7 +16,6 @@
package org.thingsboard.server.dao.rule;
import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -67,67 +66,7 @@ public class BaseRuleService extends AbstractEntityService implements RuleServic
@Override
public RuleMetaData saveRule(RuleMetaData rule) {
- ruleValidator.validate(rule);
- if (rule.getTenantId() == null) {
- log.trace("Save system rule metadata with predefined id {}", systemTenantId);
- rule.setTenantId(systemTenantId);
- }
- if (rule.getId() != null) {
- RuleMetaData oldVersion = ruleDao.findById(rule.getId());
- if (rule.getState() == null) {
- rule.setState(oldVersion.getState());
- } else if (rule.getState() != oldVersion.getState()) {
- throw new IncorrectParameterException("Use Activate/Suspend method to control state of the rule!");
- }
- } else {
- if (rule.getState() == null) {
- rule.setState(ComponentLifecycleState.SUSPENDED);
- } else if (rule.getState() != ComponentLifecycleState.SUSPENDED) {
- throw new IncorrectParameterException("Use Activate/Suspend method to control state of the rule!");
- }
- }
-
- validateFilters(rule.getFilters());
- if (rule.getProcessor() != null && !rule.getProcessor().isNull()) {
- validateComponentJson(rule.getProcessor(), ComponentType.PROCESSOR);
- }
- if (rule.getAction() != null && !rule.getAction().isNull()) {
- validateComponentJson(rule.getAction(), ComponentType.ACTION);
- }
- validateRuleAndPluginState(rule);
- return ruleDao.save(rule);
- }
-
- private void validateFilters(JsonNode filtersJson) {
- if (filtersJson == null || filtersJson.isNull()) {
- throw new IncorrectParameterException("Rule filters are required!");
- }
- if (!filtersJson.isArray()) {
- throw new IncorrectParameterException("Filters json is not an array!");
- }
- ArrayNode filtersArray = (ArrayNode) filtersJson;
- for (int i = 0; i < filtersArray.size(); i++) {
- validateComponentJson(filtersArray.get(i), ComponentType.FILTER);
- }
- }
-
- private void validateComponentJson(JsonNode json, ComponentType type) {
- if (json == null || json.isNull()) {
- throw new IncorrectParameterException(type.name() + " is required!");
- }
- String clazz = getIfValid(type.name(), json, "clazz", JsonNode::isTextual, JsonNode::asText);
- String name = getIfValid(type.name(), json, "name", JsonNode::isTextual, JsonNode::asText);
- JsonNode configuration = getIfValid(type.name(), json, "configuration", JsonNode::isObject, node -> node);
- ComponentDescriptor descriptor = componentDescriptorService.findByClazz(clazz);
- if (descriptor == null) {
- throw new IncorrectParameterException(type.name() + " clazz " + clazz + " is not a valid component!");
- }
- if (descriptor.getType() != type) {
- throw new IncorrectParameterException("Clazz " + clazz + " is not a valid " + type.name() + " component!");
- }
- if (!componentDescriptorService.validate(descriptor, configuration)) {
- throw new IncorrectParameterException(type.name() + " configuration is not valid!");
- }
+ throw new RuntimeException("Not supported since v1.5!");
}
private void validateRuleAndPluginState(RuleMetaData rule) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java
index 6c44090..da7833d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.dao.rule;
+import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -41,6 +42,10 @@ public interface RuleChainService {
RuleChain findRuleChainById(RuleChainId ruleChainId);
+ RuleNode findRuleNodeById(RuleNodeId ruleNodeId);
+
+ ListenableFuture<RuleChain> findRuleChainByIdAsync(RuleChainId ruleChainId);
+
RuleChain getRootTenantRuleChain(TenantId tenantId);
List<RuleNode> getRuleChainNodes(RuleChainId ruleChainId);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java
index 3bab1c5..a48805b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java
@@ -79,4 +79,28 @@ public interface DeviceRepository extends CrudRepository<DeviceEntity, String> {
List<DeviceEntity> findDevicesByTenantIdAndCustomerIdAndIdIn(String tenantId, String customerId, List<String> deviceIds);
List<DeviceEntity> findDevicesByTenantIdAndIdIn(String tenantId, List<String> deviceIds);
+
+ @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastConnectTs > :time")
+ List<DeviceEntity> findConnectOnlineByTenantId(@Param("tenantId") String tenantId, @Param("time") long time);
+
+ @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastConnectTs < :time")
+ List<DeviceEntity> findConnectOfflineByTenantId(@Param("tenantId") String tenantId, @Param("time") long time);
+
+ @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastUpdateTs > :time")
+ List<DeviceEntity> findUpdateOnlineByTenantId(@Param("tenantId") String tenantId, @Param("time") long time);
+
+ @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastUpdateTs < :time")
+ List<DeviceEntity> findUpdateOfflineByTenantId(@Param("tenantId") String tenantId, @Param("time") long time);
+
+ @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastConnectTs > :time AND d.type = :type")
+ List<DeviceEntity> findConnectOnlineByTenantIdAndType(@Param("tenantId") String tenantId, @Param("time") long time, @Param("type") String type);
+
+ @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastConnectTs < :time AND d.type = :type")
+ List<DeviceEntity> findConnectOfflineByTenantIdAndType(@Param("tenantId") String tenantId, @Param("time") long time, @Param("type") String type);
+
+ @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastUpdateTs > :time AND d.type = :type")
+ List<DeviceEntity> findUpdateOnlineByTenantIdAndType(@Param("tenantId") String tenantId, @Param("time") long time, @Param("type") String type);
+
+ @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastUpdateTs < :time AND d.type = :type")
+ List<DeviceEntity> findUpdateOfflineByTenantIdAndType(@Param("tenantId") String tenantId, @Param("time") long time, @Param("type") String type);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
index 4f3cd7d..baba659 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
@@ -15,7 +15,9 @@
*/
package org.thingsboard.server.dao.sql.device;
+import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.repository.CrudRepository;
@@ -24,6 +26,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.UUIDConverter;
+import org.thingsboard.server.common.data.device.DeviceStatusQuery;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.DaoUtil;
@@ -43,6 +46,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR;
*/
@Component
@SqlDao
+@Slf4j
public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device> implements DeviceDao {
@Autowired
@@ -124,6 +128,73 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(fromTimeUUID(tenantId))));
}
+ @Override
+ public ListenableFuture<List<Device>> findDevicesByTenantIdAndStatus(UUID tenantId, DeviceStatusQuery statusQuery) {
+ String strTenantId = fromTimeUUID(tenantId);
+ long minTime = System.currentTimeMillis() - statusQuery.getThreshold();
+ switch (statusQuery.getStatus()) {
+ case OFFLINE: {
+ switch (statusQuery.getContactType()) {
+ case UPLOAD:
+ return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findUpdateOfflineByTenantId(strTenantId, minTime)));
+ case CONNECT:
+ return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findConnectOfflineByTenantId(strTenantId, minTime)));
+ }
+ break;
+ }
+ case ONLINE: {
+ switch (statusQuery.getContactType()) {
+ case UPLOAD:
+ return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findUpdateOnlineByTenantId(strTenantId, minTime)));
+ case CONNECT:
+ return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findConnectOnlineByTenantId(strTenantId, minTime)));
+ }
+ break;
+ }
+ }
+
+ log.error("Could not build status query from [{}]", statusQuery);
+ throw new IllegalStateException("Could not build status query for device []");
+ }
+
+ @Override
+ public ListenableFuture<List<Device>> findDevicesByTenantIdTypeAndStatus(UUID tenantId, String type, DeviceStatusQuery statusQuery) {
+ String strTenantId = fromTimeUUID(tenantId);
+ long minTime = System.currentTimeMillis() - statusQuery.getThreshold();
+ switch (statusQuery.getStatus()) {
+ case OFFLINE: {
+ switch (statusQuery.getContactType()) {
+ case UPLOAD:
+ return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findUpdateOfflineByTenantIdAndType(strTenantId, minTime, type)));
+ case CONNECT:
+ return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findConnectOfflineByTenantIdAndType(strTenantId, minTime, type)));
+ }
+ break;
+ }
+ case ONLINE: {
+ switch (statusQuery.getContactType()) {
+ case UPLOAD:
+ return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findUpdateOnlineByTenantIdAndType(strTenantId, minTime, type)));
+ case CONNECT:
+ return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findConnectOnlineByTenantIdAndType(strTenantId, minTime, type)));
+ }
+ break;
+ }
+ }
+
+ log.error("Could not build status query from [{}]", statusQuery);
+ throw new IllegalStateException("Could not build status query for device []");
+ }
+
+ @Override
+ public void saveDeviceStatus(Device device) {
+ ListenableFuture<Device> future = service.submit(() -> save(device));
+ Futures.withFallback(future, t -> {
+ log.error("Can't update device status for [{}]", device, t);
+ throw new IllegalArgumentException("Can't update device status for {" + device + "}", t);
+ });
+ }
+
private List<EntitySubtype> convertTenantDeviceTypesToDto(UUID tenantId, List<String> types) {
List<EntitySubtype> list = Collections.emptyList();
if (types != null && !types.isEmpty()) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
index d620e11..cda4b16 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
@@ -73,7 +73,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
private PreparedStatement partitionInsertStmt;
private PreparedStatement partitionInsertTtlStmt;
- private PreparedStatement[] latestInsertStmts;
+ private PreparedStatement latestInsertStmt;
private PreparedStatement[] saveStmts;
private PreparedStatement[] saveTtlStmts;
private PreparedStatement[] fetchStmts;
@@ -306,13 +306,15 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
@Override
public ListenableFuture<Void> saveLatest(EntityId entityId, TsKvEntry tsKvEntry) {
- DataType type = tsKvEntry.getDataType();
- BoundStatement stmt = getLatestStmt(type).bind()
+ BoundStatement stmt = getLatestStmt().bind()
.setString(0, entityId.getEntityType().name())
.setUUID(1, entityId.getId())
.setString(2, tsKvEntry.getKey())
- .setLong(3, tsKvEntry.getTs());
- addValue(tsKvEntry, stmt, 4);
+ .setLong(3, tsKvEntry.getTs())
+ .set(4, tsKvEntry.getBooleanValue().orElse(null), Boolean.class)
+ .set(5, tsKvEntry.getStrValue().orElse(null), String.class)
+ .set(6, tsKvEntry.getLongValue().orElse(null), Long.class)
+ .set(7, tsKvEntry.getDoubleValue().orElse(null), Double.class);
return getFuture(executeAsyncWrite(stmt), rs -> null);
}
@@ -381,7 +383,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
if (saveStmts == null) {
saveStmts = new PreparedStatement[DataType.values().length];
for (DataType type : DataType.values()) {
- saveStmts[type.ordinal()] = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
+ saveStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.KEY_COLUMN +
@@ -398,7 +400,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
if (saveTtlStmts == null) {
saveTtlStmts = new PreparedStatement[DataType.values().length];
for (DataType type : DataType.values()) {
- saveTtlStmts[type.ordinal()] = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
+ saveTtlStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.KEY_COLUMN +
@@ -420,7 +422,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
} else if (type == Aggregation.AVG && fetchStmts[Aggregation.SUM.ordinal()] != null) {
fetchStmts[type.ordinal()] = fetchStmts[Aggregation.SUM.ordinal()];
} else {
- fetchStmts[type.ordinal()] = getSession().prepare(SELECT_PREFIX +
+ fetchStmts[type.ordinal()] = prepare(SELECT_PREFIX +
String.join(", ", ModelConstants.getFetchColumnNames(type)) + " FROM " + ModelConstants.TS_KV_CF
+ " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
@@ -435,26 +437,26 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
return fetchStmts[aggType.ordinal()];
}
- private PreparedStatement getLatestStmt(DataType dataType) {
- if (latestInsertStmts == null) {
- latestInsertStmts = new PreparedStatement[DataType.values().length];
- for (DataType type : DataType.values()) {
- latestInsertStmts[type.ordinal()] = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_LATEST_CF +
- "(" + ModelConstants.ENTITY_TYPE_COLUMN +
- "," + ModelConstants.ENTITY_ID_COLUMN +
- "," + ModelConstants.KEY_COLUMN +
- "," + ModelConstants.TS_COLUMN +
- "," + getColumnName(type) + ")" +
- " VALUES(?, ?, ?, ?, ?)");
- }
+ private PreparedStatement getLatestStmt() {
+ if (latestInsertStmt == null) {
+ latestInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_LATEST_CF +
+ "(" + ModelConstants.ENTITY_TYPE_COLUMN +
+ "," + ModelConstants.ENTITY_ID_COLUMN +
+ "," + ModelConstants.KEY_COLUMN +
+ "," + ModelConstants.TS_COLUMN +
+ "," + ModelConstants.BOOLEAN_VALUE_COLUMN +
+ "," + ModelConstants.STRING_VALUE_COLUMN +
+ "," + ModelConstants.LONG_VALUE_COLUMN +
+ "," + ModelConstants.DOUBLE_VALUE_COLUMN + ")" +
+ " VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
}
- return latestInsertStmts[dataType.ordinal()];
+ return latestInsertStmt;
}
private PreparedStatement getPartitionInsertStmt() {
if (partitionInsertStmt == null) {
- partitionInsertStmt = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
+ partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
@@ -466,7 +468,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
private PreparedStatement getPartitionInsertTtlStmt() {
if (partitionInsertTtlStmt == null) {
- partitionInsertTtlStmt = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
+ partitionInsertTtlStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
@@ -479,7 +481,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
private PreparedStatement getFindLatestStmt() {
if (findLatestStmt == null) {
- findLatestStmt = getSession().prepare(SELECT_PREFIX +
+ findLatestStmt = prepare(SELECT_PREFIX +
ModelConstants.KEY_COLUMN + "," +
ModelConstants.TS_COLUMN + "," +
ModelConstants.STRING_VALUE_COLUMN + "," +
@@ -496,7 +498,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
private PreparedStatement getFindAllLatestStmt() {
if (findAllLatestStmt == null) {
- findAllLatestStmt = getSession().prepare(SELECT_PREFIX +
+ findAllLatestStmt = prepare(SELECT_PREFIX +
ModelConstants.KEY_COLUMN + "," +
ModelConstants.TS_COLUMN + "," +
ModelConstants.STRING_VALUE_COLUMN + "," +
diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java b/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java
new file mode 100644
index 0000000..0419668
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java
@@ -0,0 +1,178 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.util;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.dao.exception.BufferLimitException;
+
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Component
+@Slf4j
+@NoSqlDao
+public class BufferedRateLimiter implements AsyncRateLimiter {
+
+ private final ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
+
+ private final int permitsLimit;
+ private final int maxPermitWaitTime;
+ private final AtomicInteger permits;
+ private final BlockingQueue<LockedFuture> queue;
+
+ private final AtomicInteger maxQueueSize = new AtomicInteger();
+ private final AtomicInteger maxGrantedPermissions = new AtomicInteger();
+ private final AtomicInteger totalGranted = new AtomicInteger();
+ private final AtomicInteger totalReleased = new AtomicInteger();
+ private final AtomicInteger totalRequested = new AtomicInteger();
+
+ public BufferedRateLimiter(@Value("${cassandra.query.buffer_size}") int queueLimit,
+ @Value("${cassandra.query.concurrent_limit}") int permitsLimit,
+ @Value("${cassandra.query.permit_max_wait_time}") int maxPermitWaitTime) {
+ this.permitsLimit = permitsLimit;
+ this.maxPermitWaitTime = maxPermitWaitTime;
+ this.permits = new AtomicInteger();
+ this.queue = new LinkedBlockingQueue<>(queueLimit);
+ }
+
+ @Override
+ public ListenableFuture<Void> acquireAsync() {
+ totalRequested.incrementAndGet();
+ if (queue.isEmpty()) {
+ if (permits.incrementAndGet() <= permitsLimit) {
+ if (permits.get() > maxGrantedPermissions.get()) {
+ maxGrantedPermissions.set(permits.get());
+ }
+ totalGranted.incrementAndGet();
+ return Futures.immediateFuture(null);
+ }
+ permits.decrementAndGet();
+ }
+
+ return putInQueue();
+ }
+
+ @Override
+ public void release() {
+ permits.decrementAndGet();
+ totalReleased.incrementAndGet();
+ reprocessQueue();
+ }
+
+ private void reprocessQueue() {
+ while (permits.get() < permitsLimit) {
+ if (permits.incrementAndGet() <= permitsLimit) {
+ if (permits.get() > maxGrantedPermissions.get()) {
+ maxGrantedPermissions.set(permits.get());
+ }
+ LockedFuture lockedFuture = queue.poll();
+ if (lockedFuture != null) {
+ totalGranted.incrementAndGet();
+ lockedFuture.latch.countDown();
+ } else {
+ permits.decrementAndGet();
+ break;
+ }
+ } else {
+ permits.decrementAndGet();
+ }
+ }
+ }
+
+ private LockedFuture createLockedFuture() {
+ CountDownLatch latch = new CountDownLatch(1);
+ ListenableFuture<Void> future = pool.submit(() -> {
+ latch.await();
+ return null;
+ });
+ return new LockedFuture(latch, future, System.currentTimeMillis());
+ }
+
+ private ListenableFuture<Void> putInQueue() {
+
+ int size = queue.size();
+ if (size > maxQueueSize.get()) {
+ maxQueueSize.set(size);
+ }
+
+ if (queue.remainingCapacity() > 0) {
+ try {
+ LockedFuture lockedFuture = createLockedFuture();
+ if (!queue.offer(lockedFuture, 1, TimeUnit.SECONDS)) {
+ lockedFuture.cancelFuture();
+ return Futures.immediateFailedFuture(new BufferLimitException());
+ }
+ if(permits.get() < permitsLimit) {
+ reprocessQueue();
+ }
+ if(permits.get() < permitsLimit) {
+ reprocessQueue();
+ }
+ return lockedFuture.future;
+ } catch (InterruptedException e) {
+ return Futures.immediateFailedFuture(new BufferLimitException());
+ }
+ }
+ return Futures.immediateFailedFuture(new BufferLimitException());
+ }
+
+ @Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}")
+ public void printStats() {
+ int expiredCount = 0;
+ for (LockedFuture lockedFuture : queue) {
+ if (lockedFuture.isExpired()) {
+ lockedFuture.cancelFuture();
+ expiredCount++;
+ }
+ }
+ log.info("Permits maxBuffer [{}] maxPermits [{}] expired [{}] currPermits [{}] currBuffer [{}] " +
+ "totalPermits [{}] totalRequests [{}] totalReleased [{}]",
+ maxQueueSize.getAndSet(0), maxGrantedPermissions.getAndSet(0), expiredCount,
+ permits.get(), queue.size(),
+ totalGranted.getAndSet(0), totalRequested.getAndSet(0), totalReleased.getAndSet(0));
+ }
+
+ private class LockedFuture {
+ final CountDownLatch latch;
+ final ListenableFuture<Void> future;
+ final long createTime;
+
+ public LockedFuture(CountDownLatch latch, ListenableFuture<Void> future, long createTime) {
+ this.latch = latch;
+ this.future = future;
+ this.createTime = createTime;
+ }
+
+ void cancelFuture() {
+ future.cancel(false);
+ latch.countDown();
+ }
+
+ boolean isExpired() {
+ return (System.currentTimeMillis() - createTime) > maxPermitWaitTime;
+ }
+
+ }
+
+
+}
diff --git a/dao/src/main/resources/cassandra/schema.cql b/dao/src/main/resources/cassandra/schema.cql
index c221f68..79e9655 100644
--- a/dao/src/main/resources/cassandra/schema.cql
+++ b/dao/src/main/resources/cassandra/schema.cql
@@ -159,6 +159,8 @@ CREATE TABLE IF NOT EXISTS thingsboard.device (
type text,
search_text text,
additional_info text,
+ last_connect bigint,
+ last_update bigint,
PRIMARY KEY (id, tenant_id, customer_id, type)
);
@@ -669,6 +671,7 @@ CREATE TABLE IF NOT EXISTS thingsboard.rule_chain (
search_text text,
first_rule_node_id uuid,
root boolean,
+ debug_mode boolean,
configuration text,
additional_info text,
PRIMARY KEY (id, tenant_id)
@@ -685,6 +688,7 @@ CREATE TABLE IF NOT EXISTS thingsboard.rule_node (
id uuid,
type text,
name text,
+ debug_mode boolean,
search_text text,
configuration text,
additional_info text,
diff --git a/dao/src/main/resources/sql/schema.sql b/dao/src/main/resources/sql/schema.sql
index 106204a..08fe7fa 100644
--- a/dao/src/main/resources/sql/schema.sql
+++ b/dao/src/main/resources/sql/schema.sql
@@ -118,7 +118,9 @@ CREATE TABLE IF NOT EXISTS device (
type varchar(255),
name varchar(255),
search_text varchar(255),
- tenant_id varchar(31)
+ tenant_id varchar(31),
+ last_connect bigint,
+ last_update bigint
);
CREATE TABLE IF NOT EXISTS device_credentials (
@@ -263,6 +265,7 @@ CREATE TABLE IF NOT EXISTS rule_chain (
name varchar(255),
first_rule_node_id varchar(31),
root boolean,
+ debug_mode boolean,
search_text varchar(255),
tenant_id varchar(31)
);
@@ -273,5 +276,6 @@ CREATE TABLE IF NOT EXISTS rule_node (
configuration varchar(10000000),
type varchar(255),
name varchar(255),
+ debug_mode boolean,
search_text varchar(255)
);
diff --git a/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java b/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java
new file mode 100644
index 0000000..f49668d
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java
@@ -0,0 +1,182 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.nosql;
+
+import com.datastax.driver.core.*;
+import com.datastax.driver.core.exceptions.UnsupportedFeatureException;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.server.dao.exception.BufferLimitException;
+import org.thingsboard.server.dao.util.AsyncRateLimiter;
+
+import java.util.concurrent.*;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class RateLimitedResultSetFutureTest {
+
+ private RateLimitedResultSetFuture resultSetFuture;
+
+ @Mock
+ private AsyncRateLimiter rateLimiter;
+ @Mock
+ private Session session;
+ @Mock
+ private Statement statement;
+ @Mock
+ private ResultSetFuture realFuture;
+ @Mock
+ private ResultSet rows;
+ @Mock
+ private Row row;
+
+ @Test
+ public void doNotReleasePermissionIfRateLimitFutureFailed() throws InterruptedException {
+ when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFailedFuture(new BufferLimitException()));
+ resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
+ Thread.sleep(1000L);
+ verify(rateLimiter).acquireAsync();
+ try {
+ assertTrue(resultSetFuture.isDone());
+ fail();
+ } catch (Exception e) {
+ assertTrue(e instanceof IllegalStateException);
+ Throwable actualCause = e.getCause();
+ assertTrue(actualCause instanceof ExecutionException);
+ }
+ verifyNoMoreInteractions(session, rateLimiter, statement);
+
+ }
+
+ @Test
+ public void getUninterruptiblyDelegateToCassandra() throws InterruptedException, ExecutionException {
+ when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null));
+ when(session.executeAsync(statement)).thenReturn(realFuture);
+ Mockito.doAnswer((Answer<Void>) invocation -> {
+ Object[] args = invocation.getArguments();
+ Runnable task = (Runnable) args[0];
+ task.run();
+ return null;
+ }).when(realFuture).addListener(Mockito.any(), Mockito.any());
+
+ when(realFuture.getUninterruptibly()).thenReturn(rows);
+
+ resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
+ ResultSet actual = resultSetFuture.getUninterruptibly();
+ assertSame(rows, actual);
+ verify(rateLimiter, times(1)).acquireAsync();
+ verify(rateLimiter, times(1)).release();
+ }
+
+ @Test
+ public void addListenerAllowsFutureTransformation() throws InterruptedException, ExecutionException {
+ when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null));
+ when(session.executeAsync(statement)).thenReturn(realFuture);
+ Mockito.doAnswer((Answer<Void>) invocation -> {
+ Object[] args = invocation.getArguments();
+ Runnable task = (Runnable) args[0];
+ task.run();
+ return null;
+ }).when(realFuture).addListener(Mockito.any(), Mockito.any());
+
+ when(realFuture.get()).thenReturn(rows);
+ when(rows.one()).thenReturn(row);
+
+ resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
+
+ ListenableFuture<Row> transform = Futures.transform(resultSetFuture, ResultSet::one);
+ Row actualRow = transform.get();
+
+ assertSame(row, actualRow);
+ verify(rateLimiter, times(1)).acquireAsync();
+ verify(rateLimiter, times(1)).release();
+ }
+
+ @Test
+ public void immidiateCassandraExceptionReturnsPermit() throws InterruptedException, ExecutionException {
+ when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null));
+ when(session.executeAsync(statement)).thenThrow(new UnsupportedFeatureException(ProtocolVersion.V3, "hjg"));
+ resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
+ ListenableFuture<Row> transform = Futures.transform(resultSetFuture, ResultSet::one);
+ try {
+ transform.get();
+ fail();
+ } catch (Exception e) {
+ assertTrue(e instanceof ExecutionException);
+ }
+ verify(rateLimiter, times(1)).acquireAsync();
+ verify(rateLimiter, times(1)).release();
+ }
+
+ @Test
+ public void queryTimeoutReturnsPermit() throws InterruptedException, ExecutionException {
+ when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null));
+ when(session.executeAsync(statement)).thenReturn(realFuture);
+ Mockito.doAnswer((Answer<Void>) invocation -> {
+ Object[] args = invocation.getArguments();
+ Runnable task = (Runnable) args[0];
+ task.run();
+ return null;
+ }).when(realFuture).addListener(Mockito.any(), Mockito.any());
+
+ when(realFuture.get()).thenThrow(new ExecutionException("Fail", new TimeoutException("timeout")));
+ resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
+ ListenableFuture<Row> transform = Futures.transform(resultSetFuture, ResultSet::one);
+ try {
+ transform.get();
+ fail();
+ } catch (Exception e) {
+ assertTrue(e instanceof ExecutionException);
+ }
+ verify(rateLimiter, times(1)).acquireAsync();
+ verify(rateLimiter, times(1)).release();
+ }
+
+ @Test
+ public void expiredQueryReturnPermit() throws InterruptedException, ExecutionException {
+ CountDownLatch latch = new CountDownLatch(1);
+ ListenableFuture<Void> future = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1)).submit(() -> {
+ latch.await();
+ return null;
+ });
+ when(rateLimiter.acquireAsync()).thenReturn(future);
+ resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
+
+ ListenableFuture<Row> transform = Futures.transform(resultSetFuture, ResultSet::one);
+// TimeUnit.MILLISECONDS.sleep(200);
+ future.cancel(false);
+ latch.countDown();
+
+ try {
+ transform.get();
+ fail();
+ } catch (Exception e) {
+ assertTrue(e instanceof ExecutionException);
+ }
+ verify(rateLimiter, times(1)).acquireAsync();
+ verify(rateLimiter, times(1)).release();
+ }
+
+}
\ No newline at end of file
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
index d083a90..44a1a09 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
@@ -217,10 +217,10 @@ public abstract class AbstractServiceTest {
ruleMetaData.setWeight(weight);
ruleMetaData.setPluginToken(pluginToken);
- ruleMetaData.setAction(createNode(ComponentScope.TENANT, ComponentType.ACTION,
+ ruleMetaData.setAction(createNode(ComponentScope.TENANT, ComponentType.OLD_ACTION,
"org.thingsboard.component.ActionTest", "TestJsonDescriptor.json", "TestJsonData.json"));
- ruleMetaData.setProcessor(createNode(ComponentScope.TENANT, ComponentType.PROCESSOR,
- "org.thingsboard.component.ProcessorTest", "TestJsonDescriptor.json", "TestJsonData.json"));
+// ruleMetaData.setProcessor(createNode(ComponentScope.TENANT, ComponentType.PROCESSOR,
+// "org.thingsboard.component.ProcessorTest", "TestJsonDescriptor.json", "TestJsonData.json"));
ruleMetaData.setFilters(mapper.createArrayNode().add(
createNode(ComponentScope.TENANT, ComponentType.FILTER,
"org.thingsboard.component.FilterTest", "TestJsonDescriptor.json", "TestJsonData.json")
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/repository/impl/CassandraMsgRepositoryTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/repository/impl/CassandraMsgRepositoryTest.java
index d17e1f2..a766aa4 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/repository/impl/CassandraMsgRepositoryTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/repository/impl/CassandraMsgRepositoryTest.java
@@ -24,6 +24,7 @@ import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.service.AbstractServiceTest;
import org.thingsboard.server.dao.service.DaoNoSqlTest;
@@ -44,7 +45,7 @@ public class CassandraMsgRepositoryTest extends AbstractServiceTest {
@Test
public void msgCanBeSavedAndRead() throws ExecutionException, InterruptedException {
- TbMsg msg = new TbMsg(UUIDs.timeBased(), "type", new DeviceId(UUIDs.timeBased()), null, new byte[4]);
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "type", new DeviceId(UUIDs.timeBased()), null, TbMsgDataType.JSON, "0000");
UUID nodeId = UUIDs.timeBased();
ListenableFuture<Void> future = msgRepository.save(msg, nodeId, 1L, 1L, 1L);
future.get();
@@ -54,7 +55,7 @@ public class CassandraMsgRepositoryTest extends AbstractServiceTest {
@Test
public void expiredMsgsAreNotReturned() throws ExecutionException, InterruptedException {
- TbMsg msg = new TbMsg(UUIDs.timeBased(), "type", new DeviceId(UUIDs.timeBased()), null, new byte[4]);
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "type", new DeviceId(UUIDs.timeBased()), null, TbMsgDataType.JSON, "0000");
UUID nodeId = UUIDs.timeBased();
ListenableFuture<Void> future = msgRepository.save(msg, nodeId, 2L, 2L, 2L);
future.get();
@@ -67,7 +68,7 @@ public class CassandraMsgRepositoryTest extends AbstractServiceTest {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("key", "value");
String dataStr = "someContent";
- TbMsg msg = new TbMsg(UUIDs.timeBased(), "type", new DeviceId(UUIDs.timeBased()), metaData, dataStr.getBytes());
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "type", new DeviceId(UUIDs.timeBased()), metaData, TbMsgDataType.JSON, dataStr);
UUID nodeId = UUIDs.timeBased();
ListenableFuture<Void> future = msgRepository.save(msg, nodeId, 1L, 1L, 1L);
future.get();
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/UnprocessedMsgFilterTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/UnprocessedMsgFilterTest.java
index 6302e63..3935c9b 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/UnprocessedMsgFilterTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/UnprocessedMsgFilterTest.java
@@ -33,8 +33,8 @@ public class UnprocessedMsgFilterTest {
public void acknowledgedMsgsAreFilteredOut() {
UUID id1 = UUID.randomUUID();
UUID id2 = UUID.randomUUID();
- TbMsg msg1 = new TbMsg(id1, "T", null, null, null);
- TbMsg msg2 = new TbMsg(id2, "T", null, null, null);
+ TbMsg msg1 = new TbMsg(id1, "T", null, null, null, null);
+ TbMsg msg2 = new TbMsg(id2, "T", null, null, null, null);
List<TbMsg> msgs = Lists.newArrayList(msg1, msg2);
List<MsgAck> acks = Lists.newArrayList(new MsgAck(id2, UUID.randomUUID(), 1L, 1L));
Collection<TbMsg> actual = msgFilter.filter(msgs, acks);
diff --git a/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java b/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java
new file mode 100644
index 0000000..67c3ce8
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java
@@ -0,0 +1,135 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.util;
+
+import com.google.common.util.concurrent.*;
+import org.junit.Test;
+import org.thingsboard.server.dao.exception.BufferLimitException;
+
+import javax.annotation.Nullable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.*;
+
+
+public class BufferedRateLimiterTest {
+
+ @Test
+ public void finishedFutureReturnedIfPermitsAreGranted() {
+ BufferedRateLimiter limiter = new BufferedRateLimiter(10, 10, 100);
+ ListenableFuture<Void> actual = limiter.acquireAsync();
+ assertTrue(actual.isDone());
+ }
+
+ @Test
+ public void notFinishedFutureReturnedIfPermitsAreNotGranted() {
+ BufferedRateLimiter limiter = new BufferedRateLimiter(10, 1, 100);
+ ListenableFuture<Void> actual1 = limiter.acquireAsync();
+ ListenableFuture<Void> actual2 = limiter.acquireAsync();
+ assertTrue(actual1.isDone());
+ assertFalse(actual2.isDone());
+ }
+
+ @Test
+ public void failedFutureReturnedIfQueueIsfull() {
+ BufferedRateLimiter limiter = new BufferedRateLimiter(1, 1, 100);
+ ListenableFuture<Void> actual1 = limiter.acquireAsync();
+ ListenableFuture<Void> actual2 = limiter.acquireAsync();
+ ListenableFuture<Void> actual3 = limiter.acquireAsync();
+
+ assertTrue(actual1.isDone());
+ assertFalse(actual2.isDone());
+ assertTrue(actual3.isDone());
+ try {
+ actual3.get();
+ fail();
+ } catch (Exception e) {
+ assertTrue(e instanceof ExecutionException);
+ Throwable actualCause = e.getCause();
+ assertTrue(actualCause instanceof BufferLimitException);
+ assertEquals("Rate Limit Buffer is full", actualCause.getMessage());
+ }
+ }
+
+ @Test
+ public void releasedPermitTriggerTasksFromQueue() throws InterruptedException {
+ BufferedRateLimiter limiter = new BufferedRateLimiter(10, 2, 100);
+ ListenableFuture<Void> actual1 = limiter.acquireAsync();
+ ListenableFuture<Void> actual2 = limiter.acquireAsync();
+ ListenableFuture<Void> actual3 = limiter.acquireAsync();
+ ListenableFuture<Void> actual4 = limiter.acquireAsync();
+ assertTrue(actual1.isDone());
+ assertTrue(actual2.isDone());
+ assertFalse(actual3.isDone());
+ assertFalse(actual4.isDone());
+ limiter.release();
+ TimeUnit.MILLISECONDS.sleep(100L);
+ assertTrue(actual3.isDone());
+ assertFalse(actual4.isDone());
+ limiter.release();
+ TimeUnit.MILLISECONDS.sleep(100L);
+ assertTrue(actual4.isDone());
+ }
+
+ @Test
+ public void permitsReleasedInConcurrentMode() throws InterruptedException {
+ BufferedRateLimiter limiter = new BufferedRateLimiter(10, 2, 100);
+ AtomicInteger actualReleased = new AtomicInteger();
+ AtomicInteger actualRejected = new AtomicInteger();
+ ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(5));
+ for (int i = 0; i < 100; i++) {
+ ListenableFuture<ListenableFuture<Void>> submit = pool.submit(limiter::acquireAsync);
+ Futures.addCallback(submit, new FutureCallback<ListenableFuture<Void>>() {
+ @Override
+ public void onSuccess(@Nullable ListenableFuture<Void> result) {
+ Futures.addCallback(result, new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(@Nullable Void result) {
+ try {
+ TimeUnit.MILLISECONDS.sleep(100);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ limiter.release();
+ actualReleased.incrementAndGet();
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ actualRejected.incrementAndGet();
+ }
+ });
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ }
+ });
+ }
+
+ TimeUnit.SECONDS.sleep(2);
+ assertTrue("Unexpected released count " + actualReleased.get(),
+ actualReleased.get() > 10 && actualReleased.get() < 20);
+ assertTrue("Unexpected rejected count " + actualRejected.get(),
+ actualRejected.get() > 80 && actualRejected.get() < 90);
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/dao/src/test/resources/cassandra-test.properties b/dao/src/test/resources/cassandra-test.properties
index 82fcbe1..737687f 100644
--- a/dao/src/test/resources/cassandra-test.properties
+++ b/dao/src/test/resources/cassandra-test.properties
@@ -47,3 +47,8 @@ cassandra.query.default_fetch_size=2000
cassandra.query.ts_key_value_partitioning=HOURS
cassandra.query.max_limit_per_request=1000
+cassandra.query.buffer_size=100000
+cassandra.query.concurrent_limit=1000
+cassandra.query.permit_max_wait_time=20000
+cassandra.query.rate_limit_print_interval_ms=30000
+
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
index ac9600a..d7438f4 100644
--- 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
@@ -19,5 +19,7 @@ package org.thingsboard.server.extensions.api.plugins;
* @author Andrew Shvayka
*/
public class PluginConstants {
+ public static final String TELEMETRY_URL_PREFIX = "/api/plugins/telemetry";
+ public static final String RPC_URL_PREFIX = "/api/plugins/rpc";
public static final String PLUGIN_URL_PREFIX = "/api/plugins";
}
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
index d116b2e..f30f1a5 100644
--- 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
@@ -16,7 +16,7 @@
package org.thingsboard.server.extensions.core.plugin.telemetry.cmd;
import lombok.NoArgsConstructor;
-import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature;
/**
* @author Andrew Shvayka
@@ -25,8 +25,8 @@ import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionT
public class AttributesSubscriptionCmd extends SubscriptionCmd {
@Override
- public SubscriptionType getType() {
- return SubscriptionType.ATTRIBUTES;
+ public TelemetryFeature getType() {
+ return TelemetryFeature.ATTRIBUTES;
}
}
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
index b06476a..7f78abd 100644
--- 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
@@ -18,7 +18,7 @@ package org.thingsboard.server.extensions.core.plugin.telemetry.cmd;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
-import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature;
@NoArgsConstructor
@AllArgsConstructor
@@ -32,7 +32,7 @@ public abstract class SubscriptionCmd implements TelemetryPluginCmd {
private String scope;
private boolean unsubscribe;
- public abstract SubscriptionType getType();
+ public abstract TelemetryFeature getType();
@Override
public String toString() {
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
index 4d64ca7..88ecb2d 100644
--- 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
@@ -18,7 +18,7 @@ package org.thingsboard.server.extensions.core.plugin.telemetry.cmd;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
-import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature;
/**
* @author Andrew Shvayka
@@ -35,7 +35,7 @@ public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
private String agg;
@Override
- public SubscriptionType getType() {
- return SubscriptionType.TIMESERIES;
+ public TelemetryFeature getType() {
+ return TelemetryFeature.TIMESERIES;
}
}
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
index c6e7a54..1acc29d 100644
--- 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
@@ -114,7 +114,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
}
Map<String, Long> statesMap = proto.getKeyStatesList().stream().collect(Collectors.toMap(SubscriptionKetStateProto::getKey, SubscriptionKetStateProto::getTs));
Subscription subscription = new Subscription(
- new SubscriptionState(proto.getSessionId(), proto.getSubscriptionId(), EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()), SubscriptionType.valueOf(proto.getType()), proto.getAllKeys(), statesMap, proto.getScope()),
+ new SubscriptionState(proto.getSessionId(), proto.getSubscriptionId(), EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()), TelemetryFeature.valueOf(proto.getType()), proto.getAllKeys(), statesMap, proto.getScope()),
false, msg.getServerAddress());
subscriptionManager.addRemoteWsSubscription(ctx, msg.getServerAddress(), proto.getSessionId(), subscription);
}
@@ -243,27 +243,19 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
switch (attr.getDataType()) {
case BOOLEAN:
Optional<Boolean> booleanValue = attr.getBooleanValue();
- if (booleanValue.isPresent()) {
- dataBuilder.setBoolValue(booleanValue.get());
- }
+ booleanValue.ifPresent(dataBuilder::setBoolValue);
break;
case LONG:
Optional<Long> longValue = attr.getLongValue();
- if (longValue.isPresent()) {
- dataBuilder.setLongValue(longValue.get());
- }
+ longValue.ifPresent(dataBuilder::setLongValue);
break;
case DOUBLE:
Optional<Double> doubleValue = attr.getDoubleValue();
- if (doubleValue.isPresent()) {
- dataBuilder.setDoubleValue(doubleValue.get());
- }
+ doubleValue.ifPresent(dataBuilder::setDoubleValue);
break;
case STRING:
Optional<String> stringValue = attr.getStrValue();
- if (stringValue.isPresent()) {
- dataBuilder.setStrValue(stringValue.get());
- }
+ stringValue.ifPresent(dataBuilder::setStrValue);
break;
}
return dataBuilder;
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
index 4fdfe4a..9cb67fd 100644
--- 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
@@ -24,7 +24,11 @@ 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.core.BasicGetAttributesResponse;
+import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
+import org.thingsboard.server.common.msg.core.GetAttributesRequest;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.msg.core.UpdateAttributesRequest;
import org.thingsboard.server.common.msg.kv.BasicAttributeKVMsg;
import org.thingsboard.server.extensions.api.plugins.PluginCallback;
import org.thingsboard.server.extensions.api.plugins.PluginContext;
@@ -35,9 +39,13 @@ import org.thingsboard.server.extensions.api.plugins.msg.TelemetryUploadRequestR
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.Subscription;
-import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@@ -97,7 +105,7 @@ public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
@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 ->
+ subscriptionManager.onLocalSubscriptionUpdate(ctx, msg.getDeviceId(), TelemetryFeature.TIMESERIES, s ->
prepareSubscriptionUpdate(request, s)
);
}
@@ -131,7 +139,7 @@ public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
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 -> {
+ subscriptionManager.onLocalSubscriptionUpdate(ctx, msg.getDeviceId(), TelemetryFeature.ATTRIBUTES, s -> {
List<TsKvEntry> subscriptionUpdate = new ArrayList<>();
for (AttributeKvEntry kv : request.getAttributes()) {
if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) {
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
index 1374ef6..8c80e78 100644
--- 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
@@ -21,7 +21,12 @@ import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
import org.thingsboard.server.extensions.api.exception.UnauthorizedException;
import org.thingsboard.server.extensions.api.plugins.PluginCallback;
import org.thingsboard.server.extensions.api.plugins.PluginContext;
@@ -32,14 +37,26 @@ import org.thingsboard.server.extensions.api.plugins.ws.msg.BinaryPluginWebSocke
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.cmd.AttributesSubscriptionCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.GetHistoryCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.SubscriptionCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.TelemetryPluginCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.TelemetryPluginCmdsWrapper;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.TimeseriesSubscriptionCmd;
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.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -131,7 +148,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
keys.forEach(key -> subState.put(key, 0L));
attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
- SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, false, subState, cmd.getScope());
+ SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.ATTRIBUTES, false, subState, cmd.getScope());
subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub);
}
@@ -168,7 +185,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
Map<String, Long> subState = new HashMap<>(attributesData.size());
attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
- SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, true, subState, cmd.getScope());
+ SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.ATTRIBUTES, true, subState, cmd.getScope());
subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub);
}
@@ -234,7 +251,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
Map<String, Long> subState = new HashMap<>(data.size());
data.forEach(v -> subState.put(v.getKey(), v.getTs()));
- SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.TIMESERIES, true, subState, cmd.getScope());
+ SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.TIMESERIES, true, subState, cmd.getScope());
subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub);
}
@@ -262,7 +279,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
Map<String, Long> subState = new HashMap<>(keys.size());
keys.forEach(key -> subState.put(key, startTs));
data.forEach(v -> subState.put(v.getKey(), v.getTs()));
- SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.TIMESERIES, false, subState, cmd.getScope());
+ SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.TIMESERIES, false, subState, cmd.getScope());
subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub);
}
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
index fc04713..98c7632 100644
--- 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
@@ -20,6 +20,7 @@ import lombok.Data;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature;
import java.util.Map;
@@ -47,7 +48,7 @@ public class Subscription {
return getSub().getEntityId();
}
- public SubscriptionType getType() {
+ public TelemetryFeature getType() {
return getSub().getType();
}
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
index c9598ef..e4a0d26 100644
--- 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
@@ -18,6 +18,7 @@ package org.thingsboard.server.extensions.core.plugin.telemetry.sub;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature;
import java.util.Map;
@@ -30,7 +31,7 @@ public class SubscriptionState {
@Getter private final String wsSessionId;
@Getter private final int subscriptionId;
@Getter private final EntityId entityId;
- @Getter private final SubscriptionType type;
+ @Getter private final TelemetryFeature type;
@Getter private final boolean allKeys;
@Getter private final Map<String, Long> keyStates;
@Getter private final String scope;
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
index ec00677..0c3b174 100644
--- 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
@@ -19,20 +19,30 @@ import lombok.Setter;
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.id.EntityId;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.extensions.api.plugins.PluginCallback;
import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature;
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.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -70,7 +80,7 @@ public class SubscriptionManager {
EntityId entityId = subscription.getEntityId();
log.trace("[{}] Registering remote subscription [{}] for device [{}] to [{}]", sessionId, subscription.getSubscriptionId(), entityId, address);
registerSubscription(sessionId, entityId, subscription);
- if (subscription.getType() == SubscriptionType.ATTRIBUTES) {
+ if (subscription.getType() == TelemetryFeature.ATTRIBUTES) {
final Map<String, Long> keyStates = subscription.getKeyStates();
ctx.loadAttributes(entityId, DataConstants.CLIENT_SCOPE, keyStates.keySet(), new PluginCallback<List<AttributeKvEntry>>() {
@Override
@@ -91,7 +101,7 @@ public class SubscriptionManager {
log.error("Failed to fetch missed updates.", e);
}
});
- } else if (subscription.getType() == SubscriptionType.TIMESERIES) {
+ } else if (subscription.getType() == TelemetryFeature.TIMESERIES) {
long curTs = System.currentTimeMillis();
List<TsKvQuery> queries = new ArrayList<>();
subscription.getKeyStates().entrySet().forEach(e -> {
@@ -175,7 +185,7 @@ public class SubscriptionManager {
}
}
- public void onLocalSubscriptionUpdate(PluginContext ctx, EntityId entityId, SubscriptionType type, Function<Subscription, List<TsKvEntry>> f) {
+ public void onLocalSubscriptionUpdate(PluginContext ctx, EntityId entityId, TelemetryFeature type, Function<Subscription, List<TsKvEntry>> f) {
onLocalSubscriptionUpdate(ctx, entityId, s -> type == s.getType(), f);
}
@@ -212,7 +222,7 @@ public class SubscriptionManager {
public void onAttributesUpdateFromServer(PluginContext ctx, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
Optional<ServerAddress> serverAddress = ctx.resolve(entityId);
if (!serverAddress.isPresent()) {
- onLocalSubscriptionUpdate(ctx, entityId, s -> SubscriptionType.ATTRIBUTES == s.getType() && (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope())), s -> {
+ onLocalSubscriptionUpdate(ctx, entityId, s -> TelemetryFeature.ATTRIBUTES == s.getType() && (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope())), s -> {
List<TsKvEntry> subscriptionUpdate = new ArrayList<TsKvEntry>();
for (AttributeKvEntry kv : attributes) {
if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) {
@@ -229,7 +239,7 @@ public class SubscriptionManager {
public void onTimeseriesUpdateFromServer(PluginContext ctx, EntityId entityId, List<TsKvEntry> entries) {
Optional<ServerAddress> serverAddress = ctx.resolve(entityId);
if (!serverAddress.isPresent()) {
- onLocalSubscriptionUpdate(ctx, entityId, SubscriptionType.TIMESERIES, s -> {
+ onLocalSubscriptionUpdate(ctx, entityId, TelemetryFeature.TIMESERIES, s -> {
List<TsKvEntry> subscriptionUpdate = new ArrayList<TsKvEntry>();
for (TsKvEntry kv : entries) {
if (s.isAllKeys() || s.getKeyStates().containsKey((kv.getKey()))) {
@@ -344,9 +354,7 @@ public class SubscriptionManager {
}
private void checkSubsciptionsPrevAddress(Set<Subscription> subscriptions) {
- Iterator<Subscription> subscriptionIterator = subscriptions.iterator();
- while (subscriptionIterator.hasNext()) {
- Subscription s = subscriptionIterator.next();
+ for (Subscription s : subscriptions) {
if (s.isLocal()) {
if (s.getServer() != null) {
log.trace("[{}] Local subscription is no longer handled on remote server address [{}]", s.getWsSessionId(), s.getServer());
pom.xml 8(+7 -1)
diff --git a/pom.xml b/pom.xml
index f331e32..dc58627 100755
--- a/pom.xml
+++ b/pom.xml
@@ -41,7 +41,7 @@
<logback.version>1.2.3</logback.version>
<mockito.version>1.9.5</mockito.version>
<rat.version>0.10</rat.version>
- <cassandra.version>3.0.0</cassandra.version>
+ <cassandra.version>3.0.7</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>
@@ -284,6 +284,7 @@
<exclude>src/sh/**</exclude>
<exclude>src/main/scripts/control/**</exclude>
<exclude>src/main/scripts/windows/**</exclude>
+ <exclude>src/main/resources/public/static/rulenode/**</exclude>
</excludes>
<mapping>
<proto>JAVADOC_STYLE</proto>
@@ -379,6 +380,11 @@
<version>${project.version}</version>
</dependency>
<dependency>
+ <groupId>org.thingsboard.rule-engine</groupId>
+ <artifactId>rule-engine-components</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>message</artifactId>
<version>${project.version}</version>
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ListeningExecutor.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ListeningExecutor.java
new file mode 100644
index 0000000..9356be9
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ListeningExecutor.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.api;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
+
+public interface ListeningExecutor extends Executor {
+
+ <T> ListenableFuture<T> executeAsync(Callable<T> task);
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java
new file mode 100644
index 0000000..18b2b94
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.thingsboard.rule.engine.api;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+
+@Data
+public class NodeDefinition {
+
+ private String details;
+ private String description;
+ private boolean inEnabled;
+ private boolean outEnabled;
+ String[] relationTypes;
+ boolean customRelations;
+ JsonNode defaultConfiguration;
+ String[] uiResources;
+ String configDirective;
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java
new file mode 100644
index 0000000..1ba18cd
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.api;
+
+import com.google.common.util.concurrent.FutureCallback;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+
+import java.util.List;
+
+/**
+ * Created by ashvayka on 02.04.18.
+ */
+public interface RuleEngineTelemetryService {
+
+ void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback);
+
+ void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, long ttl, FutureCallback<Void> callback);
+
+ void saveAndNotify(EntityId entityId, String scope, List<AttributeKvEntry> attributes, FutureCallback<Void> callback);
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java
new file mode 100644
index 0000000..eea92ed
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.api;
+
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface RuleNode {
+
+ ComponentType type();
+
+ String name();
+
+ String nodeDescription();
+
+ String nodeDetails();
+
+ Class<? extends NodeConfiguration> configClazz();
+
+ boolean inEnabled() default true;
+
+ boolean outEnabled() default true;
+
+ ComponentScope scope() default ComponentScope.TENANT;
+
+ String[] relationTypes() default {"Success", "Failure"};
+
+ String[] uiResources() default {};
+
+ String configDirective() default "";
+
+ boolean customRelations() default false;
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java
new file mode 100644
index 0000000..1db046a
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.api;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import javax.script.ScriptException;
+import java.util.Set;
+
+public interface ScriptEngine {
+
+ TbMsg executeUpdate(TbMsg msg) throws ScriptException;
+
+ TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException;
+
+ boolean executeFilter(TbMsg msg) throws ScriptException;
+
+ Set<String> executeSwitch(TbMsg msg) throws ScriptException;
+
+ JsonNode executeJson(TbMsg msg) throws ScriptException;
+
+ String executeToString(TbMsg msg) throws ScriptException;
+
+ void destroy();
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
index 07cd72c..6038e6d 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
@@ -15,11 +15,23 @@
*/
package org.thingsboard.rule.engine.api;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.dao.alarm.AlarmService;
+import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.dao.user.UserService;
-import java.util.UUID;
+import java.util.Set;
/**
* Created by ashvayka on 13.01.18.
@@ -30,6 +42,8 @@ public interface TbContext {
void tellNext(TbMsg msg, String relationType);
+ void tellNext(TbMsg msg, Set<String> relationTypes);
+
void tellSelf(TbMsg msg, long delayMs);
void tellOthers(TbMsg msg);
@@ -38,8 +52,46 @@ public interface TbContext {
void spawn(TbMsg msg);
- void ack(UUID msg);
+ void ack(TbMsg msg);
+
+ void tellError(TbMsg msg, Throwable th);
+
+ void updateSelf(RuleNode self);
+
+ RuleNodeId getSelfId();
+
+ TenantId getTenantId();
AttributesService getAttributesService();
+ CustomerService getCustomerService();
+
+ UserService getUserService();
+
+ PluginService getPluginService();
+
+ AssetService getAssetService();
+
+ DeviceService getDeviceService();
+
+ AlarmService getAlarmService();
+
+ RuleChainService getRuleChainService();
+
+ RuleEngineTelemetryService getTelemetryService();
+
+ TimeseriesService getTimeseriesService();
+
+ RelationService getRelationService();
+
+ ListeningExecutor getJsExecutor();
+
+ ListeningExecutor getMailExecutor();
+
+ ListeningExecutor getDbCallbackExecutor();
+
+ MailService getMailService();
+
+ ScriptEngine createJsScriptEngine(String script, String functionName, String... argNames);
+
}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java
index 89442bb..2555c99 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java
@@ -24,7 +24,7 @@ import java.util.concurrent.ExecutionException;
*/
public interface TbNode {
- void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException;
+ void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException;
void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException;
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeConfiguration.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeConfiguration.java
index d06c0d2..64053cd 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeConfiguration.java
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeConfiguration.java
@@ -22,8 +22,8 @@ import lombok.Data;
* Created by ashvayka on 19.01.18.
*/
@Data
-public class TbNodeConfiguration {
+public final class TbNodeConfiguration {
- private JsonNode data;
+ private final JsonNode data;
}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java
index 6766999..b42ec8e 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java
@@ -22,6 +22,10 @@ import com.fasterxml.jackson.core.JsonProcessingException;
*/
public class TbNodeException extends Exception {
+ public TbNodeException(String message) {
+ super(message);
+ }
+
public TbNodeException(Exception e) {
super(e);
}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeState.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeState.java
index c48b11d..2c77a69 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeState.java
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeState.java
@@ -18,5 +18,5 @@ package org.thingsboard.rule.engine.api;
/**
* Created by ashvayka on 19.01.18.
*/
-public class TbNodeState {
+public final class TbNodeState {
}
rule-engine/rule-engine-components/pom.xml 20(+17 -3)
diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml
index 9b903b1..419b5f6 100644
--- a/rule-engine/rule-engine-components/pom.xml
+++ b/rule-engine/rule-engine-components/pom.xml
@@ -44,6 +44,11 @@
<scope>provided</scope>
</dependency>
<dependency>
+ <groupId>org.thingsboard.common</groupId>
+ <artifactId>transport</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<scope>provided</scope>
@@ -67,6 +72,16 @@
<artifactId>guava</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>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
@@ -89,9 +104,8 @@
<scope>test</scope>
</dependency>
<dependency>
- <groupId>org.junit.jupiter</groupId>
- <artifactId>junit-jupiter-api</artifactId>
- <version>RELEASE</version>
+ <groupId>org.thingsboard.common</groupId>
+ <artifactId>transport</artifactId>
</dependency>
<!--<dependency>-->
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java
new file mode 100644
index 0000000..0549e6a
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java
@@ -0,0 +1,220 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.action;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.util.concurrent.ExecutorService;
+
+import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.ACTION,
+ name = "alarm", relationTypes = {"Created", "Updated", "Cleared", "False"},
+ configClazz = TbAlarmNodeConfiguration.class,
+ nodeDescription = "Create/Update/Clear Alarm",
+ nodeDetails = "isAlarm - JS function that verifies if Alarm should be CREATED for incoming message.\n" +
+ "isCleared - JS function that verifies if Alarm should be CLEARED for incoming message.\n" +
+ "Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.\n" +
+ "Node output:\n" +
+ "If alarm was not created, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'matadata' will contains one of those properties 'isNewAlarm/isExistingAlarm/isClearedAlarm' " +
+ "Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" +
+ "Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbActionNodeAlarmConfig")
+
+public class TbAlarmNode implements TbNode {
+
+ static final String IS_NEW_ALARM = "isNewAlarm";
+ static final String IS_EXISTING_ALARM = "isExistingAlarm";
+ static final String IS_CLEARED_ALARM = "isClearedAlarm";
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ private TbAlarmNodeConfiguration config;
+ private ScriptEngine createJsEngine;
+ private ScriptEngine clearJsEngine;
+ private ScriptEngine buildDetailsJsEngine;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbAlarmNodeConfiguration.class);
+ this.createJsEngine = ctx.createJsScriptEngine(config.getCreateConditionJs(), "isAlarm");
+ this.clearJsEngine = ctx.createJsScriptEngine(config.getClearConditionJs(), "isCleared");
+ this.buildDetailsJsEngine = ctx.createJsScriptEngine(config.getAlarmDetailsBuildJs(), "Details");
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ ListeningExecutor jsExecutor = ctx.getJsExecutor();
+
+ ListenableFuture<Boolean> shouldCreate = jsExecutor.executeAsync(() -> createJsEngine.executeFilter(msg));
+ ListenableFuture<AlarmResult> transform = Futures.transform(shouldCreate, (AsyncFunction<Boolean, AlarmResult>) create -> {
+ if (create) {
+ return createOrUpdate(ctx, msg);
+ } else {
+ return checkForClearIfExist(ctx, msg);
+ }
+ }, ctx.getDbCallbackExecutor());
+
+ withCallback(transform,
+ alarmResult -> {
+ if (alarmResult.alarm == null) {
+ ctx.tellNext(msg, "False");
+ } else if (alarmResult.isCreated) {
+ ctx.tellNext(toAlarmMsg(alarmResult, msg), "Created");
+ } else if (alarmResult.isUpdated) {
+ ctx.tellNext(toAlarmMsg(alarmResult, msg), "Updated");
+ } else if (alarmResult.isCleared) {
+ ctx.tellNext(toAlarmMsg(alarmResult, msg), "Cleared");
+ }
+ },
+ t -> ctx.tellError(msg, t));
+
+ }
+
+ private ListenableFuture<AlarmResult> createOrUpdate(TbContext ctx, TbMsg msg) {
+ ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
+ return Futures.transform(latest, (AsyncFunction<Alarm, AlarmResult>) a -> {
+ if (a == null || a.getStatus().isCleared()) {
+ return createNewAlarm(ctx, msg);
+ } else {
+ return updateAlarm(ctx, msg, a);
+ }
+ }, ctx.getDbCallbackExecutor());
+ }
+
+ private ListenableFuture<AlarmResult> checkForClearIfExist(TbContext ctx, TbMsg msg) {
+ ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
+ return Futures.transform(latest, (AsyncFunction<Alarm, AlarmResult>) a -> {
+ if (a != null && !a.getStatus().isCleared()) {
+ return clearAlarm(ctx, msg, a);
+ }
+ return Futures.immediateFuture(new AlarmResult(false, false, false, null));
+ }, ctx.getDbCallbackExecutor());
+ }
+
+ private ListenableFuture<AlarmResult> createNewAlarm(TbContext ctx, TbMsg msg) {
+ ListenableFuture<Alarm> asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg),
+ (Function<JsonNode, Alarm>) details -> buildAlarm(msg, details, ctx.getTenantId()));
+ ListenableFuture<Alarm> asyncCreated = Futures.transform(asyncAlarm,
+ (Function<Alarm, Alarm>) alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor());
+ return Futures.transform(asyncCreated, (Function<Alarm, AlarmResult>) alarm -> new AlarmResult(true, false, false, alarm));
+ }
+
+ private ListenableFuture<AlarmResult> updateAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
+ ListenableFuture<Alarm> asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg), (Function<JsonNode, Alarm>) details -> {
+ alarm.setSeverity(config.getSeverity());
+ alarm.setPropagate(config.isPropagate());
+ alarm.setDetails(details);
+ alarm.setEndTs(System.currentTimeMillis());
+ return ctx.getAlarmService().createOrUpdateAlarm(alarm);
+ }, ctx.getDbCallbackExecutor());
+
+ return Futures.transform(asyncUpdated, (Function<Alarm, AlarmResult>) a -> new AlarmResult(false, true, false, a));
+ }
+
+ private ListenableFuture<AlarmResult> clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
+ ListenableFuture<Boolean> shouldClear = ctx.getJsExecutor().executeAsync(() -> clearJsEngine.executeFilter(msg));
+ return Futures.transform(shouldClear, (AsyncFunction<Boolean, AlarmResult>) clear -> {
+ if (clear) {
+ ListenableFuture<Boolean> clearFuture = ctx.getAlarmService().clearAlarm(alarm.getId(), System.currentTimeMillis());
+ return Futures.transform(clearFuture, (Function<Boolean, AlarmResult>) cleared -> {
+ alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK);
+ return new AlarmResult(false, false, true, alarm);
+ });
+ }
+ return Futures.immediateFuture(new AlarmResult(false, false, false, null));
+ });
+ }
+
+ private Alarm buildAlarm(TbMsg msg, JsonNode details, TenantId tenantId) {
+ return Alarm.builder()
+ .tenantId(tenantId)
+ .originator(msg.getOriginator())
+ .status(AlarmStatus.ACTIVE_UNACK)
+ .severity(config.getSeverity())
+ .propagate(config.isPropagate())
+ .type(config.getAlarmType())
+ //todo-vp: alarm date should be taken from Message or current Time should be used?
+// .startTs(System.currentTimeMillis())
+// .endTs(System.currentTimeMillis())
+ .details(details)
+ .build();
+ }
+
+ private ListenableFuture<JsonNode> buildAlarmDetails(TbContext ctx, TbMsg msg) {
+ return ctx.getJsExecutor().executeAsync(() -> buildDetailsJsEngine.executeJson(msg));
+ }
+
+ private TbMsg toAlarmMsg(AlarmResult alarmResult, TbMsg originalMsg) {
+ JsonNode jsonNodes = mapper.valueToTree(alarmResult.alarm);
+ String data = jsonNodes.toString();
+ TbMsgMetaData metaData = originalMsg.getMetaData().copy();
+ if (alarmResult.isCreated) {
+ metaData.putValue(IS_NEW_ALARM, Boolean.TRUE.toString());
+ } else if (alarmResult.isUpdated) {
+ metaData.putValue(IS_EXISTING_ALARM, Boolean.TRUE.toString());
+ } else if (alarmResult.isCleared) {
+ metaData.putValue(IS_CLEARED_ALARM, Boolean.TRUE.toString());
+ }
+ return new TbMsg(UUIDs.timeBased(), "ALARM", originalMsg.getOriginator(), metaData, data);
+ }
+
+
+ @Override
+ public void destroy() {
+ if (createJsEngine != null) {
+ createJsEngine.destroy();
+ }
+ if (clearJsEngine != null) {
+ clearJsEngine.destroy();
+ }
+ if (buildDetailsJsEngine != null) {
+ buildDetailsJsEngine.destroy();
+ }
+ }
+
+ private static class AlarmResult {
+ boolean isCreated;
+ boolean isUpdated;
+ boolean isCleared;
+ Alarm alarm;
+
+ AlarmResult(boolean isCreated, boolean isUpdated, boolean isCleared, Alarm alarm) {
+ this.isCreated = isCreated;
+ this.isUpdated = isUpdated;
+ this.isCleared = isCleared;
+ this.alarm = alarm;
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNodeConfiguration.java
new file mode 100644
index 0000000..3575459
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNodeConfiguration.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.action;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+
+@Data
+public class TbAlarmNodeConfiguration implements NodeConfiguration {
+
+ private String createConditionJs;
+ private String clearConditionJs;
+ private String alarmDetailsBuildJs;
+ private String alarmType;
+ private AlarmSeverity severity;
+ private boolean propagate;
+
+
+ @Override
+ public TbAlarmNodeConfiguration defaultConfiguration() {
+ TbAlarmNodeConfiguration configuration = new TbAlarmNodeConfiguration();
+ configuration.setCreateConditionJs("return 'incoming message = ' + msg + meta;");
+ configuration.setClearConditionJs("return 'incoming message = ' + msg + meta;");
+ configuration.setAlarmDetailsBuildJs("return 'incoming message = ' + msg + meta;");
+ configuration.setAlarmType("General Alarm");
+ configuration.setSeverity(AlarmSeverity.CRITICAL);
+ configuration.setPropagate(false);
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
new file mode 100644
index 0000000..3cd299d
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.action;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.ACTION,
+ name = "log",
+ configClazz = TbLogNodeConfiguration.class,
+ nodeDescription = "Log incoming messages using JS script for transformation Message into String",
+ nodeDetails = "Transform incoming Message with configured JS condition to String and log final value. " +
+ "Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" +
+ "Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbActionNodeLogConfig")
+
+public class TbLogNode implements TbNode {
+
+ private TbLogNodeConfiguration config;
+ private ScriptEngine jsEngine;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbLogNodeConfiguration.class);
+ this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "ToString");
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ ListeningExecutor jsExecutor = ctx.getJsExecutor();
+ withCallback(jsExecutor.executeAsync(() -> jsEngine.executeToString(msg)),
+ toString -> {
+ log.info(toString);
+ ctx.tellNext(msg);
+ },
+ t -> ctx.tellError(msg, t));
+ }
+
+ @Override
+ public void destroy() {
+ if (jsEngine != null) {
+ jsEngine.destroy();
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNodeConfiguration.java
new file mode 100644
index 0000000..aafb7f1
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNodeConfiguration.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.action;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbLogNodeConfiguration implements NodeConfiguration {
+
+ private String jsScript;
+
+ @Override
+ public TbLogNodeConfiguration defaultConfiguration() {
+ TbLogNodeConfiguration configuration = new TbLogNodeConfiguration();
+ configuration.setJsScript("return 'incoming message = ' + msg + meta;");
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java
new file mode 100644
index 0000000..1d944ec
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.data;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.EntityTypeFilter;
+
+import java.util.List;
+
+@Data
+public class RelationsQuery {
+
+ private EntitySearchDirection direction;
+ private int maxLevel = 1;
+ private List<EntityTypeFilter> filters;
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
new file mode 100644
index 0000000..65b1371
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.debug;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.ACTION,
+ name = "generator",
+ configClazz = TbMsgGeneratorNodeConfiguration.class,
+ nodeDescription = "Periodically generates messages",
+ nodeDetails = "Generates messages with configurable period. ",
+ inEnabled = false,
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbActionNodeGeneratorConfig"
+)
+
+public class TbMsgGeneratorNode implements TbNode {
+
+ public static final String TB_MSG_GENERATOR_NODE_MSG = "TbMsgGeneratorNodeMsg";
+
+ private TbMsgGeneratorNodeConfiguration config;
+ private ScriptEngine jsEngine;
+ private long delay;
+ private EntityId originatorId;
+ private UUID nextTickId;
+ private TbMsg prevMsg;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbMsgGeneratorNodeConfiguration.class);
+ this.delay = TimeUnit.SECONDS.toMillis(config.getPeriodInSeconds());
+ if (!StringUtils.isEmpty(config.getOriginatorId())) {
+ originatorId = EntityIdFactory.getByTypeAndUuid(config.getOriginatorType(), config.getOriginatorId());
+ } else {
+ originatorId = ctx.getSelfId();
+ }
+ this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "Generate", "prevMsg", "prevMetadata", "prevMsgType");
+ sentTickMsg(ctx);
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ if (msg.getType().equals(TB_MSG_GENERATOR_NODE_MSG) && msg.getId().equals(nextTickId)) {
+ withCallback(generate(ctx),
+ m -> {ctx.tellNext(m); sentTickMsg(ctx);},
+ t -> {ctx.tellError(msg, t); sentTickMsg(ctx);});
+ }
+ }
+
+ private void sentTickMsg(TbContext ctx) {
+ TbMsg tickMsg = new TbMsg(UUIDs.timeBased(), TB_MSG_GENERATOR_NODE_MSG, ctx.getSelfId(), new TbMsgMetaData(), "");
+ nextTickId = tickMsg.getId();
+ ctx.tellSelf(tickMsg, delay);
+ }
+
+ protected ListenableFuture<TbMsg> generate(TbContext ctx) {
+ return ctx.getJsExecutor().executeAsync(() -> {
+ if (prevMsg == null) {
+ prevMsg = new TbMsg(UUIDs.timeBased(), "", originatorId, new TbMsgMetaData(), "{}");
+ }
+ TbMsg generated = jsEngine.executeGenerate(prevMsg);
+ prevMsg = new TbMsg(UUIDs.timeBased(), generated.getType(), originatorId, generated.getMetaData(), generated.getData());
+ return prevMsg;
+ });
+ }
+
+ @Override
+ public void destroy() {
+ prevMsg = null;
+ if (jsEngine != null) {
+ jsEngine.destroy();
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java
new file mode 100644
index 0000000..c568e3d
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.debug;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.data.EntityType;
+
+import java.util.Map;
+
+@Data
+public class TbMsgGeneratorNodeConfiguration implements NodeConfiguration<TbMsgGeneratorNodeConfiguration> {
+
+ private int msgCount;
+ private int periodInSeconds;
+ private String originatorId;
+ private EntityType originatorType;
+ private String jsScript;
+
+ @Override
+ public TbMsgGeneratorNodeConfiguration defaultConfiguration() {
+ TbMsgGeneratorNodeConfiguration configuration = new TbMsgGeneratorNodeConfiguration();
+ configuration.setMsgCount(0);
+ configuration.setPeriodInSeconds(1);
+ configuration.setJsScript("var msg = { temp: 42, humidity: 77 };\n" +
+ "var metadata = { data: 40 };\n" +
+ "var msgType = \"DebugMsg\";\n\n" +
+ "return { msg: msg, metadata: metadata, msgType: msgType };");
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
new file mode 100644
index 0000000..8ad344f
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.FILTER,
+ name = "script", relationTypes = {"True", "False"},
+ configClazz = TbJsFilterNodeConfiguration.class,
+ nodeDescription = "Filter incoming messages using JS script",
+ nodeDetails = "Evaluate incoming Message with configured JS condition. " +
+ "If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." +
+ "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code><br/>" +
+ "Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code><br/>" +
+ "Message type can be accessed via <code>msgType</code> property.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbFilterNodeScriptConfig")
+
+public class TbJsFilterNode implements TbNode {
+
+ private TbJsFilterNodeConfiguration config;
+ private ScriptEngine jsEngine;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class);
+ this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "Filter");
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ ListeningExecutor jsExecutor = ctx.getJsExecutor();
+ withCallback(jsExecutor.executeAsync(() -> jsEngine.executeFilter(msg)),
+ filterResult -> ctx.tellNext(msg, Boolean.toString(filterResult)),
+ t -> ctx.tellError(msg, t));
+ }
+
+ @Override
+ public void destroy() {
+ if (jsEngine != null) {
+ jsEngine.destroy();
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java
new file mode 100644
index 0000000..9ab74e8
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.filter;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbJsFilterNodeConfiguration implements NodeConfiguration<TbJsFilterNodeConfiguration> {
+
+ private String jsScript;
+
+ @Override
+ public TbJsFilterNodeConfiguration defaultConfiguration() {
+ TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration();
+ configuration.setJsScript("return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 10 && msg.bigObj.prop == 42;");
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
new file mode 100644
index 0000000..3c6704b
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.Set;
+
+import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.FILTER,
+ name = "switch", customRelations = true,
+ configClazz = TbJsSwitchNodeConfiguration.class,
+ nodeDescription = "Route incoming Message to one or multiple output chains",
+ nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " +
+ "If Array is empty - message not routed to next Node. " +
+ "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code><br/>" +
+ "Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code><br/>" +
+ "Message type can be accessed via <code>msgType</code> property.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbFilterNodeSwitchConfig")
+public class TbJsSwitchNode implements TbNode {
+
+ private TbJsSwitchNodeConfiguration config;
+ private ScriptEngine jsEngine;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class);
+ this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "Switch");
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ ListeningExecutor jsExecutor = ctx.getJsExecutor();
+ withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(msg)),
+ result -> processSwitch(ctx, msg, result),
+ t -> ctx.tellError(msg, t));
+ }
+
+ private void processSwitch(TbContext ctx, TbMsg msg, Set<String> nextRelations) {
+ ctx.tellNext(msg, nextRelations);
+ }
+
+ @Override
+ public void destroy() {
+ if (jsEngine != null) {
+ jsEngine.destroy();
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java
new file mode 100644
index 0000000..79b0912
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.filter;
+
+import com.google.common.collect.Sets;
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Set;
+
+@Data
+public class TbJsSwitchNodeConfiguration implements NodeConfiguration<TbJsSwitchNodeConfiguration> {
+
+ private String jsScript;
+
+ @Override
+ public TbJsSwitchNodeConfiguration defaultConfiguration() {
+ TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration();
+ configuration.setJsScript("function nextRelation(metadata, msg) {\n" +
+ " return ['one','nine'];" +
+ "};\n" +
+ "\n" +
+ "return nextRelation(metadata, msg);");
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java
index 026da1b..225cd99 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java
@@ -18,18 +18,29 @@ package org.thingsboard.rule.engine.filter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.TbNodeUtils;
import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
/**
* Created by ashvayka on 19.01.18.
*/
@Slf4j
+@RuleNode(
+ type = ComponentType.FILTER,
+ name = "message type",
+ configClazz = TbMsgTypeFilterNodeConfiguration.class,
+ relationTypes = {"True", "False"},
+ nodeDescription = "Filter incoming messages by Message Type",
+ nodeDetails = "Evaluate incoming Message with configured JS condition. " +
+ "If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.",
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbFilterNodeMessageTypeConfig")
public class TbMsgTypeFilterNode implements TbNode {
TbMsgTypeFilterNodeConfiguration config;
@Override
- public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbMsgTypeFilterNodeConfiguration.class);
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java
index 3b7ba90..ae88aa8 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java
@@ -16,15 +16,24 @@
package org.thingsboard.rule.engine.filter;
import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
/**
* Created by ashvayka on 19.01.18.
*/
@Data
-public class TbMsgTypeFilterNodeConfiguration {
+public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration<TbMsgTypeFilterNodeConfiguration> {
private List<String> messageTypes;
+ @Override
+ public TbMsgTypeFilterNodeConfiguration defaultConfiguration() {
+ TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration();
+ configuration.setMessageTypes(Arrays.asList("POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST"));
+ return configuration;
+ }
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/EmailPojo.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/EmailPojo.java
new file mode 100644
index 0000000..35eaa3b
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/EmailPojo.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.mail;
+
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Builder
+class EmailPojo {
+
+ 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/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/RuleVelocityUtils.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/RuleVelocityUtils.java
new file mode 100644
index 0000000..7413cad
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/RuleVelocityUtils.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.mail;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.thingsboard.server.common.msg.TbMsg;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Map;
+
+import static org.thingsboard.server.common.msg.TbMsgDataType.JSON;
+
+public class RuleVelocityUtils {
+
+ public static VelocityContext createContext(TbMsg msg) throws IOException {
+ VelocityContext context = new VelocityContext();
+ context.put("originator", msg.getOriginator());
+ context.put("type", msg.getType());
+ context.put("metadata", msg.getMetaData().values());
+ if (msg.getDataType() == JSON) {
+ Map map = new ObjectMapper().readValue(msg.getData(), Map.class);
+ context.put("msg", map);
+ } else {
+ context.put("msg", msg.getData());
+ }
+ return context;
+ }
+
+ public static String merge(Template template, VelocityContext context) {
+ StringWriter writer = new StringWriter();
+ template.merge(context, writer);
+ return writer.toString();
+ }
+
+ 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;
+ }
+
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java
new file mode 100644
index 0000000..cae2058
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.mail;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.core.JsonProcessingException;
+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.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import static org.thingsboard.rule.engine.mail.TbSendEmailNode.SEND_EMAIL_TYPE;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.TRANSFORMATION,
+ name = "to email",
+ configClazz = TbMsgToEmailNodeConfiguration.class,
+ nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity",
+ nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
+ "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbTransformationNodeToEmailConfig")
+public class TbMsgToEmailNode implements TbNode {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private TbMsgToEmailNodeConfiguration config;
+
+ 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(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbMsgToEmailNodeConfiguration.class);
+ try {
+ fromTemplate = toTemplate(config.getFromTemplate(), "From Template");
+ toTemplate = toTemplate(config.getToTemplate(), "To Template");
+ ccTemplate = toTemplate(config.getCcTemplate(), "Cc Template");
+ bccTemplate = toTemplate(config.getBccTemplate(), "Bcc Template");
+ subjectTemplate = toTemplate(config.getSubjectTemplate(), "Subject Template");
+ bodyTemplate = toTemplate(config.getBodyTemplate(), "Body Template");
+ } catch (ParseException e) {
+ log.error("Failed to create templates based on provided configuration!", e);
+ throw new TbNodeException(e);
+ }
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ try {
+ EmailPojo email = convert(msg);
+ TbMsg emailMsg = buildEmailMsg(msg, email);
+ ctx.tellNext(emailMsg);
+ } catch (Exception ex) {
+ log.warn("Can not convert message to email " + ex.getMessage());
+ ctx.tellError(msg, ex);
+ }
+ }
+
+ private TbMsg buildEmailMsg(TbMsg msg, EmailPojo email) throws JsonProcessingException {
+ String emailJson = MAPPER.writeValueAsString(email);
+ return new TbMsg(UUIDs.timeBased(), SEND_EMAIL_TYPE, msg.getOriginator(), msg.getMetaData().copy(), emailJson);
+ }
+
+ private EmailPojo convert(TbMsg msg) throws IOException {
+ EmailPojo.EmailPojoBuilder builder = EmailPojo.builder();
+ VelocityContext context = RuleVelocityUtils.createContext(msg);
+ fromTemplate.ifPresent(t -> builder.from(RuleVelocityUtils.merge(t, context)));
+ toTemplate.ifPresent(t -> builder.to(RuleVelocityUtils.merge(t, context)));
+ ccTemplate.ifPresent(t -> builder.cc(RuleVelocityUtils.merge(t, context)));
+ bccTemplate.ifPresent(t -> builder.bcc(RuleVelocityUtils.merge(t, context)));
+ subjectTemplate.ifPresent(t -> builder.subject(RuleVelocityUtils.merge(t, context)));
+ bodyTemplate.ifPresent(t -> builder.body(RuleVelocityUtils.merge(t, context)));
+ return builder.build();
+ }
+
+ private Optional<Template> toTemplate(String source, String name) throws ParseException {
+ if (!StringUtils.isEmpty(source)) {
+ return Optional.of(RuleVelocityUtils.create(source, name));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java
new file mode 100644
index 0000000..6b0aa58
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.mail;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbMsgToEmailNodeConfiguration implements NodeConfiguration {
+
+ private String fromTemplate;
+ private String toTemplate;
+ private String ccTemplate;
+ private String bccTemplate;
+ private String subjectTemplate;
+ private String bodyTemplate;
+
+ @Override
+ public TbMsgToEmailNodeConfiguration defaultConfiguration() {
+ TbMsgToEmailNodeConfiguration configuration = new TbMsgToEmailNodeConfiguration();
+ configuration.fromTemplate = "info@testmail.org";
+ configuration.toTemplate = "$metadata.userEmail";
+ configuration.subjectTemplate = "Device $deviceType temperature high";
+ configuration.bodyTemplate = "Device $metadata.deviceName has high temperature $msg.temp";
+
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java
new file mode 100644
index 0000000..407476b
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.mail;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.io.IOException;
+
+import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.ACTION,
+ name = "send email",
+ configClazz = TbSendEmailNodeConfiguration.class,
+ nodeDescription = "Log incoming messages using JS script for transformation Message into String",
+ nodeDetails = "Transform incoming Message with configured JS condition to String and log final value. " +
+ "Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" +
+ "Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>")
+public class TbSendEmailNode implements TbNode {
+
+ static final String SEND_EMAIL_TYPE = "SEND_EMAIL";
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private TbSendEmailNodeConfiguration config;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbSendEmailNodeConfiguration.class);
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ try {
+ validateType(msg.getType());
+ EmailPojo email = getEmail(msg);
+ withCallback(ctx.getMailExecutor().executeAsync(() -> {
+ ctx.getMailService().send(email.getFrom(), email.getTo(), email.getCc(),
+ email.getBcc(), email.getSubject(), email.getBody());
+ return null;
+ }),
+ ok -> ctx.tellNext(msg),
+ fail -> ctx.tellError(msg, fail));
+ } catch (Exception ex) {
+ ctx.tellError(msg, ex);
+ }
+
+
+ }
+
+ private EmailPojo getEmail(TbMsg msg) throws IOException {
+ EmailPojo email = MAPPER.readValue(msg.getData(), EmailPojo.class);
+ if (StringUtils.isBlank(email.getTo())) {
+ throw new IllegalStateException("Email destination can not be blank [" + email.getTo() + "]");
+ }
+ return email;
+ }
+
+ private void validateType(String type) {
+ if (!SEND_EMAIL_TYPE.equals(type)) {
+ log.warn("Not expected msg type [{}] for SendEmail Node", type);
+ throw new IllegalStateException("Not expected msg type " + type + " for SendEmail Node");
+ }
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNodeConfiguration.java
new file mode 100644
index 0000000..4768b7d
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNodeConfiguration.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.mail;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbSendEmailNodeConfiguration implements NodeConfiguration {
+
+ private String tmp;
+
+ @Override
+ public TbSendEmailNodeConfiguration defaultConfiguration() {
+ TbSendEmailNodeConfiguration configuration = new TbSendEmailNodeConfiguration();
+ configuration.tmp = "";
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java
new file mode 100644
index 0000000..3bb1cff
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java
@@ -0,0 +1,92 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
+import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE;
+
+public abstract class TbEntityGetAttrNode<T extends EntityId> implements TbNode {
+
+ private TbGetEntityAttrNodeConfiguration config;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbGetEntityAttrNodeConfiguration.class);
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ try {
+ withCallback(
+ findEntityAsync(ctx, msg.getOriginator()),
+ entityId -> withCallback(
+ config.isTelemetry() ? getLatestTelemetry(ctx, entityId) : getAttributesAsync(ctx, entityId),
+ attributes -> putAttributesAndTell(ctx, msg, attributes),
+ t -> ctx.tellError(msg, t)
+ ),
+ t -> ctx.tellError(msg, t));
+ } catch (Throwable th) {
+ ctx.tellError(msg, th);
+ }
+ }
+
+ private ListenableFuture<List<KvEntry>> getAttributesAsync(TbContext ctx, EntityId entityId) {
+ ListenableFuture<List<AttributeKvEntry>> latest = ctx.getAttributesService().find(entityId, SERVER_SCOPE, config.getAttrMapping().keySet());
+ return Futures.transform(latest, (Function<? super List<AttributeKvEntry>, ? extends List<KvEntry>>) l ->
+ l.stream().map(i -> (KvEntry) i).collect(Collectors.toList()));
+ }
+
+ private ListenableFuture<List<KvEntry>> getLatestTelemetry(TbContext ctx, EntityId entityId) {
+ ListenableFuture<List<TsKvEntry>> latest = ctx.getTimeseriesService().findLatest(entityId, config.getAttrMapping().keySet());
+ return Futures.transform(latest, (Function<? super List<TsKvEntry>, ? extends List<KvEntry>>) l ->
+ l.stream().map(i -> (KvEntry) i).collect(Collectors.toList()));
+ }
+
+
+ private void putAttributesAndTell(TbContext ctx, TbMsg msg, List<? extends KvEntry> attributes) {
+ attributes.forEach(r -> {
+ String attrName = config.getAttrMapping().get(r.getKey());
+ msg.getMetaData().putValue(attrName, r.getValueAsString());
+ });
+ ctx.tellNext(msg);
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+
+ protected abstract ListenableFuture<T> findEntityAsync(TbContext ctx, EntityId originator);
+
+ public void setConfig(TbGetEntityAttrNodeConfiguration config) {
+ this.config = config;
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
index 11c644c..84cff22 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
@@ -15,49 +15,82 @@
*/
package org.thingsboard.rule.engine.metadata;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.CollectionUtils;
import org.thingsboard.rule.engine.TbNodeUtils;
import org.thingsboard.rule.engine.api.*;
-import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
-import org.thingsboard.server.dao.attributes.AttributesService;
import java.util.List;
+import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
+import static org.thingsboard.server.common.data.DataConstants.*;
+
/**
* Created by ashvayka on 19.01.18.
*/
@Slf4j
+@RuleNode(type = ComponentType.ENRICHMENT,
+ name = "originator attributes",
+ configClazz = TbGetAttributesNodeConfiguration.class,
+ nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata",
+ nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
+ "with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " +
+ "<code>metadata.cs_temperature</code> or <code>metadata.shared_limit</code> " +
+ "If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.",
+ uiResources = {"static/rulenode/rulenode-core-config.js"},
+ configDirective = "tbEnrichmentNodeOriginatorAttributesConfig")
public class TbGetAttributesNode implements TbNode {
- TbGetAttributesNodeConfiguration config;
+ private TbGetAttributesNodeConfiguration config;
@Override
- public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbGetAttributesNodeConfiguration.class);
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
- try {
- //TODO: refactor this to work async and fetch attributes from cache.
- AttributesService service = ctx.getAttributesService();
- fetchAttributes(msg, service, config.getClientAttributeNames(), DataConstants.CLIENT_SCOPE, "cs.");
- fetchAttributes(msg, service, config.getServerAttributeNames(), DataConstants.SERVER_SCOPE, "ss.");
- fetchAttributes(msg, service, config.getSharedAttributeNames(), DataConstants.SHARED_SCOPE, "shared.");
- ctx.tellNext(msg);
- } catch (Exception e) {
- log.warn("[{}][{}] Failed to fetch attributes", msg.getOriginator(), msg.getId(), e);
- throw new TbNodeException(e);
+ if (CollectionUtils.isNotEmpty(config.getLatestTsKeyNames())) {
+ withCallback(getLatestTelemetry(ctx, msg, config.getLatestTsKeyNames()),
+ i -> ctx.tellNext(msg),
+ t -> ctx.tellError(msg, t));
+ } else {
+ ListenableFuture<List<Void>> future = Futures.allAsList(
+ putAttrAsync(ctx, msg, CLIENT_SCOPE, config.getClientAttributeNames(), "cs_"),
+ putAttrAsync(ctx, msg, SHARED_SCOPE, config.getSharedAttributeNames(), "shared_"),
+ putAttrAsync(ctx, msg, SERVER_SCOPE, config.getServerAttributeNames(), "ss_"));
+
+ withCallback(future, i -> ctx.tellNext(msg), t -> ctx.tellError(msg, t));
+ }
+ }
+
+ private ListenableFuture<Void> putAttrAsync(TbContext ctx, TbMsg msg, String scope, List<String> keys, String prefix) {
+ if (keys == null) {
+ return Futures.immediateFuture(null);
}
+ ListenableFuture<List<AttributeKvEntry>> latest = ctx.getAttributesService().find(msg.getOriginator(), scope, keys);
+ return Futures.transform(latest, (Function<? super List<AttributeKvEntry>, Void>) l -> {
+ l.forEach(r -> msg.getMetaData().putValue(prefix + r.getKey(), r.getValueAsString()));
+ return null;
+ });
}
- private void fetchAttributes(TbMsg msg, AttributesService service, List<String> attributeNames, String scope, String prefix) throws InterruptedException, java.util.concurrent.ExecutionException {
- if (attributeNames != null && attributeNames.isEmpty()) {
- List<AttributeKvEntry> attributes = service.find(msg.getOriginator(), scope, attributeNames).get();
- attributes.forEach(attr -> msg.getMetaData().putValue(prefix + attr.getKey(), attr.getValueAsString()));
+ private ListenableFuture<Void> getLatestTelemetry(TbContext ctx, TbMsg msg, List<String> keys) {
+ if (keys == null) {
+ return Futures.immediateFuture(null);
}
+ ListenableFuture<List<TsKvEntry>> latest = ctx.getTimeseriesService().findLatest(msg.getOriginator(), keys);
+ return Futures.transform(latest, (Function<? super List<TsKvEntry>, Void>) l -> {
+ l.forEach(r -> msg.getMetaData().putValue(r.getKey(), r.getValueAsString()));
+ return null;
+ });
}
@Override
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
index b54edef..6cd2247 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
@@ -16,17 +16,30 @@
package org.thingsboard.rule.engine.metadata;
import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import java.util.Collections;
import java.util.List;
/**
* Created by ashvayka on 19.01.18.
*/
@Data
-public class TbGetAttributesNodeConfiguration {
+public class TbGetAttributesNodeConfiguration implements NodeConfiguration<TbGetAttributesNodeConfiguration> {
private List<String> clientAttributeNames;
private List<String> sharedAttributeNames;
private List<String> serverAttributeNames;
+ private List<String> latestTsKeyNames;
+
+ @Override
+ public TbGetAttributesNodeConfiguration defaultConfiguration() {
+ TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
+ configuration.setClientAttributeNames(Collections.emptyList());
+ configuration.setSharedAttributeNames(Collections.emptyList());
+ configuration.setServerAttributeNames(Collections.emptyList());
+ configuration.setLatestTsKeyNames(Collections.emptyList());
+ return configuration;
+ }
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
new file mode 100644
index 0000000..b092bad
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+@RuleNode(
+ type = ComponentType.ENRICHMENT,
+ name="customer attributes",
+ configClazz = TbGetEntityAttrNodeConfiguration.class,
+ nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata",
+ nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
+ "To access those attributes in other nodes this template can be used " +
+ "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata",
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbEnrichmentNodeCustomerAttributesConfig")
+public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> {
+
+ @Override
+ protected ListenableFuture<CustomerId> findEntityAsync(TbContext ctx, EntityId originator) {
+ return EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, originator);
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java
new file mode 100644
index 0000000..0bcefae
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+@Data
+public class TbGetEntityAttrNodeConfiguration implements NodeConfiguration<TbGetEntityAttrNodeConfiguration> {
+
+ private Map<String, String> attrMapping;
+ private boolean isTelemetry = false;
+
+ @Override
+ public TbGetEntityAttrNodeConfiguration defaultConfiguration() {
+ TbGetEntityAttrNodeConfiguration configuration = new TbGetEntityAttrNodeConfiguration();
+ Map<String, String> attrMapping = new HashMap<>();
+ attrMapping.putIfAbsent("temperature", "tempo");
+ configuration.setAttrMapping(attrMapping);
+ configuration.setTelemetry(true);
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
new file mode 100644
index 0000000..8f65c31
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader;
+
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+@RuleNode(
+ type = ComponentType.ENRICHMENT,
+ name="related attributes",
+ configClazz = TbGetRelatedAttrNodeConfiguration.class,
+ nodeDescription = "Add Originators Related Entity Attributes or Latest Telemetry into Message Metadata",
+ nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
+ "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " +
+ "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
+ "To access those attributes in other nodes this template can be used " +
+ "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata",
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbEnrichmentNodeRelatedAttributesConfig")
+
+public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
+
+ private TbGetRelatedAttrNodeConfiguration config;
+
+ @Override
+ public void init(TbContext context, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbGetRelatedAttrNodeConfiguration.class);
+ setConfig(config);
+ }
+
+ @Override
+ protected ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator) {
+ return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, config.getRelationsQuery());
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java
new file mode 100644
index 0000000..dccd878
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.data.RelationsQuery;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.EntityTypeFilter;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+@Data
+public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration {
+
+ private RelationsQuery relationsQuery;
+
+ @Override
+ public TbGetRelatedAttrNodeConfiguration defaultConfiguration() {
+ TbGetRelatedAttrNodeConfiguration configuration = new TbGetRelatedAttrNodeConfiguration();
+ Map<String, String> attrMapping = new HashMap<>();
+ attrMapping.putIfAbsent("temperature", "tempo");
+ configuration.setAttrMapping(attrMapping);
+ configuration.setTelemetry(true);
+
+ RelationsQuery relationsQuery = new RelationsQuery();
+ relationsQuery.setDirection(EntitySearchDirection.FROM);
+ relationsQuery.setMaxLevel(1);
+ EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
+ relationsQuery.setFilters(Collections.singletonList(entityTypeFilter));
+ configuration.setRelationsQuery(relationsQuery);
+
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java
new file mode 100644
index 0000000..f0d28d3
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.util.EntitiesTenantIdAsyncLoader;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.ENRICHMENT,
+ name="tenant attributes",
+ configClazz = TbGetEntityAttrNodeConfiguration.class,
+ nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata",
+ nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
+ "To access those attributes in other nodes this template can be used " +
+ "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata",
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbEnrichmentNodeTenantAttributesConfig")
+public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> {
+
+ @Override
+ protected ListenableFuture<TenantId> findEntityAsync(TbContext ctx, EntityId originator) {
+ return EntitiesTenantIdAsyncLoader.findEntityIdAsync(ctx, originator);
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTelemetryNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTelemetryNode.java
new file mode 100644
index 0000000..0e6687f
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTelemetryNode.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.telemetry;
+
+import com.google.gson.JsonParser;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNode;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.ACTION,
+ name = "save timeseries data",
+ configClazz = TbMsgTelemetryNodeConfiguration.class,
+ nodeDescription = "Saves timeseries data",
+ nodeDetails = "Saves timeseries telemetry data based on configurable TTL parameter. Expects messages with 'POST_TELEMETRY' message type",
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbActionNodeTelemetryConfig"
+)
+
+public class TbMsgTelemetryNode implements TbNode {
+
+ private TbMsgTelemetryNodeConfiguration config;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbMsgTelemetryNodeConfiguration.class);
+ }
+
+ @Override
+ public void onMsg(TbContext ctx, TbMsg msg) {
+ if (!msg.getType().equals("POST_TELEMETRY")) {
+ ctx.tellError(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType()));
+ return;
+ }
+
+ String src = msg.getData();
+ TelemetryUploadRequest telemetryUploadRequest = JsonConverter.convertToTelemetry(new JsonParser().parse(src));
+ Map<Long, List<KvEntry>> tsKvMap = telemetryUploadRequest.getData();
+ if (tsKvMap == null) {
+ ctx.tellError(msg, new IllegalArgumentException("Msg body us empty: " + src));
+ return;
+ }
+ List<TsKvEntry> tsKvEntryList = new ArrayList<>();
+ for (Map.Entry<Long, List<KvEntry>> tsKvEntry : tsKvMap.entrySet()) {
+ for (KvEntry kvEntry : tsKvEntry.getValue()) {
+ tsKvEntryList.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry));
+ }
+ }
+ String ttlValue = msg.getMetaData().getValue("TTL");
+ long ttl = !StringUtils.isEmpty(ttlValue) ? Long.valueOf(ttlValue) : config.getDefaultTTL();
+ ctx.getTelemetryService().saveAndNotify(msg.getOriginator(), tsKvEntryList, ttl, new TelemetryNodeCallback(ctx, msg));
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTelemetryNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTelemetryNodeConfiguration.java
new file mode 100644
index 0000000..5523926
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTelemetryNodeConfiguration.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.telemetry;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Map;
+
+@Data
+public class TbMsgTelemetryNodeConfiguration implements NodeConfiguration<TbMsgTelemetryNodeConfiguration> {
+
+ private long defaultTTL;
+
+ @Override
+ public TbMsgTelemetryNodeConfiguration defaultConfiguration() {
+ TbMsgTelemetryNodeConfiguration configuration = new TbMsgTelemetryNodeConfiguration();
+ configuration.setDefaultTTL(0L);
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TelemetryNodeCallback.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TelemetryNodeCallback.java
new file mode 100644
index 0000000..fab4942
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TelemetryNodeCallback.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.telemetry;
+
+import com.google.common.util.concurrent.FutureCallback;
+import lombok.Data;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import javax.annotation.Nullable;
+
+/**
+ * Created by ashvayka on 02.04.18.
+ */
+@Data
+class TelemetryNodeCallback implements FutureCallback<Void> {
+ private final TbContext ctx;
+ private final TbMsg msg;
+
+ @Override
+ public void onSuccess(@Nullable Void result) {
+ ctx.tellNext(msg);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ ctx.tellError(msg, t);
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
new file mode 100644
index 0000000..05ad49c
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
@@ -0,0 +1,100 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.transform;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader;
+import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader;
+import org.thingsboard.rule.engine.util.EntitiesTenantIdAsyncLoader;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.HashSet;
+
+@Slf4j
+@RuleNode(
+ type = ComponentType.TRANSFORMATION,
+ name="change originator",
+ configClazz = TbChangeOriginatorNodeConfiguration.class,
+ nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity",
+ nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
+ "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ",
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbTransformationNodeChangeOriginatorConfig")
+public class TbChangeOriginatorNode extends TbAbstractTransformNode {
+
+ protected static final String CUSTOMER_SOURCE = "CUSTOMER";
+ protected static final String TENANT_SOURCE = "TENANT";
+ protected static final String RELATED_SOURCE = "RELATED";
+
+ private TbChangeOriginatorNodeConfiguration config;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbChangeOriginatorNodeConfiguration.class);
+ validateConfig(config);
+ setConfig(config);
+ }
+
+ @Override
+ protected ListenableFuture<TbMsg> transform(TbContext ctx, TbMsg msg) {
+ ListenableFuture<? extends EntityId> newOriginator = getNewOriginator(ctx, msg.getOriginator());
+ return Futures.transform(newOriginator, (Function<EntityId, TbMsg>) n -> new TbMsg(msg.getId(), msg.getType(), n, msg.getMetaData(), msg.getData()));
+ }
+
+ private ListenableFuture<? extends EntityId> getNewOriginator(TbContext ctx, EntityId original) {
+ switch (config.getOriginatorSource()) {
+ case CUSTOMER_SOURCE:
+ return EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, original);
+ case TENANT_SOURCE:
+ return EntitiesTenantIdAsyncLoader.findEntityIdAsync(ctx, original);
+ case RELATED_SOURCE:
+ return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, original, config.getRelationsQuery());
+ default:
+ return Futures.immediateFailedFuture(new IllegalStateException("Unexpected originator source " + config.getOriginatorSource()));
+ }
+ }
+
+ private void validateConfig(TbChangeOriginatorNodeConfiguration conf) {
+ HashSet<String> knownSources = Sets.newHashSet(CUSTOMER_SOURCE, TENANT_SOURCE, RELATED_SOURCE);
+ if (!knownSources.contains(conf.getOriginatorSource())) {
+ log.error("Unsupported source [{}] for TbChangeOriginatorNode", conf.getOriginatorSource());
+ throw new IllegalArgumentException("Unsupported source TbChangeOriginatorNode" + conf.getOriginatorSource());
+ }
+
+ if (conf.getOriginatorSource().equals(RELATED_SOURCE)) {
+ if (conf.getRelationsQuery() == null) {
+ log.error("Related source for TbChangeOriginatorNode should have relations query. Actual [{}]",
+ conf.getRelationsQuery());
+ throw new IllegalArgumentException("Wrong config for RElated Source in TbChangeOriginatorNode" + conf.getOriginatorSource());
+ }
+ }
+
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java
new file mode 100644
index 0000000..7cd77bf
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.transform;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.rule.engine.data.RelationsQuery;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.EntityTypeFilter;
+
+import java.util.Collections;
+
+@Data
+public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
+
+ private String originatorSource;
+
+ private RelationsQuery relationsQuery;
+
+ @Override
+ public TbChangeOriginatorNodeConfiguration defaultConfiguration() {
+ TbChangeOriginatorNodeConfiguration configuration = new TbChangeOriginatorNodeConfiguration();
+ configuration.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE);
+
+ RelationsQuery relationsQuery = new RelationsQuery();
+ relationsQuery.setDirection(EntitySearchDirection.FROM);
+ relationsQuery.setMaxLevel(1);
+ EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
+ relationsQuery.setFilters(Collections.singletonList(entityTypeFilter));
+ configuration.setRelationsQuery(relationsQuery);
+
+ configuration.setStartNewChain(false);
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
new file mode 100644
index 0000000..bf0c9fe
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.transform;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+@RuleNode(
+ type = ComponentType.TRANSFORMATION,
+ name = "script",
+ configClazz = TbTransformMsgNodeConfiguration.class,
+ nodeDescription = "Change Message payload, Metadata or Message type using JavaScript",
+ nodeDetails = "JavaScript function receive 3 input parameters.<br/> " +
+ "<code>metadata</code> - is a Message metadata.<br/>" +
+ "<code>msg</code> - is a Message payload.<br/>" +
+ "<code>msgType</code> - is a Message type.<br/>" +
+ "Should return the following structure:<br/>" +
+ "<code>{ msg: <i style=\"color: #666;\">new payload</i>,<br/>   metadata: <i style=\"color: #666;\">new metadata</i>,<br/>   msgType: <i style=\"color: #666;\">new msgType</i> }</code><br/>" +
+ "All fields in resulting object are optional and will be taken from original message if not specified.",
+ uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+ configDirective = "tbTransformationNodeScriptConfig")
+public class TbTransformMsgNode extends TbAbstractTransformNode {
+
+ private TbTransformMsgNodeConfiguration config;
+ private ScriptEngine jsEngine;
+
+ @Override
+ public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+ this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class);
+ this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "Transform");
+ setConfig(config);
+ }
+
+ @Override
+ protected ListenableFuture<TbMsg> transform(TbContext ctx, TbMsg msg) {
+ return ctx.getJsExecutor().executeAsync(() -> jsEngine.executeUpdate(msg));
+ }
+
+ @Override
+ public void destroy() {
+ if (jsEngine != null) {
+ jsEngine.destroy();
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java
new file mode 100644
index 0000000..a710cf8
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.transform;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
+
+ private String jsScript;
+
+ @Override
+ public TbTransformMsgNodeConfiguration defaultConfiguration() {
+ TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration();
+ configuration.setStartNewChain(false);
+ configuration.setJsScript("return {msg: msg, metadata: metadata, msgType: msgType};");
+ return configuration;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java
new file mode 100644
index 0000000..67eb808
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.util;
+
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.HasCustomerId;
+import org.thingsboard.server.common.data.id.*;
+
+public class EntitiesCustomerIdAsyncLoader {
+
+
+ public static ListenableFuture<CustomerId> findEntityIdAsync(TbContext ctx, EntityId original) {
+
+ switch (original.getEntityType()) {
+ case CUSTOMER:
+ return Futures.immediateFuture((CustomerId) original);
+ case USER:
+ return getCustomerAsync(ctx.getUserService().findUserByIdAsync((UserId) original));
+ case ASSET:
+ return getCustomerAsync(ctx.getAssetService().findAssetByIdAsync((AssetId) original));
+ case DEVICE:
+ return getCustomerAsync(ctx.getDeviceService().findDeviceByIdAsync((DeviceId) original));
+ default:
+ return Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original));
+ }
+ }
+
+ private static <T extends HasCustomerId> ListenableFuture<CustomerId> getCustomerAsync(ListenableFuture<T> future) {
+ return Futures.transform(future, (AsyncFunction<HasCustomerId, CustomerId>) in -> {
+ return in != null ? Futures.immediateFuture(in.getCustomerId())
+ : Futures.immediateFailedFuture(new IllegalStateException("Customer not found"));});
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java
new file mode 100644
index 0000000..08ce38e
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.util;
+
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.apache.commons.collections.CollectionUtils;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.data.RelationsQuery;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
+import org.thingsboard.server.dao.relation.RelationService;
+
+import java.util.List;
+
+public class EntitiesRelatedEntityIdAsyncLoader {
+
+ public static ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator,
+ RelationsQuery relationsQuery) {
+ RelationService relationService = ctx.getRelationService();
+ EntityRelationsQuery query = buildQuery(originator, relationsQuery);
+ ListenableFuture<List<EntityRelation>> asyncRelation = relationService.findByQuery(query);
+ if (relationsQuery.getDirection() == EntitySearchDirection.FROM) {
+ return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>)
+ r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getTo())
+ : Futures.immediateFailedFuture(new IllegalStateException("Relation not found")));
+ } else if (relationsQuery.getDirection() == EntitySearchDirection.TO) {
+ return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>)
+ r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getFrom())
+ : Futures.immediateFailedFuture(new IllegalStateException("Relation not found")));
+ }
+ return Futures.immediateFailedFuture(new IllegalStateException("Unknown direction"));
+ }
+
+ private static EntityRelationsQuery buildQuery(EntityId originator, RelationsQuery relationsQuery) {
+ EntityRelationsQuery query = new EntityRelationsQuery();
+ RelationsSearchParameters parameters = new RelationsSearchParameters(originator,
+ relationsQuery.getDirection(), relationsQuery.getMaxLevel());
+ query.setParameters(parameters);
+ query.setFilters(relationsQuery.getFilters());
+ return query;
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java
new file mode 100644
index 0000000..5d2aaa8
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.util;
+
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.HasTenantId;
+import org.thingsboard.server.common.data.alarm.AlarmId;
+import org.thingsboard.server.common.data.id.*;
+
+public class EntitiesTenantIdAsyncLoader {
+
+ public static ListenableFuture<TenantId> findEntityIdAsync(TbContext ctx, EntityId original) {
+
+ switch (original.getEntityType()) {
+ case TENANT:
+ return Futures.immediateFuture((TenantId) original);
+ case CUSTOMER:
+ return getTenantAsync(ctx.getCustomerService().findCustomerByIdAsync((CustomerId) original));
+ case USER:
+ return getTenantAsync(ctx.getUserService().findUserByIdAsync((UserId) original));
+ case PLUGIN:
+ return getTenantAsync(ctx.getPluginService().findPluginByIdAsync((PluginId) original));
+ case ASSET:
+ return getTenantAsync(ctx.getAssetService().findAssetByIdAsync((AssetId) original));
+ case DEVICE:
+ return getTenantAsync(ctx.getDeviceService().findDeviceByIdAsync((DeviceId) original));
+ case ALARM:
+ return getTenantAsync(ctx.getAlarmService().findAlarmByIdAsync((AlarmId) original));
+ case RULE_CHAIN:
+ return getTenantAsync(ctx.getRuleChainService().findRuleChainByIdAsync((RuleChainId) original));
+ default:
+ return Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original));
+ }
+ }
+
+ private static <T extends HasTenantId> ListenableFuture<TenantId> getTenantAsync(ListenableFuture<T> future) {
+ return Futures.transform(future, (AsyncFunction<HasTenantId, TenantId>) in -> {
+ return in != null ? Futures.immediateFuture(in.getTenantId())
+ : Futures.immediateFailedFuture(new IllegalStateException("Tenant not found"));});
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css
new file mode 100644
index 0000000..b1728b1
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css
@@ -0,0 +1,2 @@
+.tb-message-type-autocomplete .tb-not-found{display:block;line-height:1.5;height:48px}.tb-message-type-autocomplete .tb-not-found .tb-no-entries{line-height:48px}.tb-message-type-autocomplete li{height:auto!important;white-space:normal!important}.tb-generator-config tb-json-content.tb-message-body,.tb-generator-config tb-json-object-edit.tb-metadata-json{height:200px;display:block}.tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}.tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:rgba(0,0,0,.54);font-size:12px;font-weight:700;white-space:nowrap}.tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}.tb-kv-map-config .body .row{padding-top:5px;max-height:40px}.tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}.tb-kv-map-config .body md-input-container.cell{margin:0;max-height:40px}.tb-kv-map-config .body .md-button{margin:0}
+/*# sourceMappingURL=rulenode-core-config.css.map*/
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
new file mode 100644
index 0000000..edf7ddc
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
@@ -0,0 +1,3 @@
+!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}(function(e){for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t))switch(typeof e[t]){case"function":break;case"object":e[t]=function(t){var n=t.slice(1),r=e[t[0]];return function(e,t,a){r.apply(this,[e,t,a].concat(n))}}(e[t]);break;default:e[t]=e[e[t]]}return e}([function(e,t,n){e.exports=n(40)},function(e,t){},1,1,function(e,t){e.exports=" <section class=tb-alarm-config ng-form name=alarmConfigForm layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-create-condition</label> <tb-js-func ng-model=configuration.createConditionJs function-name=isAlarm function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=\"testConditionJs($event, true)\" class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-condition-function' | translate }} </md-button> </div> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-clear-condition</label> <tb-js-func ng-model=configuration.clearConditionJs function-name=isCleared function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=\"testConditionJs($event, false)\" class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-condition-function' | translate }} </md-button> </div> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-details-builder</label> <tb-js-func ng-model=configuration.alarmDetailsBuildJs function-name=Details function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testDetailsBuildJs($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-details-function' | translate }} </md-button> </div> <section layout=column layout-gt-sm=row> <md-input-container flex class=md-block> <label translate>tb.rulenode.alarm-type</label> <input ng-required=true name=alarmType ng-model=configuration.alarmType> <div ng-messages=alarmConfigForm.alarmType.$error> <div ng-message=required translate>tb.rulenode.alarm-type-required</div> </div> </md-input-container> <md-input-container flex class=md-block> <label translate>tb.rulenode.alarm-severity</label> <md-select required name=severity ng-model=configuration.severity> <md-option ng-repeat=\"(severityKey, severity) in types.alarmSeverity\" ng-value=severityKey> {{ severity.name | translate}} </md-option> </md-select> <div ng-messages=alarmConfigForm.severity.$error> <div ng-message=required translate>tb.rulenode.alarm-severity-required</div> </div> </md-input-container> </section> <md-checkbox aria-label=\"{{ 'tb.rulenode.propagate' | translate }}\" ng-model=configuration.propagate>{{ 'tb.rulenode.propagate' | translate }} </md-checkbox> </section> "},function(e,t){e.exports=" <section class=tb-generator-config ng-form name=generatorConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.message-count</label> <input ng-required=true type=number step=1 name=messageCount ng-model=configuration.msgCount min=0> <div ng-messages=generatorConfigForm.messageCount.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.message-count-required</div> <div ng-message=min translate>tb.rulenode.min-message-count-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.period-seconds</label> <input ng-required=true type=number step=1 name=periodInSeconds ng-model=configuration.periodInSeconds min=1> <div ng-messages=generatorConfigForm.periodInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.period-seconds-required</div> <div ng-message=min translate>tb.rulenode.min-period-seconds-message</div> </div> </md-input-container> <div layout=column> <label class=tb-small>{{ 'tb.rulenode.originator' | translate }}</label> <tb-entity-select the-form=generatorConfigForm tb-required=false ng-model=originator> </tb-entity-select> </div> <label translate class=\"tb-title no-padding\">tb.rulenode.generate</label> <tb-js-func ng-model=configuration.jsScript function-name=Generate function-args=\"{{ ['prevMsg', 'prevMetadata', 'prevMsgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-generator-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.to-string</label> <tb-js-func ng-model=configuration.jsScript function-name=ToString function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-to-string-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section ng-form name=telemetryConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.default-ttl</label> <input ng-required=true type=number step=1 name=defaultTTL ng-model=configuration.defaultTTL min=0> <div ng-messages=telemetryConfigForm.defaultTTL.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.default-ttl-required</div> <div ng-message=min translate>tb.rulenode.min-default-ttl-message</div> </div> </md-input-container> </section> "},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> "},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding">tb.rulenode.client-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.clientAttributeNames placeholder="{{\'tb.rulenode.client-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.shared-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.sharedAttributeNames placeholder="{{\'tb.rulenode.shared-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.server-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.serverAttributeNames placeholder="{{\'tb.rulenode.server-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> "},8,function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding" ng-class="{\'tb-required\': required}">tb.rulenode.message-types-filter</label> <md-chips id=message_type_chips ng-required=required readonly=readonly ng-model=messageTypes md-autocomplete-snap md-transform-chip=transformMessageTypeChip($chip) md-require-match=false> <md-autocomplete id=message_type md-no-cache=true md-selected-item=selectedMessageType md-search-text=messageTypeSearchText md-items="item in messageTypesSearch(messageTypeSearchText)" md-item-text=item.name md-min-length=0 placeholder="{{\'tb.rulenode.message-type\' | translate }}" md-menu-class=tb-message-type-autocomplete> <span md-highlight-text=messageTypeSearchText md-highlight-flags=^i>{{item}}</span> <md-not-found> <div class=tb-not-found> <div class=tb-no-entries ng-if="!messageTypeSearchText || !messageTypeSearchText.length"> <span translate>tb.rulenode.no-message-types-found</span> </div> <div ng-if="messageTypeSearchText && messageTypeSearchText.length"> <span translate translate-values=\'{ messageType: "{{messageTypeSearchText | truncate:true:6:'...'}}" }\'>tb.rulenode.no-message-type-matching</span> <span> <a translate ng-click="createMessageType($event, \'#message_type_chips\')">tb.rulenode.create-new-message-type</a> </span> </div> </div> </md-not-found> </md-autocomplete> <md-chip-template> <span>{{$chip.name}}</span> </md-chip-template> </md-chips> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=messageTypes class=tb-error-message>tb.rulenode.message-types-required</div> </div> </section>'},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.filter</label> <tb-js-func ng-model=configuration.jsScript function-name=Filter function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-filter-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.switch</label> <tb-js-func ng-model=configuration.jsScript function-name=Switch function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-switch-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section class=tb-kv-map-config layout=column> <div class=header flex layout=row> <span class=cell flex translate>{{ keyText }}</span> <span class=cell flex translate>{{ valText }}</span> <span ng-show=!disabled style=width:52px> </span> </div> <div class=body> <div class=row ng-form name=kvForm flex layout=row layout-align="start center" ng-repeat="keyVal in kvList track by $index"> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ keyText | translate }}" ng-required=true name=key ng-model=keyVal.key> <div ng-messages=kvForm.key.$error> <div translate ng-message=required>{{keyRequiredText}}</div> </div> </md-input-container> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ valText | translate }}" ng-required=true name=value ng-model=keyVal.value> <div ng-messages=kvForm.value.$error> <div translate ng-message=required>{{valRequiredText}}</div> </div> </md-input-container> <md-button ng-show=!disabled ng-disabled=loading class="md-icon-button md-primary" ng-click=removeKeyVal($index) aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.remove-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.delete\' | translate }}" class=material-icons> close </md-icon> </md-button> </div> </div> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=kvMap class=tb-error-message>{{requiredText}}</div> </div> <div> <md-button ng-show=!disabled ng-disabled=loading class="md-primary md-raised" ng-click=addKeyVal() aria-label="{{ \'action.add\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.add-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.add\' | translate }}" class=material-icons> add </md-icon> {{ \'action.add\' | translate }} </md-button> </div> </section> '},function(e,t){e.exports=" <section layout=column> <div layout=row> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=query.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-relation-level</label> <input name=maxRelationLevel type=number min=1 step=1 placeholder=\"{{ 'tb.rulenode.unlimited-level' | translate }}\" ng-model=query.maxLevel aria-label=\"{{ 'tb.rulenode.max-relation-level' | translate }}\"> </md-input-container> </div> <div class=md-caption style=padding-bottom:10px;color:rgba(0,0,0,.57) translate>relation.relation-filters</div> <tb-relation-filters ng-model=query.filters> </tb-relation-filters> </section> "},function(e,t){e.exports=' <section layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.originator-source</label> <md-select required ng-model=configuration.originatorSource> <md-option ng-repeat="source in ruleNodeTypes.originatorSource" ng-value=source.value> {{ source.name | translate}} </md-option> </md-select> </md-input-container> <section layout=column ng-if="configuration.originatorSource == ruleNodeTypes.originatorSource.RELATED.value"> <label translate class="tb-title tb-required">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> </section> <md-checkbox aria-label="{{ \'tb.rulenode.clone-message\' | translate }}" ng-model=configuration.startNewChain>{{ \'tb.rulenode.clone-message\' | translate }} </md-checkbox> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.transform</label> <tb-js-func ng-model=configuration.jsScript function-name=Transform function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-transformer-function' | translate }} </md-button> </div> <md-checkbox aria-label=\"{{ 'tb.rulenode.clone-message' | translate }}\" ng-model=configuration.startNewChain>{{ 'tb.rulenode.clone-message' | translate }} </md-checkbox> </section> "},function(e,t){e.exports=" <section ng-form name=toEmailConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.from-template</label> <textarea ng-required=true name=fromTemplate ng-model=configuration.fromTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.fromTemplate.$error> <div ng-message=required translate>tb.rulenode.from-template-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.to-template</label> <textarea ng-required=true name=toTemplate ng-model=configuration.toTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.toTemplate.$error> <div ng-message=required translate>tb.rulenode.to-template-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.cc-template</label> <textarea name=ccTemplate ng-model=configuration.ccTemplate rows=2></textarea> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.bcc-template</label> <textarea name=ccTemplate ng-model=configuration.bccTemplate rows=2></textarea> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.subject-template</label> <textarea ng-required=true name=subjectTemplate ng-model=configuration.subjectTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.subjectTemplate.$error> <div ng-message=required translate>tb.rulenode.subject-template-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.body-template</label> <textarea ng-required=true name=bodyTemplate ng-model=configuration.bodyTemplate rows=6></textarea> <div ng-messages=toEmailConfigForm.bodyTemplate.$error> <div ng-message=required translate>tb.rulenode.body-template-required</div> </div> </md-input-container> </section> "},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,r){var a=function(a,i,l,s){var u=o.default;i.html(u),a.types=n,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue},a.testConditionJs=function(e,n){var i=angular.copy(n?a.configuration.createConditionJs:a.configuration.clearConditionJs),o={temperature:22.4,humidity:78},l={sensorType:"temperature"};r.testNodeScript(e,i,"filter",t.instant("tb.rulenode.condition")+"",n?"isAlarm":"isCleared",["msg","metadata","msgType"],o,l,"POST_TELEMETRY").then(function(e){n?a.configuration.createConditionJs=e:a.configuration.clearConditionJs=e,s.$setDirty()})},a.testDetailsBuildJs=function(e){var n=angular.copy(a.configuration.alarmDetailsBuildJs),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};r.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){a.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(i.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(4),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,r){var a=function(a,i,l,s){var u=o.default;i.html(u),a.types=n,a.originator=null,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue,a.configuration.originatorId&&a.configuration.originatorType?a.originator={id:a.configuration.originatorId,entityType:a.configuration.originatorType}:a.originator=null,a.$watch("originator",function(e,t){angular.equals(e,t)||(a.originator?(s.$viewValue.originatorId=a.originator.id,s.$viewValue.originatorType=a.originator.entityType):(s.$viewValue.originatorId=null,s.$viewValue.originatorType=null))},!0)},a.testScript=function(e){var n=angular.copy(a.configuration.jsScript),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};r.testNodeScript(e,n,"generate",t.instant("tb.rulenode.generator")+"","Generate",["prevMsg","prevMetadata","prevMsgType"],i,o,"DebugMsg").then(function(e){a.configuration.jsScript=e,s.$setDirty()})},e(i.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(1);var i=n(5),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(24),i=r(a),o=n(21),l=r(o),s=n(20),u=r(s),d=n(23),c=r(d);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTelemetryConfig",i.default).directive("tbActionNodeGeneratorConfig",l.default).directive("tbActionNodeAlarmConfig",u.default).directive("tbActionNodeLogConfig",c.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};n.testNodeScript(e,a,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(6),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(7),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(8),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(27),i=r(a),o=n(28),l=r(o),s=n(25),u=r(s),d=n(29),c=r(d);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",i.default).directive("tbEnrichmentNodeRelatedAttributesConfig",l.default).directive("tbEnrichmentNodeCustomerAttributesConfig",u.default).directive("tbEnrichmentNodeTenantAttributesConfig",c.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(9),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(10),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(11),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(32),i=r(a),o=n(31),l=r(o),s=n(33),u=r(s);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",i.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",u.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){function s(){if(l.$viewValue){for(var e=[],t=0;t<r.messageTypes.length;t++)e.push(r.messageTypes[t].value);l.$viewValue.messageTypes=e,u()}}function u(){if(r.required){var e=!(!l.$viewValue.messageTypes||!l.$viewValue.messageTypes.length);l.$setValidity("messageTypes",e)}else l.$setValidity("messageTypes",!0)}var d=o.default;a.html(d),r.selectedMessageType=null,r.messageTypeSearchText=null,r.ngModelCtrl=l;var c=[];for(var m in n.messageType){var g={name:n.messageType[m].name,value:n.messageType[m].value};c.push(g)}r.transformMessageTypeChip=function(e){var n,r=t("filter")(c,{name:e},!0);return n=r&&r.length?angular.copy(r[0]):{name:e,value:e}},r.messageTypesSearch=function(e){var n=e?t("filter")(c,{name:e}):c;return n.map(function(e){return e.name})},r.createMessageType=function(e,t){var n=angular.element(t,a)[0].firstElementChild,r=angular.element(n),i=r.scope().$mdChipsCtrl.getChipBuffer();e.preventDefault(),e.stopPropagation(),r.scope().$mdChipsCtrl.appendChip(i.trim()),r.scope().$mdChipsCtrl.resetChipBuffer()},l.$render=function(){var e=l.$viewValue,t=[];if(e&&e.messageTypes)for(var a=0;a<e.messageTypes.length;a++){var i=e.messageTypes[a];n.messageType[i]?t.push(angular.copy(n.messageType[i])):t.push({name:i,value:i})}r.messageTypes=t,r.$watch("messageTypes",function(e,t){angular.equals(e,t)||s()},!0)},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",readonly:"=ngReadonly"},link:r}}a.$inject=["$compile","$filter","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(2);var i=n(12),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript),i={passed:12,name:"Vit",bigObj:{prop:42}},o={temp:10};n.testNodeScript(e,a,"filter",t.instant("tb.rulenode.filter")+"","Filter",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(13),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};n.testNodeScript(e,a,"switch",t.instant("tb.rulenode.switch")+"","Switch",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(14),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){function i(e){e>-1&&t.kvList.splice(e,1)}function l(){t.kvList||(t.kvList=[]),t.kvList.push({key:"",value:""})}function s(){var e={};t.kvList.forEach(function(t){t.key&&(e[t.key]=t.value)}),a.$setViewValue(e),u()}function u(){var e=!0;t.required&&!t.kvList.length&&(e=!1),a.$setValidity("kvMap",e)}var d=o.default;n.html(d),t.ngModelCtrl=a,t.removeKeyVal=i,t.addKeyVal=l,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||a.$setViewValue(t.query)}),a.$render=function(){if(a.$viewValue){var e=a.$viewValue;t.kvList.length=0;for(var n in e)t.kvList.push({key:n,value:e[n]})}t.$watch("kvList",function(e,t){angular.equals(e,t)||s()},!0),u()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(15),o=r(i);n(3)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(16),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(17),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(36),i=r(a),o=n(38),l=r(o),s=n(39),u=r(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",i.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",u.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};n.testNodeScript(e,a,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(18),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(19),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{
+default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(43),i=r(a),o=n(30),l=r(o),s=n(26),u=r(s),d=n(37),c=r(d),m=n(22),g=r(m),f=n(35),p=r(f),b=n(34),v=r(b),y=n(42),T=r(y);t.default=angular.module("thingsboard.ruleChain.config",[i.default,l.default,u.default,c.default,g.default]).directive("tbRelationsQueryConfig",p.default).directive("tbKvMapConfig",v.default).config(T.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","latest-timeseries":"Latest timeseries","relations-query":"Relations query","max-relation-level":"Max relation level","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","to-template":"To Template","to-template-required":"To Template is required","cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","body-template":"Body Template","body-template-required":"Body Template is required"},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};angular.merge(e.en_US,t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){(0,o.default)(t);for(var n in t){var r=t[n];e.translations(n,r)}}a.$inject=["$translateProvider","locales"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(41),o=r(i)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{messageType:{POST_ATTRIBUTES:{name:"Post attributes",value:"POST_ATTRIBUTES"},POST_TELEMETRY:{name:"Post telemetry",value:"POST_TELEMETRY"},RPC_REQUEST:{name:"RPC Request",value:"RPC_REQUEST"}},originatorSource:{CUSTOMER:{name:"tb.rulenode.originator-customer",value:"CUSTOMER"},TENANT:{name:"tb.rulenode.originator-tenant",value:"TENANT"},RELATED:{name:"tb.rulenode.originator-related",value:"RELATED"}}}).name}]));
+//# sourceMappingURL=rulenode-core-config.js.map
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java
new file mode 100644
index 0000000..69c3aca
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java
@@ -0,0 +1,341 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.action;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.apache.commons.lang3.NotImplementedException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.alarm.AlarmService;
+
+import javax.script.ScriptException;
+import java.io.IOException;
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.*;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.*;
+import static org.thingsboard.rule.engine.action.TbAlarmNode.*;
+import static org.thingsboard.server.common.data.alarm.AlarmSeverity.CRITICAL;
+import static org.thingsboard.server.common.data.alarm.AlarmSeverity.WARNING;
+import static org.thingsboard.server.common.data.alarm.AlarmStatus.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbAlarmNodeTest {
+
+ private TbAlarmNode node;
+
+ @Mock
+ private TbContext ctx;
+ @Mock
+ private ListeningExecutor executor;
+ @Mock
+ private AlarmService alarmService;
+
+ @Mock
+ private ScriptEngine createJs;
+ @Mock
+ private ScriptEngine clearJs;
+ @Mock
+ private ScriptEngine detailsJs;
+
+ private EntityId originator = new DeviceId(UUIDs.timeBased());
+ private TenantId tenantId = new TenantId(UUIDs.timeBased());
+ private TbMsgMetaData metaData = new TbMsgMetaData();
+ private String rawJson = "{\"name\": \"Vit\", \"passed\": 5}";
+
+ @Test
+ public void newAlarmCanBeCreated() throws ScriptException, IOException {
+ initWithScript();
+ metaData.putValue("key", "value");
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
+
+ when(createJs.executeFilter(msg)).thenReturn(true);
+ when(detailsJs.executeJson(msg)).thenReturn(null);
+ when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null));
+
+ doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class));
+
+ node.onMsg(ctx, msg);
+
+ ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+ verify(ctx).tellNext(captor.capture(), eq("Created"));
+ TbMsg actualMsg = captor.getValue();
+
+ assertEquals("ALARM", actualMsg.getType());
+ assertEquals(originator, actualMsg.getOriginator());
+ assertEquals("value", actualMsg.getMetaData().getValue("key"));
+ assertEquals(Boolean.TRUE.toString(), actualMsg.getMetaData().getValue(IS_NEW_ALARM));
+ assertNotSame(metaData, actualMsg.getMetaData());
+
+ Alarm actualAlarm = new ObjectMapper().readValue(actualMsg.getData().getBytes(), Alarm.class);
+ Alarm expectedAlarm = Alarm.builder()
+ .tenantId(tenantId)
+ .originator(originator)
+ .status(ACTIVE_UNACK)
+ .severity(CRITICAL)
+ .propagate(true)
+ .type("SomeType")
+ .details(null)
+ .build();
+
+ assertEquals(expectedAlarm, actualAlarm);
+
+ verify(executor, times(2)).executeAsync(any(Callable.class));
+ }
+
+ @Test
+ public void shouldCreateScriptThrowsException() throws ScriptException {
+ initWithScript();
+ metaData.putValue("key", "value");
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
+
+ when(createJs.executeFilter(msg)).thenThrow(new NotImplementedException("message"));
+
+ node.onMsg(ctx, msg);
+
+ verifyError(msg, "message", NotImplementedException.class);
+
+
+ verify(ctx).createJsScriptEngine("CREATE", "isAlarm");
+ verify(ctx).createJsScriptEngine("CLEAR", "isCleared");
+ verify(ctx).createJsScriptEngine("DETAILS", "Details");
+ verify(ctx).getJsExecutor();
+
+ verifyNoMoreInteractions(ctx, alarmService, clearJs, detailsJs);
+ }
+
+ @Test
+ public void buildDetailsThrowsException() throws ScriptException, IOException {
+ initWithScript();
+ metaData.putValue("key", "value");
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
+
+ when(createJs.executeFilter(msg)).thenReturn(true);
+ when(detailsJs.executeJson(msg)).thenThrow(new NotImplementedException("message"));
+ when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null));
+
+ node.onMsg(ctx, msg);
+
+ verifyError(msg, "message", NotImplementedException.class);
+
+ verify(ctx).createJsScriptEngine("CREATE", "isAlarm");
+ verify(ctx).createJsScriptEngine("CLEAR", "isCleared");
+ verify(ctx).createJsScriptEngine("DETAILS", "Details");
+ verify(ctx, times(2)).getJsExecutor();
+ verify(ctx).getAlarmService();
+ verify(ctx).getTenantId();
+ verify(alarmService).findLatestByOriginatorAndType(tenantId, originator, "SomeType");
+
+ verifyNoMoreInteractions(ctx, alarmService, clearJs);
+ }
+
+ @Test
+ public void ifAlarmClearedCreateNew() throws ScriptException, IOException {
+ initWithScript();
+ metaData.putValue("key", "value");
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
+
+ Alarm clearedAlarm = Alarm.builder().status(CLEARED_ACK).build();
+
+ when(createJs.executeFilter(msg)).thenReturn(true);
+ when(detailsJs.executeJson(msg)).thenReturn(null);
+ when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(clearedAlarm));
+
+ doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class));
+
+ node.onMsg(ctx, msg);
+
+ ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+ verify(ctx).tellNext(captor.capture(), eq("Created"));
+ TbMsg actualMsg = captor.getValue();
+
+ assertEquals("ALARM", actualMsg.getType());
+ assertEquals(originator, actualMsg.getOriginator());
+ assertEquals("value", actualMsg.getMetaData().getValue("key"));
+ assertEquals(Boolean.TRUE.toString(), actualMsg.getMetaData().getValue(IS_NEW_ALARM));
+ assertNotSame(metaData, actualMsg.getMetaData());
+
+ Alarm actualAlarm = new ObjectMapper().readValue(actualMsg.getData().getBytes(), Alarm.class);
+ Alarm expectedAlarm = Alarm.builder()
+ .tenantId(tenantId)
+ .originator(originator)
+ .status(ACTIVE_UNACK)
+ .severity(CRITICAL)
+ .propagate(true)
+ .type("SomeType")
+ .details(null)
+ .build();
+
+ assertEquals(expectedAlarm, actualAlarm);
+
+ verify(executor, times(2)).executeAsync(any(Callable.class));
+ }
+
+ @Test
+ public void alarmCanBeUpdated() throws ScriptException, IOException {
+ initWithScript();
+ metaData.putValue("key", "value");
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
+
+ long oldEndDate = System.currentTimeMillis();
+ Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build();
+
+ when(createJs.executeFilter(msg)).thenReturn(true);
+ when(clearJs.executeFilter(msg)).thenReturn(false);
+ when(detailsJs.executeJson(msg)).thenReturn(null);
+ when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm));
+
+ doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm);
+
+ node.onMsg(ctx, msg);
+
+ ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+ verify(ctx).tellNext(captor.capture(), eq("Updated"));
+ TbMsg actualMsg = captor.getValue();
+
+ assertEquals("ALARM", actualMsg.getType());
+ assertEquals(originator, actualMsg.getOriginator());
+ assertEquals("value", actualMsg.getMetaData().getValue("key"));
+ assertEquals(Boolean.TRUE.toString(), actualMsg.getMetaData().getValue(IS_EXISTING_ALARM));
+ assertNotSame(metaData, actualMsg.getMetaData());
+
+ Alarm actualAlarm = new ObjectMapper().readValue(actualMsg.getData().getBytes(), Alarm.class);
+ assertTrue(activeAlarm.getEndTs() > oldEndDate);
+ Alarm expectedAlarm = Alarm.builder()
+ .tenantId(tenantId)
+ .originator(originator)
+ .status(ACTIVE_UNACK)
+ .severity(CRITICAL)
+ .propagate(true)
+ .type("SomeType")
+ .details(null)
+ .endTs(activeAlarm.getEndTs())
+ .build();
+
+ assertEquals(expectedAlarm, actualAlarm);
+
+ verify(executor, times(2)).executeAsync(any(Callable.class));
+ }
+
+ @Test
+ public void alarmCanBeCleared() throws ScriptException, IOException {
+ initWithScript();
+ metaData.putValue("key", "value");
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
+
+ long oldEndDate = System.currentTimeMillis();
+ Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build();
+
+ when(createJs.executeFilter(msg)).thenReturn(false);
+ when(clearJs.executeFilter(msg)).thenReturn(true);
+// when(detailsJs.executeJson(msg)).thenReturn(null);
+ when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm));
+ when(alarmService.clearAlarm(eq(activeAlarm.getId()), anyLong())).thenReturn(Futures.immediateFuture(true));
+// doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm);
+
+ node.onMsg(ctx, msg);
+
+ ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+ verify(ctx).tellNext(captor.capture(), eq("Cleared"));
+ TbMsg actualMsg = captor.getValue();
+
+ assertEquals("ALARM", actualMsg.getType());
+ assertEquals(originator, actualMsg.getOriginator());
+ assertEquals("value", actualMsg.getMetaData().getValue("key"));
+ assertEquals(Boolean.TRUE.toString(), actualMsg.getMetaData().getValue(IS_CLEARED_ALARM));
+ assertNotSame(metaData, actualMsg.getMetaData());
+
+ Alarm actualAlarm = new ObjectMapper().readValue(actualMsg.getData().getBytes(), Alarm.class);
+ Alarm expectedAlarm = Alarm.builder()
+ .tenantId(tenantId)
+ .originator(originator)
+ .status(CLEARED_UNACK)
+ .severity(WARNING)
+ .propagate(false)
+ .type("SomeType")
+ .details(null)
+ .endTs(oldEndDate)
+ .build();
+
+ assertEquals(expectedAlarm, actualAlarm);
+ }
+
+ private void initWithScript() {
+ try {
+ TbAlarmNodeConfiguration config = new TbAlarmNodeConfiguration();
+ config.setPropagate(true);
+ config.setSeverity(CRITICAL);
+ config.setAlarmType("SomeType");
+ config.setCreateConditionJs("CREATE");
+ config.setClearConditionJs("CLEAR");
+ config.setAlarmDetailsBuildJs("DETAILS");
+ ObjectMapper mapper = new ObjectMapper();
+ TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+ when(ctx.createJsScriptEngine("CREATE", "isAlarm")).thenReturn(createJs);
+ when(ctx.createJsScriptEngine("CLEAR", "isCleared")).thenReturn(clearJs);
+ when(ctx.createJsScriptEngine("DETAILS", "Details")).thenReturn(detailsJs);
+
+ when(ctx.getTenantId()).thenReturn(tenantId);
+ when(ctx.getJsExecutor()).thenReturn(executor);
+ when(ctx.getAlarmService()).thenReturn(alarmService);
+
+ mockJsExecutor();
+
+ node = new TbAlarmNode();
+ node.init(ctx, nodeConfiguration);
+ } catch (TbNodeException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ private void mockJsExecutor() {
+ when(ctx.getJsExecutor()).thenReturn(executor);
+ doAnswer((Answer<ListenableFuture<Boolean>>) invocationOnMock -> {
+ try {
+ Callable task = (Callable) (invocationOnMock.getArguments())[0];
+ return Futures.immediateFuture((Boolean) task.call());
+ } catch (Throwable th) {
+ return Futures.immediateFailedFuture(th);
+ }
+ }).when(executor).executeAsync(any(Callable.class));
+ }
+
+ private void verifyError(TbMsg msg, String message, Class expectedClass) {
+ ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+ verify(ctx).tellError(same(msg), captor.capture());
+
+ Throwable value = captor.getValue();
+ assertEquals(expectedClass, value.getClass());
+ assertEquals(message, value.getMessage());
+ }
+
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java
new file mode 100644
index 0000000..08a22f0
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java
@@ -0,0 +1,121 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.filter;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbJsFilterNodeTest {
+
+ private TbJsFilterNode node;
+
+ @Mock
+ private TbContext ctx;
+ @Mock
+ private ListeningExecutor executor;
+ @Mock
+ private ScriptEngine scriptEngine;
+
+ @Test
+ public void falseEvaluationDoNotSendMsg() throws TbNodeException, ScriptException {
+ initWithScript();
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}");
+ mockJsExecutor();
+ when(scriptEngine.executeFilter(msg)).thenReturn(false);
+
+ node.onMsg(ctx, msg);
+ verify(ctx).getJsExecutor();
+ verify(ctx).tellNext(msg, "false");
+ }
+
+ @Test
+ public void exceptionInJsThrowsException() throws TbNodeException, ScriptException {
+ initWithScript();
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}");
+ mockJsExecutor();
+ when(scriptEngine.executeFilter(msg)).thenThrow(new ScriptException("error"));
+
+
+ node.onMsg(ctx, msg);
+ verifyError(msg, "error", ScriptException.class);
+ }
+
+ @Test
+ public void metadataConditionCanBeTrue() throws TbNodeException, ScriptException {
+ initWithScript();
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}");
+ mockJsExecutor();
+ when(scriptEngine.executeFilter(msg)).thenReturn(true);
+
+ node.onMsg(ctx, msg);
+ verify(ctx).getJsExecutor();
+ verify(ctx).tellNext(msg, "true");
+ }
+
+ private void initWithScript() throws TbNodeException {
+ TbJsFilterNodeConfiguration config = new TbJsFilterNodeConfiguration();
+ config.setJsScript("scr");
+ ObjectMapper mapper = new ObjectMapper();
+ TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+ when(ctx.createJsScriptEngine("scr", "Filter")).thenReturn(scriptEngine);
+
+ node = new TbJsFilterNode();
+ node.init(ctx, nodeConfiguration);
+ }
+
+ private void mockJsExecutor() {
+ when(ctx.getJsExecutor()).thenReturn(executor);
+ doAnswer((Answer<ListenableFuture<Boolean>>) invocationOnMock -> {
+ try {
+ Callable task = (Callable) (invocationOnMock.getArguments())[0];
+ return Futures.immediateFuture((Boolean) task.call());
+ } catch (Throwable th) {
+ return Futures.immediateFailedFuture(th);
+ }
+ }).when(executor).executeAsync(Matchers.any(Callable.class));
+ }
+
+ private void verifyError(TbMsg msg, String message, Class expectedClass) {
+ ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+ verify(ctx).tellError(same(msg), captor.capture());
+
+ Throwable value = captor.getValue();
+ assertEquals(expectedClass, value.getClass());
+ assertEquals(message, value.getMessage());
+ }
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java
new file mode 100644
index 0000000..a495124
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java
@@ -0,0 +1,103 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.filter;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbJsSwitchNodeTest {
+
+ private TbJsSwitchNode node;
+
+ @Mock
+ private TbContext ctx;
+ @Mock
+ private ListeningExecutor executor;
+ @Mock
+ private ScriptEngine scriptEngine;
+
+ @Test
+ public void multipleRoutesAreAllowed() throws TbNodeException, ScriptException {
+ initWithScript();
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "10");
+ metaData.putValue("humidity", "99");
+ String rawJson = "{\"name\": \"Vit\", \"passed\": 5}";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+ mockJsExecutor();
+ when(scriptEngine.executeSwitch(msg)).thenReturn(Sets.newHashSet("one", "three"));
+
+ node.onMsg(ctx, msg);
+ verify(ctx).getJsExecutor();
+ verify(ctx).tellNext(msg, Sets.newHashSet("one", "three"));
+ }
+
+ private void initWithScript() throws TbNodeException {
+ TbJsSwitchNodeConfiguration config = new TbJsSwitchNodeConfiguration();
+ config.setJsScript("scr");
+ ObjectMapper mapper = new ObjectMapper();
+ TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+ when(ctx.createJsScriptEngine("scr", "Switch")).thenReturn(scriptEngine);
+
+ node = new TbJsSwitchNode();
+ node.init(ctx, nodeConfiguration);
+ }
+
+ private void mockJsExecutor() {
+ when(ctx.getJsExecutor()).thenReturn(executor);
+ doAnswer((Answer<ListenableFuture<Set<String>>>) invocationOnMock -> {
+ try {
+ Callable task = (Callable) (invocationOnMock.getArguments())[0];
+ return Futures.immediateFuture((Set<String>) task.call());
+ } catch (Throwable th) {
+ return Futures.immediateFailedFuture(th);
+ }
+ }).when(executor).executeAsync(Matchers.any(Callable.class));
+ }
+
+ private void verifyError(TbMsg msg, String message, Class expectedClass) {
+ ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+ verify(ctx).tellError(same(msg), captor.capture());
+
+ Throwable value = captor.getValue();
+ assertEquals(expectedClass, value.getClass());
+ assertEquals(message, value.getMessage());
+ }
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java
new file mode 100644
index 0000000..877047c
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.mail;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.mockito.Mockito.verify;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbMsgToEmailNodeTest {
+
+ private TbMsgToEmailNode emailNode;
+
+ @Mock
+ private TbContext ctx;
+
+ private EntityId originator = new DeviceId(UUIDs.timeBased());
+ private TbMsgMetaData metaData = new TbMsgMetaData();
+ private String rawJson = "{\"name\": \"temp\", \"passed\": 5 , \"complex\": {\"val\":12, \"count\":100}}";
+
+ @Test
+ public void msgCanBeConverted() throws IOException {
+ initWithScript();
+ metaData.putValue("username", "oreo");
+ metaData.putValue("userEmail", "user@email.io");
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
+
+ emailNode.onMsg(ctx, msg);
+
+ ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+ verify(ctx).tellNext(captor.capture());
+ TbMsg actualMsg = captor.getValue();
+
+ assertEquals("SEND_EMAIL", actualMsg.getType());
+ assertEquals(originator, actualMsg.getOriginator());
+ assertEquals("oreo", actualMsg.getMetaData().getValue("username"));
+ assertNotSame(metaData, actualMsg.getMetaData());
+
+
+ EmailPojo actual = new ObjectMapper().readValue(actualMsg.getData().getBytes(), EmailPojo.class);
+
+ EmailPojo expected = new EmailPojo.EmailPojoBuilder()
+ .from("test@mail.org")
+ .to("user@email.io")
+ .subject("Hi oreo there")
+ .body("temp is to high. Current 5 and 100")
+ .build();
+ assertEquals(expected, actual);
+ }
+
+ private void initWithScript() {
+ try {
+ TbMsgToEmailNodeConfiguration config = new TbMsgToEmailNodeConfiguration();
+ config.setFromTemplate("test@mail.org");
+ config.setToTemplate("$metadata.userEmail");
+ config.setSubjectTemplate("Hi $metadata.username there");
+ config.setBodyTemplate("$msg.name is to high. Current $msg.passed and $msg.complex.count");
+ ObjectMapper mapper = new ObjectMapper();
+ TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+ emailNode = new TbMsgToEmailNode();
+ emailNode.init(ctx, nodeConfiguration);
+ } catch (TbNodeException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java
new file mode 100644
index 0000000..e26312b
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java
@@ -0,0 +1,264 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Futures;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.dao.user.UserService;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbGetCustomerAttributeNodeTest {
+
+ private TbGetCustomerAttributeNode node;
+
+ @Mock
+ private TbContext ctx;
+
+ @Mock
+ private AttributesService attributesService;
+ @Mock
+ private TimeseriesService timeseriesService;
+ @Mock
+ private UserService userService;
+ @Mock
+ private AssetService assetService;
+ @Mock
+ private DeviceService deviceService;
+
+ private TbMsg msg;
+
+ @Before
+ public void init() throws TbNodeException {
+ TbGetEntityAttrNodeConfiguration config = new TbGetEntityAttrNodeConfiguration();
+ Map<String, String> attrMapping = new HashMap<>();
+ attrMapping.putIfAbsent("temperature", "tempo");
+ config.setAttrMapping(attrMapping);
+ config.setTelemetry(false);
+ ObjectMapper mapper = new ObjectMapper();
+ TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+ node = new TbGetCustomerAttributeNode();
+ node.init(null, nodeConfiguration);
+ }
+
+ @Test
+ public void errorThrownIfCannotLoadAttributes() {
+ UserId userId = new UserId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ User user = new User();
+ user.setCustomerId(customerId);
+
+ msg = new TbMsg(UUIDs.timeBased(), "USER", userId, new TbMsgMetaData(), "{}");
+
+ when(ctx.getUserService()).thenReturn(userService);
+ when(userService.findUserByIdAsync(userId)).thenReturn(Futures.immediateFuture(user));
+
+ when(ctx.getAttributesService()).thenReturn(attributesService);
+ when(attributesService.find(customerId, SERVER_SCOPE, Collections.singleton("temperature")))
+ .thenThrow(new IllegalStateException("something wrong"));
+
+ node.onMsg(ctx, msg);
+ final ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+ verify(ctx).tellError(same(msg), captor.capture());
+
+ Throwable value = captor.getValue();
+ assertEquals("something wrong", value.getMessage());
+ assertTrue(msg.getMetaData().getData().isEmpty());
+ }
+
+ @Test
+ public void errorThrownIfCannotLoadAttributesAsync() {
+ UserId userId = new UserId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ User user = new User();
+ user.setCustomerId(customerId);
+
+ msg = new TbMsg(UUIDs.timeBased(), "USER", userId, new TbMsgMetaData(), "{}");
+
+ when(ctx.getUserService()).thenReturn(userService);
+ when(userService.findUserByIdAsync(userId)).thenReturn(Futures.immediateFuture(user));
+
+ when(ctx.getAttributesService()).thenReturn(attributesService);
+ when(attributesService.find(customerId, SERVER_SCOPE, Collections.singleton("temperature")))
+ .thenReturn(Futures.immediateFailedFuture(new IllegalStateException("something wrong")));
+
+ node.onMsg(ctx, msg);
+ final ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+ verify(ctx).tellError(same(msg), captor.capture());
+
+ Throwable value = captor.getValue();
+ assertEquals("something wrong", value.getMessage());
+ assertTrue(msg.getMetaData().getData().isEmpty());
+ }
+
+ @Test
+ public void errorThrownIfCustomerCannotBeFound() {
+ UserId userId = new UserId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ User user = new User();
+ user.setCustomerId(customerId);
+
+ msg = new TbMsg(UUIDs.timeBased(), "USER", userId, new TbMsgMetaData(), "{}");
+
+ when(ctx.getUserService()).thenReturn(userService);
+ when(userService.findUserByIdAsync(userId)).thenReturn(Futures.immediateFuture(null));
+
+ node.onMsg(ctx, msg);
+ final ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+ verify(ctx).tellError(same(msg), captor.capture());
+
+ Throwable value = captor.getValue();
+ assertEquals(IllegalStateException.class, value.getClass());
+ assertEquals("Customer not found", value.getMessage());
+ assertTrue(msg.getMetaData().getData().isEmpty());
+ }
+
+ @Test
+ public void customerAttributeAddedInMetadata() {
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ msg = new TbMsg(UUIDs.timeBased(), "CUSTOMER", customerId, new TbMsgMetaData(), "{}");
+ entityAttributeFetched(customerId);
+ }
+
+ @Test
+ public void usersCustomerAttributesFetched() {
+ UserId userId = new UserId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ User user = new User();
+ user.setCustomerId(customerId);
+
+ msg = new TbMsg(UUIDs.timeBased(), "USER", userId, new TbMsgMetaData(), "{}");
+
+ when(ctx.getUserService()).thenReturn(userService);
+ when(userService.findUserByIdAsync(userId)).thenReturn(Futures.immediateFuture(user));
+
+ entityAttributeFetched(customerId);
+ }
+
+ @Test
+ public void assetsCustomerAttributesFetched() {
+ AssetId assetId = new AssetId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ Asset asset = new Asset();
+ asset.setCustomerId(customerId);
+
+ msg = new TbMsg(UUIDs.timeBased(), "USER", assetId, new TbMsgMetaData(), "{}");
+
+ when(ctx.getAssetService()).thenReturn(assetService);
+ when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFuture(asset));
+
+ entityAttributeFetched(customerId);
+ }
+
+ @Test
+ public void deviceCustomerAttributesFetched() {
+ DeviceId deviceId = new DeviceId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ Device device = new Device();
+ device.setCustomerId(customerId);
+
+ msg = new TbMsg(UUIDs.timeBased(), "USER", deviceId, new TbMsgMetaData(), "{}");
+
+ when(ctx.getDeviceService()).thenReturn(deviceService);
+ when(deviceService.findDeviceByIdAsync(deviceId)).thenReturn(Futures.immediateFuture(device));
+
+ entityAttributeFetched(customerId);
+ }
+
+ @Test
+ public void deviceCustomerTelemetryFetched() throws TbNodeException {
+ TbGetEntityAttrNodeConfiguration config = new TbGetEntityAttrNodeConfiguration();
+ Map<String, String> attrMapping = new HashMap<>();
+ attrMapping.putIfAbsent("temperature", "tempo");
+ config.setAttrMapping(attrMapping);
+ config.setTelemetry(true);
+ ObjectMapper mapper = new ObjectMapper();
+ TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+ node = new TbGetCustomerAttributeNode();
+ node.init(null, nodeConfiguration);
+
+
+ DeviceId deviceId = new DeviceId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ Device device = new Device();
+ device.setCustomerId(customerId);
+
+ msg = new TbMsg(UUIDs.timeBased(), "USER", deviceId, new TbMsgMetaData(), "{}");
+
+ when(ctx.getDeviceService()).thenReturn(deviceService);
+ when(deviceService.findDeviceByIdAsync(deviceId)).thenReturn(Futures.immediateFuture(device));
+
+ List<TsKvEntry> timeseries = Lists.newArrayList(new BasicTsKvEntry(1L, new StringDataEntry("temperature", "highest")));
+
+ when(ctx.getTimeseriesService()).thenReturn(timeseriesService);
+ when(timeseriesService.findLatest(customerId, Collections.singleton("temperature")))
+ .thenReturn(Futures.immediateFuture(timeseries));
+
+ node.onMsg(ctx, msg);
+ verify(ctx).tellNext(msg);
+ assertEquals(msg.getMetaData().getValue("tempo"), "highest");
+ }
+
+ private void entityAttributeFetched(CustomerId customerId) {
+ List<AttributeKvEntry> attributes = Lists.newArrayList(new BaseAttributeKvEntry(new StringDataEntry("temperature", "high"), 1L));
+
+ when(ctx.getAttributesService()).thenReturn(attributesService);
+ when(attributesService.find(customerId, SERVER_SCOPE, Collections.singleton("temperature")))
+ .thenReturn(Futures.immediateFuture(attributes));
+
+ node.onMsg(ctx, msg);
+ verify(ctx).tellNext(msg);
+ assertEquals(msg.getMetaData().getValue("tempo"), "high");
+ }
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java
new file mode 100644
index 0000000..1b66433
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java
@@ -0,0 +1,124 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.transform;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.Futures;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.asset.AssetService;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbChangeOriginatorNodeTest {
+
+ private TbChangeOriginatorNode node;
+
+ @Mock
+ private TbContext ctx;
+ @Mock
+ private AssetService assetService;
+
+
+ @Test
+ public void originatorCanBeChangedToCustomerId() throws TbNodeException {
+ init(false);
+ AssetId assetId = new AssetId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ Asset asset = new Asset();
+ asset.setCustomerId(customerId);
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "ASSET", assetId, new TbMsgMetaData(), "{}");
+
+ when(ctx.getAssetService()).thenReturn(assetService);
+ when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFuture(asset));
+
+ node.onMsg(ctx, msg);
+ ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+ verify(ctx).tellNext(captor.capture());
+ TbMsg actualMsg = captor.getValue();
+ assertEquals(customerId, actualMsg.getOriginator());
+ assertEquals(msg.getId(), actualMsg.getId());
+ }
+
+ @Test
+ public void newChainCanBeStarted() throws TbNodeException {
+ init(true);
+ AssetId assetId = new AssetId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ Asset asset = new Asset();
+ asset.setCustomerId(customerId);
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "ASSET", assetId, new TbMsgMetaData(), "{}");
+
+ when(ctx.getAssetService()).thenReturn(assetService);
+ when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFuture(asset));
+
+ node.onMsg(ctx, msg);
+ ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+ verify(ctx).spawn(captor.capture());
+ TbMsg actualMsg = captor.getValue();
+ assertEquals(customerId, actualMsg.getOriginator());
+ assertEquals(msg.getId(), actualMsg.getId());
+ }
+
+ @Test
+ public void exceptionThrownIfCannotFindNewOriginator() throws TbNodeException {
+ init(true);
+ AssetId assetId = new AssetId(UUIDs.timeBased());
+ CustomerId customerId = new CustomerId(UUIDs.timeBased());
+ Asset asset = new Asset();
+ asset.setCustomerId(customerId);
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "ASSET", assetId, new TbMsgMetaData(), "{}");
+
+ when(ctx.getAssetService()).thenReturn(assetService);
+ when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFailedFuture(new IllegalStateException("wrong")));
+
+ node.onMsg(ctx, msg);
+ ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+ verify(ctx).tellError(same(msg), captor.capture());
+ Throwable value = captor.getValue();
+ assertEquals("wrong", value.getMessage());
+ }
+
+ public void init(boolean startNewChain) throws TbNodeException {
+ TbChangeOriginatorNodeConfiguration config = new TbChangeOriginatorNodeConfiguration();
+ config.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE);
+ config.setStartNewChain(startNewChain);
+ ObjectMapper mapper = new ObjectMapper();
+ TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+ node = new TbChangeOriginatorNode();
+ node.init(null, nodeConfiguration);
+ }
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java
new file mode 100644
index 0000000..b904d7e
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java
@@ -0,0 +1,141 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.transform;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbTransformMsgNodeTest {
+
+ private TbTransformMsgNode node;
+
+ @Mock
+ private TbContext ctx;
+ @Mock
+ private ListeningExecutor executor;
+ @Mock
+ private ScriptEngine scriptEngine;
+
+ @Test
+ public void metadataCanBeUpdated() throws TbNodeException, ScriptException {
+ initWithScript(false);
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "7");
+ String rawJson = "{\"passed\": 5}";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+ TbMsg transformedMsg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{new}");
+ mockJsExecutor();
+ when(scriptEngine.executeUpdate(msg)).thenReturn(transformedMsg);
+
+ node.onMsg(ctx, msg);
+ verify(ctx).getJsExecutor();
+ ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+ verify(ctx).tellNext(captor.capture());
+ TbMsg actualMsg = captor.getValue();
+ assertEquals(transformedMsg, actualMsg);
+ }
+
+
+ @Test
+ public void newChainCanBeStarted() throws TbNodeException, ScriptException {
+ initWithScript(true);
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "7");
+ String rawJson = "{\"passed\": 5";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+ TbMsg transformedMsg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{new}");
+ mockJsExecutor();
+ when(scriptEngine.executeUpdate(msg)).thenReturn(transformedMsg);
+
+ node.onMsg(ctx, msg);
+ verify(ctx).getJsExecutor();
+ ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+ verify(ctx).spawn(captor.capture());
+ TbMsg actualMsg = captor.getValue();
+ assertEquals(transformedMsg, actualMsg);
+ }
+
+ @Test
+ public void exceptionHandledCorrectly() throws TbNodeException, ScriptException {
+ initWithScript(false);
+ TbMsgMetaData metaData = new TbMsgMetaData();
+ metaData.putValue("temp", "7");
+ String rawJson = "{\"passed\": 5";
+
+ TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+ mockJsExecutor();
+ when(scriptEngine.executeUpdate(msg)).thenThrow(new IllegalStateException("error"));
+
+ node.onMsg(ctx, msg);
+ verifyError(msg, "error", IllegalStateException.class);
+ }
+
+ private void initWithScript(boolean startChain) throws TbNodeException {
+ TbTransformMsgNodeConfiguration config = new TbTransformMsgNodeConfiguration();
+ config.setJsScript("scr");
+ config.setStartNewChain(startChain);
+ ObjectMapper mapper = new ObjectMapper();
+ TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+ when(ctx.createJsScriptEngine("scr", "Transform")).thenReturn(scriptEngine);
+
+ node = new TbTransformMsgNode();
+ node.init(ctx, nodeConfiguration);
+ }
+
+ private void mockJsExecutor() {
+ when(ctx.getJsExecutor()).thenReturn(executor);
+ doAnswer((Answer<ListenableFuture<TbMsg>>) invocationOnMock -> {
+ try {
+ Callable task = (Callable) (invocationOnMock.getArguments())[0];
+ return Futures.immediateFuture((TbMsg) task.call());
+ } catch (Throwable th) {
+ return Futures.immediateFailedFuture(th);
+ }
+ }).when(executor).executeAsync(Matchers.any(Callable.class));
+ }
+
+ private void verifyError(TbMsg msg, String message, Class expectedClass) {
+ ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+ verify(ctx).tellError(same(msg), captor.capture());
+
+ Throwable value = captor.getValue();
+ assertEquals(expectedClass, value.getClass());
+ assertEquals(message, value.getMessage());
+ }
+}
\ No newline at end of file
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
index 7ba5e36..af07737 100644
--- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
@@ -35,6 +35,7 @@ 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.quota.QuotaService;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
import org.thingsboard.server.transport.coap.session.CoapExchangeObserverProxy;
import org.thingsboard.server.transport.coap.session.CoapSessionCtx;
@@ -53,15 +54,17 @@ public class CoapTransportResource extends CoapResource {
private final SessionMsgProcessor processor;
private final DeviceAuthService authService;
private final QuotaService quotaService;
+ private final DeviceOfflineService offlineService;
private final Field observerField;
private final long timeout;
public CoapTransportResource(SessionMsgProcessor processor, DeviceAuthService authService, CoapTransportAdaptor adaptor, String name,
- long timeout, QuotaService quotaService) {
+ long timeout, QuotaService quotaService, DeviceOfflineService offlineService) {
super(name);
this.processor = processor;
this.authService = authService;
this.quotaService = quotaService;
+ this.offlineService = offlineService;
this.adaptor = adaptor;
this.timeout = timeout;
// This is important to turn off existing observable logic in
@@ -168,6 +171,7 @@ public class CoapTransportResource extends CoapResource {
case TO_SERVER_RPC_REQUEST:
ctx.setSessionType(SessionType.SYNC);
msg = adaptor.convertToActorMsg(ctx, type, request);
+ offlineService.online(ctx.getDevice(), true);
break;
case SUBSCRIBE_ATTRIBUTES_REQUEST:
case SUBSCRIBE_RPC_COMMANDS_REQUEST:
@@ -175,11 +179,13 @@ public class CoapTransportResource extends CoapResource {
advanced.setObserver(new CoapExchangeObserverProxy(systemObserver, ctx));
ctx.setSessionType(SessionType.ASYNC);
msg = adaptor.convertToActorMsg(ctx, type, request);
+ offlineService.online(ctx.getDevice(), false);
break;
case UNSUBSCRIBE_ATTRIBUTES_REQUEST:
case UNSUBSCRIBE_RPC_COMMANDS_REQUEST:
ctx.setSessionType(SessionType.ASYNC);
msg = adaptor.convertToActorMsg(ctx, type, request);
+ offlineService.online(ctx.getDevice(), false);
break;
default:
log.trace("[{}] Unsupported msg type: {}", ctx.getSessionId(), type);
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
index 15706d4..4037ee7 100644
--- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
@@ -27,6 +27,7 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
import javax.annotation.PostConstruct;
@@ -57,6 +58,9 @@ public class CoapTransportService {
@Autowired(required = false)
private QuotaService quotaService;
+ @Autowired(required = false)
+ private DeviceOfflineService offlineService;
+
@Value("${coap.bind_address}")
private String host;
@@ -86,7 +90,7 @@ public class CoapTransportService {
private void createResources() {
CoapResource api = new CoapResource(API);
- api.add(new CoapTransportResource(processor, authService, adaptor, V1, timeout, quotaService));
+ api.add(new CoapTransportResource(processor, authService, adaptor, V1, timeout, quotaService, offlineService));
server.add(api);
}
diff --git a/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
index 072c735..fd0346a 100644
--- a/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
+++ b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.transport.coap;
+import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapResponse;
@@ -31,6 +32,7 @@ import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.device.DeviceStatusQuery;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -51,6 +53,7 @@ import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.auth.DeviceAuthResult;
import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
import java.util.ArrayList;
import java.util.List;
@@ -137,6 +140,31 @@ public class CoapServerTest {
public static QuotaService quotaService() {
return key -> false;
}
+
+ @Bean
+ public static DeviceOfflineService offlineService() {
+ return new DeviceOfflineService() {
+ @Override
+ public void online(Device device, boolean isUpdate) {
+
+ }
+
+ @Override
+ public void offline(Device device) {
+
+ }
+
+ @Override
+ public ListenableFuture<List<Device>> findOfflineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold) {
+ return null;
+ }
+
+ @Override
+ public ListenableFuture<List<Device>> findOnlineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold) {
+ return null;
+ }
+ };
+ }
}
@Autowired
diff --git a/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
index 4d90b5f..03a4201 100644
--- a/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
+++ b/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
@@ -26,6 +26,7 @@ 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.Device;
import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
import org.thingsboard.server.common.msg.core.*;
import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
@@ -36,6 +37,7 @@ import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
import org.thingsboard.server.transport.http.session.HttpSessionCtx;
import javax.servlet.http.HttpServletRequest;
@@ -63,6 +65,9 @@ public class DeviceApiController {
@Autowired(required = false)
private QuotaService quotaService;
+ @Autowired(required = false)
+ private DeviceOfflineService offlineService;
+
@RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.GET, produces = "application/json")
public DeferredResult<ResponseEntity> getDeviceAttributes(@PathVariable("deviceToken") String deviceToken,
@RequestParam(value = "clientKeys", required = false, defaultValue = "") String clientKeys,
@@ -82,7 +87,7 @@ public class DeviceApiController {
Set<String> sharedKeySet = !StringUtils.isEmpty(sharedKeys) ? new HashSet<>(Arrays.asList(sharedKeys.split(","))) : null;
request = new BasicGetAttributesRequest(0, clientKeySet, sharedKeySet);
}
- process(ctx, request);
+ process(ctx, request, false);
} else {
responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
}
@@ -100,7 +105,7 @@ public class DeviceApiController {
HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
try {
- process(ctx, JsonConverter.convertToAttributes(new JsonParser().parse(json)));
+ process(ctx, JsonConverter.convertToAttributes(new JsonParser().parse(json)), true);
} catch (IllegalStateException | JsonSyntaxException ex) {
responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
}
@@ -120,7 +125,7 @@ public class DeviceApiController {
HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
try {
- process(ctx, JsonConverter.convertToTelemetry(new JsonParser().parse(json)));
+ process(ctx, JsonConverter.convertToTelemetry(new JsonParser().parse(json)), true);
} catch (IllegalStateException | JsonSyntaxException ex) {
responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
}
@@ -150,7 +155,7 @@ public class DeviceApiController {
if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
try {
JsonObject response = new JsonParser().parse(json).getAsJsonObject();
- process(ctx, new ToDeviceRpcResponseMsg(requestId, response.toString()));
+ process(ctx, new ToDeviceRpcResponseMsg(requestId, response.toString()), true);
} catch (IllegalStateException | JsonSyntaxException ex) {
responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
}
@@ -173,7 +178,7 @@ public class DeviceApiController {
JsonObject request = new JsonParser().parse(json).getAsJsonObject();
process(ctx, new ToServerRpcRequestMsg(0,
request.get("method").getAsString(),
- request.get("params").toString()));
+ request.get("params").toString()), true);
} catch (IllegalStateException | JsonSyntaxException ex) {
responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
}
@@ -199,7 +204,7 @@ public class DeviceApiController {
HttpSessionCtx ctx = getHttpSessionCtx(responseWriter, timeout);
if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
try {
- process(ctx, msg);
+ process(ctx, msg, false);
} catch (IllegalStateException | JsonSyntaxException ex) {
responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
}
@@ -217,9 +222,10 @@ public class DeviceApiController {
return new HttpSessionCtx(processor, authService, responseWriter, timeout != 0 ? timeout : defaultTimeout);
}
- private void process(HttpSessionCtx ctx, FromDeviceMsg request) {
+ private void process(HttpSessionCtx ctx, FromDeviceMsg request, boolean isUpdate) {
AdaptorToSessionActorMsg msg = new BasicAdaptorToSessionActorMsg(ctx, request);
processor.process(new BasicToDeviceActorSessionMsg(ctx.getDevice(), msg));
+ offlineService.online(ctx.getDevice(), isUpdate);
}
private boolean quotaExceeded(HttpServletRequest request, DeferredResult<ResponseEntity> responseWriter) {
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
index 8766599..8d475a4 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
@@ -37,6 +37,7 @@ import org.thingsboard.server.common.transport.adaptor.AdaptorException;
import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.common.transport.quota.QuotaService;
import org.thingsboard.server.dao.EncryptionUtil;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
@@ -72,13 +73,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
private final DeviceAuthService authService;
private final RelationService relationService;
private final QuotaService quotaService;
+ private final DeviceOfflineService offlineService;
private final SslHandler sslHandler;
private volatile boolean connected;
private volatile InetSocketAddress address;
private volatile GatewaySessionCtx gatewaySessionCtx;
public MqttTransportHandler(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService, RelationService relationService,
- MqttTransportAdaptor adaptor, SslHandler sslHandler, QuotaService quotaService) {
+ MqttTransportAdaptor adaptor, SslHandler sslHandler, QuotaService quotaService, DeviceOfflineService offlineService) {
this.processor = processor;
this.deviceService = deviceService;
this.relationService = relationService;
@@ -88,6 +90,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
this.sessionId = deviceSessionCtx.getSessionId().toUidStr();
this.sslHandler = sslHandler;
this.quotaService = quotaService;
+ this.offlineService = offlineService;
}
@Override
@@ -129,11 +132,13 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
case PINGREQ:
if (checkConnected(ctx)) {
ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0)));
+ offlineService.online(deviceSessionCtx.getDevice(), false);
}
break;
case DISCONNECT:
if (checkConnected(ctx)) {
processDisconnect(ctx);
+ offlineService.offline(deviceSessionCtx.getDevice());
}
break;
default:
@@ -185,23 +190,28 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
try {
if (topicName.equals(DEVICE_TELEMETRY_TOPIC)) {
msg = adaptor.convertToActorMsg(deviceSessionCtx, POST_TELEMETRY_REQUEST, mqttMsg);
+ offlineService.online(deviceSessionCtx.getDevice(), true);
} else if (topicName.equals(DEVICE_ATTRIBUTES_TOPIC)) {
msg = adaptor.convertToActorMsg(deviceSessionCtx, POST_ATTRIBUTES_REQUEST, mqttMsg);
+ offlineService.online(deviceSessionCtx.getDevice(), true);
} else if (topicName.startsWith(DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX)) {
msg = adaptor.convertToActorMsg(deviceSessionCtx, GET_ATTRIBUTES_REQUEST, mqttMsg);
if (msgId >= 0) {
ctx.writeAndFlush(createMqttPubAckMsg(msgId));
}
+ offlineService.online(deviceSessionCtx.getDevice(), false);
} else if (topicName.startsWith(DEVICE_RPC_RESPONSE_TOPIC)) {
msg = adaptor.convertToActorMsg(deviceSessionCtx, TO_DEVICE_RPC_RESPONSE, mqttMsg);
if (msgId >= 0) {
ctx.writeAndFlush(createMqttPubAckMsg(msgId));
}
+ offlineService.online(deviceSessionCtx.getDevice(), true);
} else if (topicName.startsWith(DEVICE_RPC_REQUESTS_TOPIC)) {
msg = adaptor.convertToActorMsg(deviceSessionCtx, TO_SERVER_RPC_REQUEST, mqttMsg);
if (msgId >= 0) {
ctx.writeAndFlush(createMqttPubAckMsg(msgId));
}
+ offlineService.online(deviceSessionCtx.getDevice(), true);
}
} catch (AdaptorException e) {
log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e);
@@ -250,6 +260,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
}
}
ctx.writeAndFlush(createSubAckMessage(mqttMsg.variableHeader().messageId(), grantedQoSList));
+ offlineService.online(deviceSessionCtx.getDevice(), false);
}
private void processUnsubscribe(ChannelHandlerContext ctx, MqttUnsubscribeMessage mqttMsg) {
@@ -273,6 +284,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
}
}
ctx.writeAndFlush(createUnSubAckMessage(mqttMsg.variableHeader().messageId()));
+ offlineService.online(deviceSessionCtx.getDevice(), false);
}
private MqttMessage createUnSubAckMessage(int msgId) {
@@ -304,6 +316,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED));
connected = true;
checkGatewaySession();
+ offlineService.online(deviceSessionCtx.getDevice(), false);
}
}
@@ -315,6 +328,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED));
connected = true;
checkGatewaySession();
+ offlineService.online(deviceSessionCtx.getDevice(), false);
} else {
ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_NOT_AUTHORIZED));
ctx.close();
@@ -365,6 +379,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("[{}] Unexpected Exception", sessionId, cause);
ctx.close();
+ if(deviceSessionCtx.getDevice() != null) {
+ offlineService.offline(deviceSessionCtx.getDevice());
+ }
}
private static MqttSubAckMessage createSubAckMessage(Integer msgId, List<Integer> grantedQoSList) {
@@ -403,7 +420,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
if (infoNode != null) {
JsonNode gatewayNode = infoNode.get("gateway");
if (gatewayNode != null && gatewayNode.asBoolean()) {
- gatewaySessionCtx = new GatewaySessionCtx(processor, deviceService, authService, relationService, deviceSessionCtx);
+ gatewaySessionCtx = new GatewaySessionCtx(processor, deviceService, authService,
+ relationService, deviceSessionCtx, offlineService);
}
}
}
@@ -411,5 +429,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
processor.process(SessionCloseMsg.onError(deviceSessionCtx.getSessionId()));
+ if(deviceSessionCtx.getDevice() != null) {
+ offlineService.offline(deviceSessionCtx.getDevice());
+ }
}
}
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
index 976d8ba..94cf940 100644
--- 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
@@ -24,6 +24,7 @@ import io.netty.handler.ssl.SslHandler;
import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
@@ -42,10 +43,11 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
private final MqttTransportAdaptor adaptor;
private final MqttSslHandlerProvider sslHandlerProvider;
private final QuotaService quotaService;
+ private final DeviceOfflineService offlineService;
public MqttTransportServerInitializer(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService, RelationService relationService,
MqttTransportAdaptor adaptor, MqttSslHandlerProvider sslHandlerProvider,
- QuotaService quotaService) {
+ QuotaService quotaService, DeviceOfflineService offlineService) {
this.processor = processor;
this.deviceService = deviceService;
this.authService = authService;
@@ -53,6 +55,7 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
this.adaptor = adaptor;
this.sslHandlerProvider = sslHandlerProvider;
this.quotaService = quotaService;
+ this.offlineService = offlineService;
}
@Override
@@ -67,7 +70,7 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
pipeline.addLast("encoder", MqttEncoder.INSTANCE);
MqttTransportHandler handler = new MqttTransportHandler(processor, deviceService, authService, relationService,
- adaptor, sslHandler, quotaService);
+ adaptor, sslHandler, quotaService, offlineService);
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
index 1ae7d38..90b4591 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
@@ -30,6 +30,7 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
@@ -69,6 +70,9 @@ public class MqttTransportService {
@Autowired(required = false)
private QuotaService quotaService;
+ @Autowired(required = false)
+ private DeviceOfflineService offlineService;
+
@Value("${mqtt.bind_address}")
private String host;
@Value("${mqtt.bind_port}")
@@ -106,7 +110,7 @@ public class MqttTransportService {
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MqttTransportServerInitializer(processor, deviceService, authService, relationService,
- adaptor, sslHandlerProvider, quotaService));
+ adaptor, sslHandlerProvider, quotaService, offlineService));
serverChannel = b.bind(host, port).sync().channel();
log.info("Mqtt transport started!");
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
index 7eda5bd..b4dd8db 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
@@ -36,6 +36,7 @@ import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
@@ -61,14 +62,17 @@ public class GatewaySessionCtx {
private final DeviceService deviceService;
private final DeviceAuthService authService;
private final RelationService relationService;
+ private final DeviceOfflineService offlineService;
private final Map<String, GatewayDeviceSessionCtx> devices;
private ChannelHandlerContext channel;
- public GatewaySessionCtx(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService, RelationService relationService, DeviceSessionCtx gatewaySessionCtx) {
+ public GatewaySessionCtx(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService,
+ RelationService relationService, DeviceSessionCtx gatewaySessionCtx, DeviceOfflineService offlineService) {
this.processor = processor;
this.deviceService = deviceService;
this.authService = authService;
this.relationService = relationService;
+ this.offlineService = offlineService;
this.gateway = gatewaySessionCtx.getDevice();
this.gatewaySessionId = gatewaySessionCtx.getSessionId();
this.devices = new HashMap<>();
@@ -98,6 +102,7 @@ public class GatewaySessionCtx {
log.debug("[{}] Added device [{}] to the gateway session", gatewaySessionId, deviceName);
processor.process(new BasicToDeviceActorSessionMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new AttributesSubscribeMsg())));
processor.process(new BasicToDeviceActorSessionMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new RpcSubscribeMsg())));
+ offlineService.online(device, false);
}
}
@@ -107,6 +112,7 @@ public class GatewaySessionCtx {
if (deviceSessionCtx != null) {
processor.process(SessionCloseMsg.onDisconnect(deviceSessionCtx.getSessionId()));
deviceSessionCtx.setClosed(true);
+ offlineService.offline(deviceSessionCtx.getDevice());
log.debug("[{}] Removed device [{}] from the gateway session", gatewaySessionId, deviceName);
} else {
log.debug("[{}] Device [{}] was already removed from the gateway session", gatewaySessionId, deviceName);
@@ -117,6 +123,7 @@ public class GatewaySessionCtx {
public void onGatewayDisconnect() {
devices.forEach((k, v) -> {
processor.process(SessionCloseMsg.onDisconnect(v.getSessionId()));
+ offlineService.offline(v.getDevice());
});
}
@@ -138,6 +145,7 @@ public class GatewaySessionCtx {
GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
+ offlineService.online(deviceSessionCtx.getDevice(), true);
}
} else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
@@ -154,6 +162,7 @@ public class GatewaySessionCtx {
GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
new BasicAdaptorToSessionActorMsg(deviceSessionCtx, new ToDeviceRpcResponseMsg(requestId, data))));
+ offlineService.online(deviceSessionCtx.getDevice(), true);
} else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
}
@@ -176,6 +185,7 @@ public class GatewaySessionCtx {
GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
+ offlineService.online(deviceSessionCtx.getDevice(), true);
}
} else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
@@ -210,6 +220,7 @@ public class GatewaySessionCtx {
processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
ack(msg);
+ offlineService.online(deviceSessionCtx.getDevice(), false);
} else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
}
ui/package.json 3(+2 -1)
diff --git a/ui/package.json b/ui/package.json
index ad95ef4..ec3f880 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -15,7 +15,7 @@
},
"dependencies": {
"@flowjs/ng-flow": "^2.7.1",
- "ace-builds": "^1.2.5",
+ "ace-builds": "1.3.1",
"angular": "1.5.8",
"angular-animate": "1.5.8",
"angular-aria": "1.5.8",
@@ -69,6 +69,7 @@
"moment": "^2.15.0",
"ngclipboard": "^1.1.1",
"ngreact": "^0.3.0",
+ "ngFlowchart": "git://github.com/thingsboard/ngFlowchart.git#master",
"objectpath": "^1.2.1",
"oclazyload": "^1.0.9",
"raphael": "^2.2.7",
ui/server.js 20(+20 -0)
diff --git a/ui/server.js b/ui/server.js
index fae132f..65a2bc7 100644
--- a/ui/server.js
+++ b/ui/server.js
@@ -30,6 +30,9 @@ const httpProxy = require('http-proxy');
const forwardHost = 'localhost';
const forwardPort = 8080;
+const ruleNodeUiforwardHost = 'localhost';
+const ruleNodeUiforwardPort = 8080;
+
const app = express();
const server = http.createServer(app);
@@ -52,17 +55,34 @@ const apiProxy = httpProxy.createProxyServer({
}
});
+const ruleNodeUiApiProxy = httpProxy.createProxyServer({
+ target: {
+ host: ruleNodeUiforwardHost,
+ port: ruleNodeUiforwardPort
+ }
+});
+
apiProxy.on('error', function (err, req, res) {
console.warn('API proxy error: ' + err);
res.end('Error.');
});
+ruleNodeUiApiProxy.on('error', function (err, req, res) {
+ console.warn('RuleNode UI API proxy error: ' + err);
+ res.end('Error.');
+});
+
console.info(`Forwarding API requests to http://${forwardHost}:${forwardPort}`);
+console.info(`Forwarding Rule Node UI requests to http://${ruleNodeUiforwardHost}:${ruleNodeUiforwardPort}`);
app.all('/api/*', (req, res) => {
apiProxy.web(req, res);
});
+app.all('/static/rulenode/*', (req, res) => {
+ ruleNodeUiApiProxy.web(req, res);
+});
+
app.get('*', function(req, res) {
res.sendFile(path.join(__dirname, 'src/index.html'));
});
diff --git a/ui/src/app/api/component-descriptor.service.js b/ui/src/app/api/component-descriptor.service.js
index 4478d71..cc3f710 100644
--- a/ui/src/app/api/component-descriptor.service.js
+++ b/ui/src/app/api/component-descriptor.service.js
@@ -26,7 +26,8 @@ function ComponentDescriptorService($http, $q) {
var service = {
getComponentDescriptorsByType: getComponentDescriptorsByType,
getComponentDescriptorByClazz: getComponentDescriptorByClazz,
- getPluginActionsByPluginClazz: getPluginActionsByPluginClazz
+ getPluginActionsByPluginClazz: getPluginActionsByPluginClazz,
+ getComponentDescriptorsByTypes: getComponentDescriptorsByTypes
}
return service;
@@ -52,6 +53,41 @@ function ComponentDescriptorService($http, $q) {
return deferred.promise;
}
+ function getComponentDescriptorsByTypes(componentTypes) {
+ var deferred = $q.defer();
+ var result = [];
+ for (var i=componentTypes.length-1;i>=0;i--) {
+ var componentType = componentTypes[i];
+ if (componentsByType[componentType]) {
+ result = result.concat(componentsByType[componentType]);
+ componentTypes.splice(i, 1);
+ }
+ }
+ if (!componentTypes.length) {
+ deferred.resolve(result);
+ } else {
+ var url = '/api/components?componentTypes=' + componentTypes.join(',');
+ $http.get(url, null).then(function success(response) {
+ var components = response.data;
+ for (var i = 0; i < components.length; i++) {
+ var component = components[i];
+ var componentsList = componentsByType[component.type];
+ if (!componentsList) {
+ componentsList = [];
+ componentsByType[component.type] = componentsList;
+ }
+ componentsList.push(component);
+ componentsByClazz[component.clazz] = component;
+ }
+ result = result.concat(components);
+ deferred.resolve(components);
+ }, function fail() {
+ deferred.reject();
+ });
+ }
+ return deferred.promise;
+ }
+
function getComponentDescriptorByClazz(componentDescriptorClazz) {
var deferred = $q.defer();
if (componentsByClazz[componentDescriptorClazz]) {
ui/src/app/api/entity.service.js 8(+7 -1)
diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js
index e4c51a2..ba1265f 100644
--- a/ui/src/app/api/entity.service.js
+++ b/ui/src/app/api/entity.service.js
@@ -22,7 +22,7 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes])
/*@ngInject*/
function EntityService($http, $q, $filter, $translate, $log, userService, deviceService,
assetService, tenantService, customerService,
- ruleService, pluginService, dashboardService, entityRelationService, attributeService, types, utils) {
+ ruleService, pluginService, ruleChainService, dashboardService, entityRelationService, attributeService, types, utils) {
var service = {
getEntity: getEntity,
getEntities: getEntities,
@@ -73,6 +73,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
case types.entityType.user:
promise = userService.getUser(entityId, true, config);
break;
+ case types.entityType.rulechain:
+ promise = ruleChainService.getRuleChain(entityId, config);
+ break;
case types.entityType.alarm:
$log.error('Get Alarm Entity is not implemented!');
break;
@@ -271,6 +274,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
case types.entityType.plugin:
promise = pluginService.getAllPlugins(pageLink, config);
break;
+ case types.entityType.rulechain:
+ promise = ruleChainService.getRuleChains(pageLink, config);
+ break;
case types.entityType.dashboard:
if (user.authority === 'CUSTOMER_USER') {
promise = dashboardService.getCustomerDashboards(customerId, pageLink, config);
ui/src/app/api/rule-chain.service.js 307(+307 -0)
diff --git a/ui/src/app/api/rule-chain.service.js b/ui/src/app/api/rule-chain.service.js
new file mode 100644
index 0000000..03b3a82
--- /dev/null
+++ b/ui/src/app/api/rule-chain.service.js
@@ -0,0 +1,307 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.api.ruleChain', [])
+ .factory('ruleChainService', RuleChainService).name;
+
+/*@ngInject*/
+function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, componentDescriptorService) {
+
+ var ruleNodeComponents = null;
+
+ var service = {
+ getSystemRuleChains: getSystemRuleChains,
+ getTenantRuleChains: getTenantRuleChains,
+ getRuleChains: getRuleChains,
+ getRuleChain: getRuleChain,
+ saveRuleChain: saveRuleChain,
+ deleteRuleChain: deleteRuleChain,
+ getRuleChainMetaData: getRuleChainMetaData,
+ saveRuleChainMetaData: saveRuleChainMetaData,
+ getRuleNodeComponents: getRuleNodeComponents,
+ getRuleNodeComponentByClazz: getRuleNodeComponentByClazz,
+ getRuleNodeSupportedLinks: getRuleNodeSupportedLinks,
+ resolveTargetRuleChains: resolveTargetRuleChains,
+ testScript: testScript
+ };
+
+ return service;
+
+ function getSystemRuleChains (pageLink, config) {
+ var deferred = $q.defer();
+ var url = '/api/system/ruleChains?limit=' + pageLink.limit;
+ if (angular.isDefined(pageLink.textSearch)) {
+ url += '&textSearch=' + pageLink.textSearch;
+ }
+ if (angular.isDefined(pageLink.idOffset)) {
+ url += '&idOffset=' + pageLink.idOffset;
+ }
+ if (angular.isDefined(pageLink.textOffset)) {
+ url += '&textOffset=' + pageLink.textOffset;
+ }
+ $http.get(url, config).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getTenantRuleChains (pageLink, config) {
+ var deferred = $q.defer();
+ var url = '/api/tenant/ruleChains?limit=' + pageLink.limit;
+ if (angular.isDefined(pageLink.textSearch)) {
+ url += '&textSearch=' + pageLink.textSearch;
+ }
+ if (angular.isDefined(pageLink.idOffset)) {
+ url += '&idOffset=' + pageLink.idOffset;
+ }
+ if (angular.isDefined(pageLink.textOffset)) {
+ url += '&textOffset=' + pageLink.textOffset;
+ }
+ $http.get(url, config).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getRuleChains (pageLink, config) {
+ var deferred = $q.defer();
+ var url = '/api/ruleChains?limit=' + pageLink.limit;
+ if (angular.isDefined(pageLink.textSearch)) {
+ url += '&textSearch=' + pageLink.textSearch;
+ }
+ if (angular.isDefined(pageLink.idOffset)) {
+ url += '&idOffset=' + pageLink.idOffset;
+ }
+ if (angular.isDefined(pageLink.textOffset)) {
+ url += '&textOffset=' + pageLink.textOffset;
+ }
+ $http.get(url, config).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getRuleChain(ruleChainId, config) {
+ var deferred = $q.defer();
+ var url = '/api/ruleChain/' + ruleChainId;
+ $http.get(url, config).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function saveRuleChain(ruleChain) {
+ var deferred = $q.defer();
+ var url = '/api/ruleChain';
+ $http.post(url, ruleChain).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function deleteRuleChain(ruleChainId) {
+ var deferred = $q.defer();
+ var url = '/api/ruleChain/' + ruleChainId;
+ $http.delete(url).then(function success() {
+ deferred.resolve();
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getRuleChainMetaData(ruleChainId, config) {
+ var deferred = $q.defer();
+ var url = '/api/ruleChain/' + ruleChainId + '/metadata';
+ $http.get(url, config).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function saveRuleChainMetaData(ruleChainMetaData) {
+ var deferred = $q.defer();
+ var url = '/api/ruleChain/metadata';
+ $http.post(url, ruleChainMetaData).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getRuleNodeSupportedLinks(component) {
+ var relationTypes = component.configurationDescriptor.nodeDefinition.relationTypes;
+ var customRelations = component.configurationDescriptor.nodeDefinition.customRelations;
+ var linkLabels = [];
+ for (var i=0;i<relationTypes.length;i++) {
+ linkLabels.push({
+ name: relationTypes[i], custom: false
+ });
+ }
+ if (customRelations) {
+ linkLabels.push(
+ { name: 'Custom', custom: true }
+ );
+ }
+ return linkLabels;
+ }
+
+ function getRuleNodeComponents() {
+ var deferred = $q.defer();
+ if (ruleNodeComponents) {
+ deferred.resolve(ruleNodeComponents);
+ } else {
+ loadRuleNodeComponents().then(
+ (components) => {
+ resolveRuleNodeComponentsUiResources(components).then(
+ (components) => {
+ ruleNodeComponents = components;
+ ruleNodeComponents.push(
+ types.ruleChainNodeComponent
+ );
+ deferred.resolve(ruleNodeComponents);
+ },
+ () => {
+ deferred.reject();
+ }
+ );
+ },
+ () => {
+ deferred.reject();
+ }
+ );
+ }
+ return deferred.promise;
+ }
+
+ function resolveRuleNodeComponentsUiResources(components) {
+ var deferred = $q.defer();
+ var tasks = [];
+ for (var i=0;i<components.length;i++) {
+ var component = components[i];
+ tasks.push(resolveRuleNodeComponentUiResources(component));
+ }
+ $q.all(tasks).then(
+ (components) => {
+ deferred.resolve(components);
+ },
+ () => {
+ deferred.resolve(components);
+ }
+ );
+ return deferred.promise;
+ }
+
+ function resolveRuleNodeComponentUiResources(component) {
+ var deferred = $q.defer();
+ var uiResources = component.configurationDescriptor.nodeDefinition.uiResources;
+ if (uiResources && uiResources.length) {
+ var tasks = [];
+ for (var i=0;i<uiResources.length;i++) {
+ var uiResource = uiResources[i];
+ tasks.push($ocLazyLoad.load(uiResource));
+ }
+ $q.all(tasks).then(
+ () => {
+ deferred.resolve(component);
+ },
+ () => {
+ component.configurationDescriptor.nodeDefinition.uiResourceLoadError = $translate.instant('rulenode.ui-resources-load-error');
+ deferred.resolve(component);
+ }
+ )
+ } else {
+ deferred.resolve(component);
+ }
+ return deferred.promise;
+ }
+
+ function getRuleNodeComponentByClazz(clazz) {
+ var res = $filter('filter')(ruleNodeComponents, {clazz: clazz}, true);
+ if (res && res.length) {
+ return res[0];
+ }
+ return null;
+ }
+
+ function resolveTargetRuleChains(ruleChainConnections) {
+ var deferred = $q.defer();
+ if (ruleChainConnections && ruleChainConnections.length) {
+ var tasks = [];
+ for (var i = 0; i < ruleChainConnections.length; i++) {
+ tasks.push(resolveRuleChain(ruleChainConnections[i].targetRuleChainId.id));
+ }
+ $q.all(tasks).then(
+ (ruleChains) => {
+ var ruleChainsMap = {};
+ for (var i = 0; i < ruleChains.length; i++) {
+ ruleChainsMap[ruleChains[i].id.id] = ruleChains[i];
+ }
+ deferred.resolve(ruleChainsMap);
+ },
+ () => {
+ deferred.reject();
+ }
+ );
+ } else {
+ deferred.resolve({});
+ }
+ return deferred.promise;
+ }
+
+ function resolveRuleChain(ruleChainId) {
+ var deferred = $q.defer();
+ getRuleChain(ruleChainId, {ignoreErrors: true}).then(
+ (ruleChain) => {
+ deferred.resolve(ruleChain);
+ },
+ () => {
+ deferred.resolve({
+ id: {id: ruleChainId, entityType: types.entityType.rulechain}
+ });
+ }
+ );
+ return deferred.promise;
+ }
+
+ function loadRuleNodeComponents() {
+ return componentDescriptorService.getComponentDescriptorsByTypes(types.ruleNodeTypeComponentTypes);
+ }
+
+ function testScript(inputParams) {
+ var deferred = $q.defer();
+ var url = '/api/ruleChain/testScript';
+ $http.post(url, inputParams).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+}
ui/src/app/app.js 5(+5 -0)
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index 0b6a141..3131013 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -49,6 +49,7 @@ import 'material-ui';
import 'react-schema-form';
import react from 'ngreact';
import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
+import 'ngFlowchart/dist/ngFlowchart';
import thingsboardLocales from './locale/locale.constant';
import thingsboardLogin from './login';
@@ -73,6 +74,7 @@ import thingsboardApiAttribute from './api/attribute.service';
import thingsboardApiEntity from './api/entity.service';
import thingsboardApiAlarm from './api/alarm.service';
import thingsboardApiAuditLog from './api/audit-log.service';
+import thingsboardApiRuleChain from './api/rule-chain.service';
import 'typeface-roboto';
import 'font-awesome/css/font-awesome.min.css';
@@ -85,6 +87,7 @@ import 'mdPickers/dist/mdPickers.min.css';
import 'angular-hotkeys/build/hotkeys.min.css';
import 'angular-carousel/dist/angular-carousel.min.css';
import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css';
+import 'ngFlowchart/dist/flowchart.css';
import '../scss/main.scss';
import AppConfig from './app.config';
@@ -112,6 +115,7 @@ angular.module('thingsboard', [
'ngclipboard',
react.name,
'flow',
+ 'flowchart',
thingsboardLocales,
thingsboardLogin,
thingsboardDialogs,
@@ -135,6 +139,7 @@ angular.module('thingsboard', [
thingsboardApiEntity,
thingsboardApiAlarm,
thingsboardApiAuditLog,
+ thingsboardApiRuleChain,
uiRouter])
.config(AppConfig)
.factory('globalInterceptor', GlobalInterceptor)
ui/src/app/common/types.constant.js 102(+101 -1)
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index ef6ffde..2186508 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -279,6 +279,23 @@ export default angular.module('thingsboard.types', [])
function: "function",
alarm: "alarm"
},
+ contentType: {
+ "JSON": {
+ value: "JSON",
+ name: "content-type.json",
+ code: "json"
+ },
+ "TEXT": {
+ value: "TEXT",
+ name: "content-type.text",
+ code: "text"
+ },
+ "BINARY": {
+ value: "BINARY",
+ name: "content-type.binary",
+ code: "text"
+ }
+ },
componentType: {
filter: "FILTER",
processor: "PROCESSOR",
@@ -294,7 +311,9 @@ export default angular.module('thingsboard.types', [])
customer: "CUSTOMER",
user: "USER",
dashboard: "DASHBOARD",
- alarm: "ALARM"
+ alarm: "ALARM",
+ rulechain: "RULE_CHAIN",
+ rulenode: "RULE_NODE"
},
aliasEntityType: {
current_customer: "CURRENT_CUSTOMER"
@@ -354,6 +373,12 @@ export default angular.module('thingsboard.types', [])
list: 'entity.list-of-alarms',
nameStartsWith: 'entity.alarm-name-starts-with'
},
+ "RULE_CHAIN": {
+ type: 'entity.type-rulechain',
+ typePlural: 'entity.type-rulechains',
+ list: 'entity.list-of-rulechains',
+ nameStartsWith: 'entity.rulechain-name-starts-with'
+ },
"CURRENT_CUSTOMER": {
type: 'entity.type-current-customer',
list: 'entity.type-current-customer'
@@ -381,6 +406,16 @@ export default angular.module('thingsboard.types', [])
name: "event.type-stats"
}
},
+ debugEventType: {
+ debugRuleNode: {
+ value: "DEBUG_RULE_NODE",
+ name: "event.type-debug-rule-node"
+ },
+ debugRuleChain: {
+ value: "DEBUG_RULE_CHAIN",
+ name: "event.type-debug-rule-chain"
+ }
+ },
extensionType: {
http: "HTTP",
mqtt: "MQTT",
@@ -450,6 +485,71 @@ export default angular.module('thingsboard.types', [])
clientSide: false
}
},
+ ruleNodeTypeComponentTypes: ["FILTER", "ENRICHMENT", "TRANSFORMATION", "ACTION"],
+ ruleChainNodeComponent: {
+ type: 'RULE_CHAIN',
+ name: 'rule chain',
+ clazz: 'tb.internal.RuleChain',
+ configurationDescriptor: {
+ nodeDefinition: {
+ description: "",
+ details: "Forwards incoming messages to specified Rule Chain",
+ inEnabled: true,
+ outEnabled: false,
+ relationTypes: [],
+ customRelations: false,
+ defaultConfiguration: {}
+ }
+ }
+ },
+ inputNodeComponent: {
+ type: 'INPUT',
+ name: 'Input',
+ clazz: 'tb.internal.Input'
+ },
+ ruleNodeType: {
+ FILTER: {
+ value: "FILTER",
+ name: "rulenode.type-filter",
+ details: "rulenode.type-filter-details",
+ nodeClass: "tb-filter-type",
+ icon: "filter_list"
+ },
+ ENRICHMENT: {
+ value: "ENRICHMENT",
+ name: "rulenode.type-enrichment",
+ details: "rulenode.type-enrichment-details",
+ nodeClass: "tb-enrichment-type",
+ icon: "playlist_add"
+ },
+ TRANSFORMATION: {
+ value: "TRANSFORMATION",
+ name: "rulenode.type-transformation",
+ details: "rulenode.type-transformation-details",
+ nodeClass: "tb-transformation-type",
+ icon: "transform"
+ },
+ ACTION: {
+ value: "ACTION",
+ name: "rulenode.type-action",
+ details: "rulenode.type-action-details",
+ nodeClass: "tb-action-type",
+ icon: "flash_on"
+ },
+ RULE_CHAIN: {
+ value: "RULE_CHAIN",
+ name: "rulenode.type-rule-chain",
+ details: "rulenode.type-rule-chain-details",
+ nodeClass: "tb-rule-chain-type",
+ icon: "settings_ethernet"
+ },
+ INPUT: {
+ value: "INPUT",
+ nodeClass: "tb-input-type",
+ icon: "input",
+ special: true
+ }
+ },
valueType: {
string: {
value: "string",
ui/src/app/components/ace-editor-fix.js 45(+45 -0)
diff --git a/ui/src/app/components/ace-editor-fix.js b/ui/src/app/components/ace-editor-fix.js
new file mode 100644
index 0000000..f68767e
--- /dev/null
+++ b/ui/src/app/components/ace-editor-fix.js
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default function fixAceEditor(aceEditor) {
+ aceEditor.$blockScrolling = Infinity;
+ aceEditor.on("showGutterTooltip", function (tooltip) {
+ if (!tooltip.isAttachedToBody) {
+ document.body.appendChild(tooltip.$element); //eslint-disable-line
+ tooltip.isAttachedToBody = true;
+ onElementRemoved(tooltip.$parentNode, () => {
+ if (tooltip.$element.parentNode != null) {
+ tooltip.$element.parentNode.removeChild(tooltip.$element);
+ }
+ });
+ }
+ });
+}
+
+function onElementRemoved(element, callback) {
+ if (!document.body.contains(element)) { //eslint-disable-line
+ callback();
+ } else {
+ var observer;
+ observer = new MutationObserver(function(mutations) { //eslint-disable-line
+ if (!document.body.contains(element)) { //eslint-disable-line
+ callback();
+ observer.disconnect();
+ }
+ });
+ observer.observe(document.body, {childList: true}); //eslint-disable-line
+ }
+}
diff --git a/ui/src/app/components/confirm-on-exit.directive.js b/ui/src/app/components/confirm-on-exit.directive.js
index fe9a9bd..e04e110 100644
--- a/ui/src/app/components/confirm-on-exit.directive.js
+++ b/ui/src/app/components/confirm-on-exit.directive.js
@@ -18,17 +18,17 @@ export default angular.module('thingsboard.directives.confirmOnExit', [])
.name;
/*@ngInject*/
-function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
+function ConfirmOnExit($state, $mdDialog, $window, $filter, $parse, userService) {
return {
- link: function ($scope) {
-
+ link: function ($scope, $element, $attributes) {
+ $scope.confirmForm = $scope.$eval($attributes.confirmForm);
$window.onbeforeunload = function () {
- if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
+ if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.$eval($attributes.isDirty))) {
return $filter('translate')('confirm-on-exit.message');
}
}
$scope.$on('$stateChangeStart', function (event, next, current, params) {
- if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
+ if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.$eval($attributes.isDirty))) {
event.preventDefault();
var confirm = $mdDialog.confirm()
.title($filter('translate')('confirm-on-exit.title'))
@@ -40,7 +40,9 @@ function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
if ($scope.confirmForm) {
$scope.confirmForm.$setPristine();
} else {
- $scope.isDirty = false;
+ var remoteSetter = $parse($attributes.isDirty).assign;
+ remoteSetter($scope, false);
+ //$scope.isDirty = false;
}
$state.go(next.name, params);
}, function () {
@@ -48,9 +50,6 @@ function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
}
});
},
- scope: {
- confirmForm: '=',
- isDirty: '='
- }
+ scope: false
};
}
\ No newline at end of file
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index 376f2d6..54a1a09 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -122,9 +122,9 @@
<md-menu-content id="menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
<md-menu-item ng-repeat ="item in vm.widgetContextMenuItems">
<md-button ng-disabled="!item.enabled" ng-click="item.action(vm.widgetContextMenuEvent, widget)">
+ <span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
<md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
<span translate>{{item.value}}</span>
- <span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
</md-button>
</md-menu-item>
</md-menu-content>
@@ -137,9 +137,9 @@
<md-menu-content id="menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
<md-menu-item ng-repeat ="item in vm.contextMenuItems">
<md-button ng-disabled="!item.enabled" ng-click="item.action(vm.contextMenuEvent)">
+ <span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
<md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
<span translate>{{item.value}}</span>
- <span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
</md-button>
</md-menu-item>
</md-menu-content>
diff --git a/ui/src/app/components/details-sidenav.directive.js b/ui/src/app/components/details-sidenav.directive.js
index e455a80..2516134 100644
--- a/ui/src/app/components/details-sidenav.directive.js
+++ b/ui/src/app/components/details-sidenav.directive.js
@@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.detailsSidenav', [])
.name;
/*@ngInject*/
-function DetailsSidenav($timeout) {
+function DetailsSidenav($timeout, $mdUtil, $q, $animate) {
var linker = function (scope, element, attrs) {
@@ -42,6 +42,63 @@ function DetailsSidenav($timeout) {
scope.isEdit = true;
}
+ var backdrop;
+ var previousContainerStyles;
+
+ if (attrs.hasOwnProperty('tbEnableBackdrop')) {
+ backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter");
+ element.on('$destroy', function() {
+ backdrop && backdrop.remove();
+ });
+ scope.$on('$destroy', function(){
+ backdrop && backdrop.remove();
+ });
+ scope.$watch('isOpen', updateIsOpen);
+ }
+
+ function updateIsOpen(isOpen) {
+ backdrop[isOpen ? 'on' : 'off']('click', (ev)=>{
+ ev.preventDefault();
+ scope.isOpen = false;
+ scope.$apply();
+ });
+ var parent = element.parent();
+ var restorePositioning = updateContainerPositions(parent, isOpen);
+
+ return $q.all([
+ isOpen && backdrop ? $animate.enter(backdrop, parent) : backdrop ?
+ $animate.leave(backdrop) : $q.when(true)
+ ]).then(function() {
+ restorePositioning && restorePositioning();
+ });
+ }
+
+ function updateContainerPositions(parent, willOpen) {
+ var drawerEl = element[0];
+ var scrollTop = parent[0].scrollTop;
+ if (willOpen && scrollTop) {
+ previousContainerStyles = {
+ top: drawerEl.style.top,
+ bottom: drawerEl.style.bottom,
+ height: drawerEl.style.height
+ };
+ var positionStyle = {
+ top: scrollTop + 'px',
+ bottom: 'auto',
+ height: parent[0].clientHeight + 'px'
+ };
+ backdrop.css(positionStyle);
+ }
+ if (!willOpen && previousContainerStyles) {
+ return function() {
+ backdrop[0].style.top = null;
+ backdrop[0].style.bottom = null;
+ backdrop[0].style.height = null;
+ previousContainerStyles = null;
+ };
+ }
+ }
+
scope.toggleDetailsEditMode = function () {
if (!scope.isAlwaysEdit) {
if (!scope.isEdit) {
ui/src/app/components/details-sidenav.scss 10(+10 -0)
diff --git a/ui/src/app/components/details-sidenav.scss b/ui/src/app/components/details-sidenav.scss
index c7e9919..360b133 100644
--- a/ui/src/app/components/details-sidenav.scss
+++ b/ui/src/app/components/details-sidenav.scss
@@ -59,4 +59,14 @@ md-sidenav.tb-sidenav-details {
background-color: $primary-hue-3;
}
}
+
+ md-tab-content.md-active > div {
+ height: 100%;
+ & > *:first-child {
+ height: 100%;
+ }
+ md-content {
+ height: 100%;
+ }
+ }
}
diff --git a/ui/src/app/components/details-sidenav.tpl.html b/ui/src/app/components/details-sidenav.tpl.html
index c504a24..763bc22 100644
--- a/ui/src/app/components/details-sidenav.tpl.html
+++ b/ui/src/app/components/details-sidenav.tpl.html
@@ -16,7 +16,7 @@
-->
<md-sidenav class="md-sidenav-right md-whiteframe-4dp tb-sidenav-details"
- md-disable-backdrop="true"
+ md-disable-backdrop
md-is-open="isOpen"
md-component-id="right"
layout="column">
ui/src/app/components/js-func.directive.js 39(+33 -6)
diff --git a/ui/src/app/components/js-func.directive.js b/ui/src/app/components/js-func.directive.js
index f95d003..ef77df7 100644
--- a/ui/src/app/components/js-func.directive.js
+++ b/ui/src/app/components/js-func.directive.js
@@ -22,6 +22,8 @@ import thingsboardToast from '../services/toast';
import thingsboardUtils from '../common/utils.service';
import thingsboardExpandFullscreen from './expand-fullscreen.directive';
+import fixAceEditor from './ace-editor-fix';
+
/* eslint-disable import/no-unresolved, import/default */
import jsFuncTemplate from './js-func.tpl.html';
@@ -41,6 +43,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
var template = $templateCache.get(jsFuncTemplate);
element.html(template);
+ scope.functionName = attrs.functionName;
scope.functionArgs = scope.$eval(attrs.functionArgs);
scope.validationArgs = scope.$eval(attrs.validationArgs);
scope.resultType = attrs.resultType;
@@ -48,6 +51,8 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
scope.resultType = "nocheck";
}
+ scope.validationTriggerArg = attrs.validationTriggerArg;
+
scope.functionValid = true;
var Range = ace.acequire("ace/range").Range;
@@ -56,7 +61,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
scope.functionArgsString = '';
- for (var i in scope.functionArgs) {
+ for (var i = 0; i < scope.functionArgs.length; i++) {
if (scope.functionArgsString.length > 0) {
scope.functionArgsString += ', ';
}
@@ -64,11 +69,15 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
}
scope.onFullscreenChanged = function () {
+ updateEditorSize();
+ };
+
+ function updateEditorSize() {
if (scope.js_editor) {
scope.js_editor.resize();
scope.js_editor.renderer.updateFull();
}
- };
+ }
scope.jsEditorOptions = {
useWrapMode: true,
@@ -83,6 +92,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
scope.js_editor.session.on("change", function () {
scope.cleanupJsErrors();
});
+ fixAceEditor(_ace);
}
};
@@ -128,6 +138,9 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
scope.validate = function () {
try {
var toValidate = new Function(scope.functionArgsString, scope.functionBody);
+ if (scope.noValidate) {
+ return true;
+ }
var res;
var validationError;
for (var i=0;i<scope.validationArgs.length;i++) {
@@ -197,9 +210,19 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
}
};
- scope.$on('form-submit', function () {
- scope.functionValid = scope.validate();
- scope.updateValidity();
+ scope.$on('form-submit', function (event, args) {
+ if (!args || scope.validationTriggerArg && scope.validationTriggerArg == args) {
+ scope.validationArgs = scope.$eval(attrs.validationArgs);
+ scope.cleanupJsErrors();
+ scope.functionValid = true;
+ scope.updateValidity();
+ scope.functionValid = scope.validate();
+ scope.updateValidity();
+ }
+ });
+
+ scope.$on('update-ace-editor-size', function () {
+ updateEditorSize();
});
$compile(element.contents())(scope);
@@ -208,7 +231,11 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
return {
restrict: "E",
require: "^ngModel",
- scope: {},
+ scope: {
+ disabled:'=ngDisabled',
+ noValidate: '=?',
+ fillHeight:'=?'
+ },
link: linker
};
}
ui/src/app/components/js-func.scss 12(+10 -2)
diff --git a/ui/src/app/components/js-func.scss b/ui/src/app/components/js-func.scss
index 2bd5df1..d800d5f 100644
--- a/ui/src/app/components/js-func.scss
+++ b/ui/src/app/components/js-func.scss
@@ -15,16 +15,24 @@
*/
tb-js-func {
position: relative;
+ .tb-disabled {
+ color: rgba(0,0,0,0.38);
+ }
+ .fill-height {
+ height: 100%;
+ }
}
.tb-js-func-panel {
margin-left: 15px;
border: 1px solid #C0C0C0;
- height: 100%;
+ height: calc(100% - 80px);
#tb-javascript-input {
min-width: 200px;
- min-height: 200px;
width: 100%;
height: 100%;
+ &:not(.fill-height) {
+ min-height: 200px;
+ }
}
}
ui/src/app/components/js-func.tpl.html 17(+9 -8)
diff --git a/ui/src/app/components/js-func.tpl.html b/ui/src/app/components/js-func.tpl.html
index 806de4a..d048598 100644
--- a/ui/src/app/components/js-func.tpl.html
+++ b/ui/src/app/components/js-func.tpl.html
@@ -15,19 +15,20 @@
limitations under the License.
-->
-<div style="background: #fff;" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
+<div style="background: #fff;" ng-class="{'tb-disabled': disabled, 'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()">
<div layout="row" layout-align="start center" style="height: 40px;">
- <span style="font-style: italic;">function({{ functionArgsString }}) {</span>
+ <label class="tb-title no-padding">function {{ functionName }}({{ functionArgsString }}) {</label>
<span flex></span>
<div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div>
</div>
- <div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column">
- <div flex id="tb-javascript-input"
- ui-ace="jsEditorOptions"
+ <div id="tb-javascript-panel" class="tb-js-func-panel">
+ <div id="tb-javascript-input" ng-class="{'fill-height': fillHeight}"
+ ui-ace="jsEditorOptions"
+ ng-readonly="disabled"
ng-model="functionBody">
</div>
</div>
<div layout="row" layout-align="start center" style="height: 40px;">
- <span style="font-style: italic;">}</span>
- </div>
-</div>
\ No newline at end of file
+ <label class="tb-title no-padding">}</label>
+ </div>
+</div>
ui/src/app/components/json-content.directive.js 175(+175 -0)
diff --git a/ui/src/app/components/json-content.directive.js b/ui/src/app/components/json-content.directive.js
new file mode 100644
index 0000000..84f8417
--- /dev/null
+++ b/ui/src/app/components/json-content.directive.js
@@ -0,0 +1,175 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './json-content.scss';
+
+import 'brace/ext/language_tools';
+import 'brace/mode/json';
+import 'brace/mode/text';
+import 'ace-builds/src-min-noconflict/snippets/json';
+import 'ace-builds/src-min-noconflict/snippets/text';
+
+import fixAceEditor from './ace-editor-fix';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import jsonContentTemplate from './json-content.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.jsonContent', [])
+ .directive('tbJsonContent', JsonContent)
+ .name;
+
+/*@ngInject*/
+function JsonContent($compile, $templateCache, toast, types, utils) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+ var template = $templateCache.get(jsonContentTemplate);
+ element.html(template);
+
+ scope.label = attrs.label;
+
+ scope.validationTriggerArg = attrs.validationTriggerArg;
+
+ scope.contentValid = true;
+
+ scope.json_editor;
+
+ scope.onFullscreenChanged = function () {
+ updateEditorSize();
+ };
+
+ function updateEditorSize() {
+ if (scope.json_editor) {
+ scope.json_editor.resize();
+ scope.json_editor.renderer.updateFull();
+ }
+ }
+
+ var mode;
+ if (scope.contentType) {
+ mode = types.contentType[scope.contentType].code;
+ } else {
+ mode = 'text';
+ }
+
+ scope.jsonEditorOptions = {
+ useWrapMode: true,
+ mode: mode,
+ advanced: {
+ enableSnippets: true,
+ enableBasicAutocompletion: true,
+ enableLiveAutocompletion: true
+ },
+ onLoad: function (_ace) {
+ scope.json_editor = _ace;
+ scope.json_editor.session.on("change", function () {
+ scope.cleanupJsonErrors();
+ });
+ fixAceEditor(_ace);
+ }
+ };
+
+ scope.$watch('contentType', () => {
+ var mode;
+ if (scope.contentType) {
+ mode = types.contentType[scope.contentType].code;
+ } else {
+ mode = 'text';
+ }
+ if (scope.json_editor) {
+ scope.json_editor.session.setMode('ace/mode/' + mode);
+ }
+ });
+
+ scope.cleanupJsonErrors = function () {
+ toast.hide();
+ };
+
+ scope.updateValidity = function () {
+ ngModelCtrl.$setValidity('contentBody', scope.contentValid);
+ };
+
+ scope.$watch('contentBody', function (newContent, oldContent) {
+ ngModelCtrl.$setViewValue(scope.contentBody);
+ if (!angular.equals(newContent, oldContent)) {
+ scope.contentValid = true;
+ }
+ scope.updateValidity();
+ });
+
+ ngModelCtrl.$render = function () {
+ scope.contentBody = ngModelCtrl.$viewValue;
+ };
+
+ scope.showError = function (error) {
+ var toastParent = angular.element('#tb-json-panel', element);
+ toast.showError(error, toastParent, 'bottom left');
+ };
+
+ scope.validate = function () {
+ try {
+ if (scope.validateContent) {
+ if (scope.contentType == types.contentType.JSON.value) {
+ angular.fromJson(scope.contentBody);
+ }
+ }
+ return true;
+ } catch (e) {
+ var details = utils.parseException(e);
+ var errorInfo = 'Error:';
+ if (details.name) {
+ errorInfo += ' ' + details.name + ':';
+ }
+ if (details.message) {
+ errorInfo += ' ' + details.message;
+ }
+ scope.showError(errorInfo);
+ return false;
+ }
+ };
+
+ scope.$on('form-submit', function (event, args) {
+ if (!scope.readonly) {
+ if (!args || scope.validationTriggerArg && scope.validationTriggerArg == args) {
+ scope.cleanupJsonErrors();
+ scope.contentValid = true;
+ scope.updateValidity();
+ scope.contentValid = scope.validate();
+ scope.updateValidity();
+ }
+ }
+ });
+
+ scope.$on('update-ace-editor-size', function () {
+ updateEditorSize();
+ });
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {
+ contentType: '=',
+ validateContent: '=?',
+ readonly:'=ngReadonly',
+ fillHeight:'=?'
+ },
+ link: linker
+ };
+}
ui/src/app/components/json-content.scss 35(+35 -0)
diff --git a/ui/src/app/components/json-content.scss b/ui/src/app/components/json-content.scss
new file mode 100644
index 0000000..db57451
--- /dev/null
+++ b/ui/src/app/components/json-content.scss
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+tb-json-content {
+ position: relative;
+ .fill-height {
+ height: 100%;
+ }
+}
+
+.tb-json-content-panel {
+ margin-left: 15px;
+ border: 1px solid #C0C0C0;
+ height: 100%;
+ #tb-json-input {
+ min-width: 200px;
+ width: 100%;
+ height: 100%;
+ &:not(.fill-height) {
+ min-height: 200px;
+ }
+ }
+}
ui/src/app/components/json-content.tpl.html 31(+31 -0)
diff --git a/ui/src/app/components/json-content.tpl.html b/ui/src/app/components/json-content.tpl.html
new file mode 100644
index 0000000..4fad30e
--- /dev/null
+++ b/ui/src/app/components/json-content.tpl.html
@@ -0,0 +1,31 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
+ <div layout="row" layout-align="start center">
+ <label class="tb-title no-padding">{{ label }}</label>
+ <span flex></span>
+ <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
+ </div>
+ <div flex id="tb-json-panel" class="tb-json-content-panel" layout="column">
+ <div flex id="tb-json-input" ng-class="{'fill-height': fillHeight}"
+ ng-readonly="readonly"
+ ui-ace="jsonEditorOptions"
+ ng-model="contentBody">
+ </div>
+ </div>
+</div>
ui/src/app/components/json-object-edit.directive.js 183(+183 -0)
diff --git a/ui/src/app/components/json-object-edit.directive.js b/ui/src/app/components/json-object-edit.directive.js
new file mode 100644
index 0000000..215b7b9
--- /dev/null
+++ b/ui/src/app/components/json-object-edit.directive.js
@@ -0,0 +1,183 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './json-object-edit.scss';
+
+import 'brace/ext/language_tools';
+import 'brace/mode/json';
+import 'ace-builds/src-min-noconflict/snippets/json';
+
+import fixAceEditor from './ace-editor-fix';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import jsonObjectEditTemplate from './json-object-edit.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.jsonObjectEdit', [])
+ .directive('tbJsonObjectEdit', JsonObjectEdit)
+ .name;
+
+/*@ngInject*/
+function JsonObjectEdit($compile, $templateCache, $document, toast, utils) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+ var template = $templateCache.get(jsonObjectEditTemplate);
+ element.html(template);
+
+ scope.label = attrs.label;
+
+ scope.objectValid = true;
+ scope.validationError = '';
+
+ scope.json_editor;
+
+ scope.onFullscreenChanged = function () {
+ updateEditorSize();
+ };
+
+ function updateEditorSize() {
+ if (scope.json_editor) {
+ scope.json_editor.resize();
+ scope.json_editor.renderer.updateFull();
+ }
+ }
+
+ scope.jsonEditorOptions = {
+ useWrapMode: true,
+ mode: 'json',
+ advanced: {
+ enableSnippets: true,
+ enableBasicAutocompletion: true,
+ enableLiveAutocompletion: true
+ },
+ onLoad: function (_ace) {
+ scope.json_editor = _ace;
+ scope.json_editor.session.on("change", function () {
+ scope.cleanupJsonErrors();
+ });
+ fixAceEditor(_ace);
+ }
+ };
+
+ scope.cleanupJsonErrors = function () {
+ toast.hide();
+ };
+
+ scope.updateValidity = function () {
+ ngModelCtrl.$setValidity('objectValid', scope.objectValid);
+ };
+
+ scope.$watch('contentBody', function (newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal)) {
+ var object = scope.validate();
+ if (scope.objectValid) {
+ if (object == null) {
+ scope.object = null;
+ } else {
+ if (scope.object == null) {
+ scope.object = {};
+ }
+ Object.keys(scope.object).forEach(function (key) {
+ delete scope.object[key];
+ });
+ Object.keys(object).forEach(function (key) {
+ scope.object[key] = object[key];
+ });
+ }
+ ngModelCtrl.$setViewValue(scope.object);
+ }
+ scope.updateValidity();
+ }
+ });
+
+ ngModelCtrl.$render = function () {
+ scope.object = ngModelCtrl.$viewValue;
+ var content = '';
+ try {
+ if (scope.object) {
+ content = angular.toJson(scope.object, true);
+ }
+ } catch (e) {
+ //
+ }
+ scope.contentBody = content;
+ };
+
+ scope.showError = function (error) {
+ var toastParent = angular.element('#tb-json-panel', element);
+ toast.showError(error, toastParent, 'bottom left');
+ };
+
+ scope.validate = function () {
+ if (!scope.contentBody || !scope.contentBody.length) {
+ if (scope.required) {
+ scope.validationError = 'Json object is required.';
+ scope.objectValid = false;
+ } else {
+ scope.validationError = '';
+ scope.objectValid = true;
+ }
+ return null;
+ } else {
+ try {
+ var object = angular.fromJson(scope.contentBody);
+ scope.validationError = '';
+ scope.objectValid = true;
+ return object;
+ } catch (e) {
+ var details = utils.parseException(e);
+ var errorInfo = 'Error:';
+ if (details.name) {
+ errorInfo += ' ' + details.name + ':';
+ }
+ if (details.message) {
+ errorInfo += ' ' + details.message;
+ }
+ scope.validationError = errorInfo;
+ scope.objectValid = false;
+ return null;
+ }
+ }
+ };
+
+ scope.$on('form-submit', function () {
+ if (!scope.readonly) {
+ scope.cleanupJsonErrors();
+ if (!scope.objectValid) {
+ scope.showError(scope.validationError);
+ }
+ }
+ });
+
+ scope.$on('update-ace-editor-size', function () {
+ updateEditorSize();
+ });
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {
+ required:'=ngRequired',
+ readonly:'=ngReadonly',
+ fillHeight:'=?'
+ },
+ link: linker
+ };
+}
ui/src/app/components/json-object-edit.scss 35(+35 -0)
diff --git a/ui/src/app/components/json-object-edit.scss b/ui/src/app/components/json-object-edit.scss
new file mode 100644
index 0000000..232d69a
--- /dev/null
+++ b/ui/src/app/components/json-object-edit.scss
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+tb-json-object-edit {
+ position: relative;
+ .fill-height {
+ height: 100%;
+ }
+}
+
+.tb-json-object-panel {
+ margin-left: 15px;
+ border: 1px solid #C0C0C0;
+ height: 100%;
+ #tb-json-input {
+ min-width: 200px;
+ width: 100%;
+ height: 100%;
+ &:not(.fill-height) {
+ min-height: 200px;
+ }
+ }
+}
diff --git a/ui/src/app/components/json-object-edit.tpl.html b/ui/src/app/components/json-object-edit.tpl.html
new file mode 100644
index 0000000..ebab3c7
--- /dev/null
+++ b/ui/src/app/components/json-object-edit.tpl.html
@@ -0,0 +1,34 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
+ <div layout="row" layout-align="start center">
+ <label class="tb-title no-padding"
+ ng-class="{'tb-required': required,
+ 'tb-readonly': readonly,
+ 'tb-error': !objectValid}">{{ label }}</label>
+ <span flex></span>
+ <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
+ </div>
+ <div flex id="tb-json-panel" class="tb-json-object-panel" layout="column">
+ <div flex id="tb-json-input" ng-class="{'fill-height': fillHeight}"
+ ng-readonly="readonly"
+ ui-ace="jsonEditorOptions"
+ ng-model="contentBody">
+ </div>
+ </div>
+</div>
ui/src/app/components/kv-map.directive.js 119(+119 -0)
diff --git a/ui/src/app/components/kv-map.directive.js b/ui/src/app/components/kv-map.directive.js
new file mode 100644
index 0000000..bc8865f
--- /dev/null
+++ b/ui/src/app/components/kv-map.directive.js
@@ -0,0 +1,119 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './kv-map.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import kvMapTemplate from './kv-map.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.keyValMap', [])
+ .directive('tbKeyValMap', KeyValMap)
+ .name;
+
+/*@ngInject*/
+function KeyValMap() {
+ return {
+ restrict: "E",
+ scope: true,
+ bindToController: {
+ disabled:'=ngDisabled',
+ titleText: '@?',
+ keyPlaceholderText: '@?',
+ valuePlaceholderText: '@?',
+ noDataText: '@?',
+ keyValMap: '='
+ },
+ controller: KeyValMapController,
+ controllerAs: 'vm',
+ templateUrl: kvMapTemplate
+ };
+}
+
+/*@ngInject*/
+function KeyValMapController($scope, $mdUtil) {
+
+ let vm = this;
+
+ vm.kvList = [];
+
+ vm.removeKeyVal = removeKeyVal;
+ vm.addKeyVal = addKeyVal;
+
+ $scope.$watch('vm.keyValMap', () => {
+ stopWatchKvList();
+ vm.kvList.length = 0;
+ if (vm.keyValMap) {
+ for (var property in vm.keyValMap) {
+ if (vm.keyValMap.hasOwnProperty(property)) {
+ vm.kvList.push(
+ {
+ key: property + '',
+ value: vm.keyValMap[property]
+ }
+ );
+ }
+ }
+ }
+ $mdUtil.nextTick(() => {
+ watchKvList();
+ });
+ });
+
+ function watchKvList() {
+ $scope.kvListWatcher = $scope.$watch('vm.kvList', () => {
+ if (!vm.keyValMap) {
+ return;
+ }
+ for (var property in vm.keyValMap) {
+ if (vm.keyValMap.hasOwnProperty(property)) {
+ delete vm.keyValMap[property];
+ }
+ }
+ for (var i=0;i<vm.kvList.length;i++) {
+ var entry = vm.kvList[i];
+ vm.keyValMap[entry.key] = entry.value;
+ }
+ }, true);
+ }
+
+ function stopWatchKvList() {
+ if ($scope.kvListWatcher) {
+ $scope.kvListWatcher();
+ $scope.kvListWatcher = null;
+ }
+ }
+
+
+ function removeKeyVal(index) {
+ if (index > -1) {
+ vm.kvList.splice(index, 1);
+ }
+ }
+
+ function addKeyVal() {
+ if (!vm.kvList) {
+ vm.kvList = [];
+ }
+ vm.kvList.push(
+ {
+ key: '',
+ value: ''
+ }
+ );
+ }
+}
ui/src/app/components/kv-map.tpl.html 58(+58 -0)
diff --git a/ui/src/app/components/kv-map.tpl.html b/ui/src/app/components/kv-map.tpl.html
new file mode 100644
index 0000000..3e550f3
--- /dev/null
+++ b/ui/src/app/components/kv-map.tpl.html
@@ -0,0 +1,58 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<section layout="column" class="tb-kv-map">
+ <label translate class="tb-title no-padding">{{ vm.titleText }}</label>
+ <div layout="row"
+ ng-repeat="keyVal in vm.kvList track by $index"
+ style="max-height: 40px;" layout-align="start center">
+ <md-input-container flex md-no-float class="md-block"
+ style="margin: 10px 0px 0px 0px; max-height: 40px;">
+ <input placeholder="{{ (vm.keyPlaceholderText ? vm.keyPlaceholderText : 'key-val.key') | translate }}"
+ ng-disabled="vm.disabled" ng-required="true" name="key" ng-model="keyVal.key">
+ </md-input-container>
+ <md-input-container flex md-no-float class="md-block"
+ style="margin: 10px 0px 0px 0px; max-height: 40px;">
+ <input placeholder="{{ (vm.valuePlaceholderText ? vm.valuePlaceholderText : 'key-val.value') | translate }}"
+ ng-disabled="vm.disabled" ng-required="true" name="value" ng-model="keyVal.value">
+ </md-input-container>
+ <md-button ng-show="!vm.disabled" ng-disabled="$root.loading" class="md-icon-button md-primary"
+ ng-click="vm.removeKeyVal($index)"
+ aria-label="{{ 'action.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'key-val.remove-entry' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.delete' | translate }}"
+ class="material-icons">
+ close
+ </md-icon>
+ </md-button>
+ </div>
+ <span ng-show="!vm.kvList.length"
+ layout-align="center center" ng-class="{'disabled': vm.disabled}"
+ class="no-data-found" translate>{{vm.noDataText ? vm.noDataText : 'key-val.no-data'}}</span>
+ <div>
+ <md-button ng-show="!vm.disabled" ng-disabled="$root.loading" class="md-primary md-raised"
+ ng-click="vm.addKeyVal()"
+ aria-label="{{ 'action.add' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'key-val.add-entry' | translate }}
+ </md-tooltip>
+ <span translate>action.add</span>
+ </md-button>
+ </div>
+</section>
diff --git a/ui/src/app/components/mousepoint-menu.directive.js b/ui/src/app/components/mousepoint-menu.directive.js
index 2a0ab52..9015268 100644
--- a/ui/src/app/components/mousepoint-menu.directive.js
+++ b/ui/src/app/components/mousepoint-menu.directive.js
@@ -27,7 +27,12 @@ function MousepointMenu() {
var offset = $element.offset();
var x = $event.pageX - offset.left;
var y = $event.pageY - offset.top;
-
+ if ($attrs.tbOffsetX) {
+ x += Number($attrs.tbOffsetX);
+ }
+ if ($attrs.tbOffsetY) {
+ y += Number($attrs.tbOffsetY);
+ }
var offsets = {
left: x,
top: y
diff --git a/ui/src/app/components/react/json-form-ace-editor.jsx b/ui/src/app/components/react/json-form-ace-editor.jsx
index 1c4c02e..5afd3d1 100644
--- a/ui/src/app/components/react/json-form-ace-editor.jsx
+++ b/ui/src/app/components/react/json-form-ace-editor.jsx
@@ -23,6 +23,8 @@ import FlatButton from 'material-ui/FlatButton';
import 'brace/ext/language_tools';
import 'brace/theme/github';
+import fixAceEditor from './../ace-editor-fix';
+
class ThingsboardAceEditor extends React.Component {
constructor(props) {
@@ -31,6 +33,7 @@ class ThingsboardAceEditor extends React.Component {
this.onBlur = this.onBlur.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onTidy = this.onTidy.bind(this);
+ this.onLoad = this.onLoad.bind(this);
var value = props.value ? props.value + '' : '';
this.state = {
value: value,
@@ -72,6 +75,10 @@ class ThingsboardAceEditor extends React.Component {
}
}
+ onLoad(editor) {
+ fixAceEditor(editor);
+ }
+
render() {
const styles = reactCSS({
@@ -117,6 +124,7 @@ class ThingsboardAceEditor extends React.Component {
onChange={this.onValueChanged}
onFocus={this.onFocus}
onBlur={this.onBlur}
+ onLoad={this.onLoad}
name={this.props.form.title}
value={this.state.value}
readOnly={this.props.form.readonly}
diff --git a/ui/src/app/components/widget/widget-config.directive.js b/ui/src/app/components/widget/widget-config.directive.js
index d0ee6a3..4d4d958 100644
--- a/ui/src/app/components/widget/widget-config.directive.js
+++ b/ui/src/app/components/widget/widget-config.directive.js
@@ -23,6 +23,8 @@ import thingsboardJsonForm from '../json-form.directive';
import thingsboardManageWidgetActions from './action/manage-widget-actions.directive';
import 'angular-ui-ace';
+import fixAceEditor from './../ace-editor-fix';
+
import './widget-config.scss';
/* eslint-disable import/no-unresolved, import/default */
@@ -72,6 +74,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
+ },
+ onLoad: function (_ace) {
+ fixAceEditor(_ace);
}
};
diff --git a/ui/src/app/entity/entity-autocomplete.directive.js b/ui/src/app/entity/entity-autocomplete.directive.js
index c2053c0..2dfc3be 100644
--- a/ui/src/app/entity/entity-autocomplete.directive.js
+++ b/ui/src/app/entity/entity-autocomplete.directive.js
@@ -143,6 +143,12 @@ export default function EntityAutocomplete($compile, $templateCache, $q, $filter
scope.noEntitiesMatchingText = 'plugin.no-plugins-matching';
scope.entityRequiredText = 'plugin.plugin-required';
break;
+ case types.entityType.rulechain:
+ scope.selectEntityText = 'rulechain.select-rulechain';
+ scope.entityText = 'rulechain.rulechain';
+ scope.noEntitiesMatchingText = 'rulechain.no-rulechains-matching';
+ scope.entityRequiredText = 'rulechain.rulechain-required';
+ break;
case types.entityType.tenant:
scope.selectEntityText = 'tenant.select-tenant';
scope.entityText = 'tenant.tenant';
diff --git a/ui/src/app/entity/entity-type-list.tpl.html b/ui/src/app/entity/entity-type-list.tpl.html
index eb32124..d7d3918 100644
--- a/ui/src/app/entity/entity-type-list.tpl.html
+++ b/ui/src/app/entity/entity-type-list.tpl.html
@@ -16,8 +16,7 @@
-->
<section flex layout='column' class="tb-entity-type-list">
- <md-chips flex
- readonly="disabled"
+ <md-chips readonly="disabled"
id="entity_type_list_chips"
ng-required="tbRequired"
ng-model="entityTypeList"
diff --git a/ui/src/app/entity/relation/relation-filters.directive.js b/ui/src/app/entity/relation/relation-filters.directive.js
index 00d3b26..9ab66ca 100644
--- a/ui/src/app/entity/relation/relation-filters.directive.js
+++ b/ui/src/app/entity/relation/relation-filters.directive.js
@@ -46,6 +46,7 @@ export default function RelationFilters($compile, $templateCache) {
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
+ scope.relationFilters.length = 0;
value.forEach(function (filter) {
scope.relationFilters.push(filter);
});
diff --git a/ui/src/app/entity/relation/relation-filters.scss b/ui/src/app/entity/relation/relation-filters.scss
index 649879d..34254d0 100644
--- a/ui/src/app/entity/relation/relation-filters.scss
+++ b/ui/src/app/entity/relation/relation-filters.scss
@@ -51,9 +51,6 @@
md-chips-wrap {
padding: 0px;
margin: 0 0 24px;
- .md-chip-input-container {
- margin: 0;
- }
md-autocomplete {
height: 30px;
md-autocomplete-wrap {
ui/src/app/event/event.scss 25(+21 -4)
diff --git a/ui/src/app/event/event.scss b/ui/src/app/event/event.scss
index b3be35c..b0fc46f 100644
--- a/ui/src/app/event/event.scss
+++ b/ui/src/app/event/event.scss
@@ -24,6 +24,17 @@ md-list.tb-event-table {
height: 48px;
padding: 0px;
overflow: hidden;
+ .tb-cell {
+ text-overflow: ellipsis;
+ &.tb-scroll {
+ white-space: nowrap;
+ overflow-y: hidden;
+ overflow-x: auto;
+ }
+ &.tb-nowrap {
+ white-space: nowrap;
+ }
+ }
}
.tb-row:hover {
@@ -39,13 +50,19 @@ md-list.tb-event-table {
color: rgba(0,0,0,.54);
font-size: 12px;
font-weight: 700;
- white-space: nowrap;
background: none;
+ white-space: nowrap;
}
}
.tb-cell {
- padding: 0 24px;
+ &:first-child {
+ padding-left: 14px;
+ }
+ &:last-child {
+ padding-right: 14px;
+ }
+ padding: 0 6px;
margin: auto 0;
color: rgba(0,0,0,.87);
font-size: 13px;
@@ -53,8 +70,8 @@ md-list.tb-event-table {
text-align: left;
overflow: hidden;
.md-button {
- padding: 0;
- margin: 0;
+ padding: 0;
+ margin: 0;
}
}
diff --git a/ui/src/app/event/event-content-dialog.controller.js b/ui/src/app/event/event-content-dialog.controller.js
index 108f95e..b780b78 100644
--- a/ui/src/app/event/event-content-dialog.controller.js
+++ b/ui/src/app/event/event-content-dialog.controller.js
@@ -17,11 +17,14 @@ import $ from 'jquery';
import 'brace/ext/language_tools';
import 'brace/mode/java';
import 'brace/theme/github';
+import beautify from 'js-beautify';
/* eslint-disable angular/angularelement */
+const js_beautify = beautify.js;
+
/*@ngInject*/
-export default function EventContentDialogController($mdDialog, content, title, showingCallback) {
+export default function EventContentDialogController($mdDialog, types, content, contentType, title, showingCallback) {
var vm = this;
@@ -32,9 +35,19 @@ export default function EventContentDialogController($mdDialog, content, title,
vm.content = content;
vm.title = title;
+ var mode;
+ if (contentType) {
+ mode = types.contentType[contentType].code;
+ if (contentType == types.contentType.JSON.value && vm.content) {
+ vm.content = js_beautify(vm.content, {indent_size: 4});
+ }
+ } else {
+ mode = 'java';
+ }
+
vm.contentOptions = {
useWrapMode: false,
- mode: 'java',
+ mode: mode,
showGutter: false,
showPrintMargin: false,
theme: 'github',
@@ -55,7 +68,7 @@ export default function EventContentDialogController($mdDialog, content, title,
var lines = vm.content.split('\n');
newHeight = 16 * lines.length + 16;
var maxLineLength = 0;
- for (var i in lines) {
+ for (var i = 0; i < lines.length; i++) {
var line = lines[i].replace(/\t/g, ' ').replace(/\n/g, '');
var lineLength = line.length;
maxLineLength = Math.max(maxLineLength, lineLength);
diff --git a/ui/src/app/event/event-header.directive.js b/ui/src/app/event/event-header.directive.js
index afac804..bc4cdbe 100644
--- a/ui/src/app/event/event-header.directive.js
+++ b/ui/src/app/event/event-header.directive.js
@@ -18,6 +18,7 @@
import eventHeaderLcEventTemplate from './event-header-lc-event.tpl.html';
import eventHeaderStatsTemplate from './event-header-stats.tpl.html';
import eventHeaderErrorTemplate from './event-header-error.tpl.html';
+import eventHeaderDebugRuleNodeTemplate from './event-header-debug-rulenode.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
@@ -38,6 +39,12 @@ export default function EventHeaderDirective($compile, $templateCache, types) {
case types.eventType.error.value:
template = eventHeaderErrorTemplate;
break;
+ case types.debugEventType.debugRuleNode.value:
+ template = eventHeaderDebugRuleNodeTemplate;
+ break;
+ case types.debugEventType.debugRuleChain.value:
+ template = eventHeaderDebugRuleNodeTemplate;
+ break;
}
return $templateCache.get(template);
}
diff --git a/ui/src/app/event/event-header-debug-rulenode.tpl.html b/ui/src/app/event/event-header-debug-rulenode.tpl.html
new file mode 100644
index 0000000..34f4513
--- /dev/null
+++ b/ui/src/app/event/event-header-debug-rulenode.tpl.html
@@ -0,0 +1,27 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div hide-xs hide-sm translate class="tb-cell" flex="25">event.event-time</div>
+<div translate class="tb-cell" flex="20">event.server</div>
+<div translate class="tb-cell" flex="10">event.type</div>
+<div translate class="tb-cell" flex="15">event.entity</div>
+<div translate class="tb-cell" flex="20">event.message-id</div>
+<div translate class="tb-cell" flex="20">event.message-type</div>
+<div translate class="tb-cell" flex="15">event.data-type</div>
+<div translate class="tb-cell" flex="10">event.data</div>
+<div translate class="tb-cell" flex="10">event.metadata</div>
+<div translate class="tb-cell" flex="10">event.error</div>
ui/src/app/event/event-row.directive.js 24(+22 -2)
diff --git a/ui/src/app/event/event-row.directive.js b/ui/src/app/event/event-row.directive.js
index f005542..b808fb8 100644
--- a/ui/src/app/event/event-row.directive.js
+++ b/ui/src/app/event/event-row.directive.js
@@ -20,6 +20,7 @@ import eventErrorDialogTemplate from './event-content-dialog.tpl.html';
import eventRowLcEventTemplate from './event-row-lc-event.tpl.html';
import eventRowStatsTemplate from './event-row-stats.tpl.html';
import eventRowErrorTemplate from './event-row-error.tpl.html';
+import eventRowDebugRuleNodeTemplate from './event-row-debug-rulenode.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
@@ -40,6 +41,12 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
case types.eventType.error.value:
template = eventRowErrorTemplate;
break;
+ case types.debugEventType.debugRuleNode.value:
+ template = eventRowDebugRuleNodeTemplate;
+ break;
+ case types.debugEventType.debugRuleChain.value:
+ template = eventRowDebugRuleNodeTemplate;
+ break;
}
return $templateCache.get(template);
}
@@ -53,17 +60,22 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
scope.loadTemplate();
});
+ scope.types = types;
+
scope.event = attrs.event;
- scope.showContent = function($event, content, title) {
+ scope.showContent = function($event, content, title, contentType) {
var onShowingCallback = {
onShowing: function(){}
}
+ if (!contentType) {
+ contentType = null;
+ }
$mdDialog.show({
controller: 'EventContentDialogController',
controllerAs: 'vm',
templateUrl: eventErrorDialogTemplate,
- locals: {content: content, title: title, showingCallback: onShowingCallback},
+ locals: {content: content, title: title, contentType: contentType, showingCallback: onShowingCallback},
parent: angular.element($document[0].body),
fullscreen: true,
targetEvent: $event,
@@ -74,6 +86,14 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
});
}
+ scope.checkTooltip = function($event) {
+ var el = $event.target;
+ var $el = angular.element(el);
+ if(el.offsetWidth < el.scrollWidth && !$el.attr('title')){
+ $el.attr('title', $el.text());
+ }
+ }
+
$compile(element.contents())(scope);
}
diff --git a/ui/src/app/event/event-row-debug-rulenode.tpl.html b/ui/src/app/event/event-row-debug-rulenode.tpl.html
new file mode 100644
index 0000000..bb832b1
--- /dev/null
+++ b/ui/src/app/event/event-row-debug-rulenode.tpl.html
@@ -0,0 +1,63 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div hide-xs hide-sm class="tb-cell" flex="25">{{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}</div>
+<div class="tb-cell" flex="20">{{event.body.server}}</div>
+<div class="tb-cell" flex="10">{{event.body.type}}</div>
+<div class="tb-cell" flex="15">{{event.body.entityName}}</div>
+<div class="tb-cell tb-nowrap" flex="20" ng-mouseenter="checkTooltip($event)">{{event.body.msgId}}</div>
+<div class="tb-cell" flex="20" ng-mouseenter="checkTooltip($event)">{{event.body.msgType}}</div>
+<div class="tb-cell" flex="15">{{event.body.dataType}}</div>
+<div class="tb-cell" flex="10">
+ <md-button ng-if="event.body.data" class="md-icon-button md-primary"
+ ng-click="showContent($event, event.body.data, 'event.data', event.body.dataType)"
+ aria-label="{{ 'action.view' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.view' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.view' | translate }}"
+ class="material-icons">
+ more_horiz
+ </md-icon>
+ </md-button>
+</div>
+<div class="tb-cell" flex="10">
+ <md-button ng-if="event.body.metadata" class="md-icon-button md-primary"
+ ng-click="showContent($event, event.body.metadata, 'event.metadata', 'JSON')"
+ aria-label="{{ 'action.view' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.view' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.view' | translate }}"
+ class="material-icons">
+ more_horiz
+ </md-icon>
+ </md-button>
+</div>
+<div class="tb-cell" flex="10">
+ <md-button ng-if="event.body.error" class="md-icon-button md-primary"
+ ng-click="showContent($event, event.body.error, 'event.error')"
+ aria-label="{{ 'action.view' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.view' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.view' | translate }}"
+ class="material-icons">
+ more_horiz
+ </md-icon>
+ </md-button>
+</div>
ui/src/app/event/event-table.directive.js 18(+15 -3)
diff --git a/ui/src/app/event/event-table.directive.js b/ui/src/app/event/event-table.directive.js
index 4291014..c61078d 100644
--- a/ui/src/app/event/event-table.directive.js
+++ b/ui/src/app/event/event-table.directive.js
@@ -36,8 +36,8 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
for (var type in types.eventType) {
var eventType = types.eventType[type];
var enabled = true;
- for (var disabledType in disabledEventTypes) {
- if (eventType.value === disabledEventTypes[disabledType]) {
+ for (var i=0;i<disabledEventTypes.length;i++) {
+ if (eventType.value === disabledEventTypes[i]) {
enabled = false;
break;
}
@@ -47,7 +47,19 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
}
}
} else {
- scope.eventTypes = types.eventType;
+ scope.eventTypes = angular.copy(types.eventType);
+ }
+
+ if (attrs.debugEventTypes) {
+ var debugEventTypes = attrs.debugEventTypes.split(',');
+ for (i=0;i<debugEventTypes.length;i++) {
+ for (type in types.debugEventType) {
+ eventType = types.debugEventType[type];
+ if (eventType.value === debugEventTypes[i]) {
+ scope.eventTypes[type] = eventType;
+ }
+ }
+ }
}
scope.eventType = attrs.defaultEventType;
diff --git a/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js b/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
index bf4886c..e1d2519 100644
--- a/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
+++ b/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
@@ -128,8 +128,8 @@ export default function ExtensionFormOpcDirective($compile, $templateCache, $tra
let addedFile = event.target.result;
if (addedFile && addedFile.length > 0) {
- model[options.fileName] = $file.name;
- model[options.file] = addedFile.replace(/^data.*base64,/, "");
+ model[options.location] = $file.name;
+ model[options.fileContent] = addedFile.replace(/^data.*base64,/, "");
}
}
@@ -142,8 +142,8 @@ export default function ExtensionFormOpcDirective($compile, $templateCache, $tra
scope.clearFile = function(model, options) {
scope.theForm.$setDirty();
- model[options.fileName] = null;
- model[options.file] = null;
+ model[options.location] = null;
+ model[options.fileContent] = null;
};
diff --git a/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
index 501eeeb..5a7c00b 100644
--- a/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
+++ b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
@@ -212,8 +212,8 @@
</md-input-container>
<section class="dropdown-section">
- <div class="tb-container" ng-class="{'ng-invalid':!server.keystore.file}">
- <span ng-init='fieldsToFill = {"fileName":"fileName", "file":"file"}'></span>
+ <div class="tb-container" ng-class="{'ng-invalid':!server.keystore.fileContent}">
+ <span ng-init='fieldsToFill = {"location":"location", "fileContent":"fileContent"}'></span>
<label class="tb-label" translate>extension.opc-keystore-location</label>
<div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, server.keystore, fieldsToFill)' class="tb-file-select-container">
<div class="tb-file-clear-container">
@@ -231,14 +231,14 @@
class="file-input"
flow-btn id="dropFileKeystore_{{serverIndex}}"
name="keystoreFile"
- ng-model="server.keystore.file"
+ ng-model="server.keystore.fileContent"
>
</div>
</div>
</div>
<div class="dropdown-messages">
- <div ng-if="!server.keystore[fieldsToFill.fileName]" class="tb-error-message" translate>extension.no-file</div>
- <div ng-if="server.keystore[fieldsToFill.fileName]">{{server.keystore[fieldsToFill.fileName]}}</div>
+ <div ng-if="!server.keystore[fieldsToFill.location]" class="tb-error-message" translate>extension.no-file</div>
+ <div ng-if="server.keystore[fieldsToFill.location]">{{server.keystore[fieldsToFill.location]}}</div>
</div>
</section>
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
index 6071fd2..88c6353 100644
--- a/ui/src/app/import-export/import-export.service.js
+++ b/ui/src/app/import-export/import-export.service.js
@@ -26,7 +26,7 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
/*@ngInject*/
export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types,
dashboardUtils, entityService, dashboardService, pluginService, ruleService,
- widgetService, toast, attributeService) {
+ ruleChainService, widgetService, toast, attributeService) {
var service = {
@@ -38,6 +38,8 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
importPlugin: importPlugin,
exportRule: exportRule,
importRule: importRule,
+ exportRuleChain: exportRuleChain,
+ importRuleChain: importRuleChain,
exportWidgetType: exportWidgetType,
importWidgetType: importWidgetType,
exportWidgetsBundle: exportWidgetsBundle,
@@ -275,6 +277,89 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
return true;
}
+ // Rule chain functions
+
+ function exportRuleChain(ruleChainId) {
+ ruleChainService.getRuleChain(ruleChainId).then(
+ (ruleChain) => {
+ ruleChainService.getRuleChainMetaData(ruleChainId).then(
+ (ruleChainMetaData) => {
+ var ruleChainExport = {
+ ruleChain: prepareRuleChain(ruleChain),
+ metadata: prepareRuleChainMetaData(ruleChainMetaData)
+ };
+ var name = ruleChain.name;
+ name = name.toLowerCase().replace(/\W/g,"_");
+ exportToPc(ruleChainExport, name + '.json');
+ },
+ (rejection) => {
+ processExportRuleChainRejection(rejection);
+ }
+ );
+ },
+ (rejection) => {
+ processExportRuleChainRejection(rejection);
+ }
+ );
+ }
+
+ function prepareRuleChain(ruleChain) {
+ ruleChain = prepareExport(ruleChain);
+ if (ruleChain.firstRuleNodeId) {
+ ruleChain.firstRuleNodeId = null;
+ }
+ ruleChain.root = false;
+ return ruleChain;
+ }
+
+ function prepareRuleChainMetaData(ruleChainMetaData) {
+ delete ruleChainMetaData.ruleChainId;
+ for (var i=0;i<ruleChainMetaData.nodes.length;i++) {
+ var node = ruleChainMetaData.nodes[i];
+ ruleChainMetaData.nodes[i] = prepareExport(node);
+ }
+ return ruleChainMetaData;
+ }
+
+ function processExportRuleChainRejection(rejection) {
+ var message = rejection;
+ if (!message) {
+ message = $translate.instant('error.unknown-error');
+ }
+ toast.showError($translate.instant('rulechain.export-failed-error', {error: message}));
+ }
+
+ function importRuleChain($event) {
+ var deferred = $q.defer();
+ openImportDialog($event, 'rulechain.import', 'rulechain.rulechain-file').then(
+ function success(ruleChainImport) {
+ if (!validateImportedRuleChain(ruleChainImport)) {
+ toast.showError($translate.instant('rulechain.invalid-rulechain-file-error'));
+ deferred.reject();
+ } else {
+ deferred.resolve(ruleChainImport);
+ }
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function validateImportedRuleChain(ruleChainImport) {
+ if (angular.isUndefined(ruleChainImport.ruleChain)) {
+ return false;
+ }
+ if (angular.isUndefined(ruleChainImport.metadata)) {
+ return false;
+ }
+ if (angular.isUndefined(ruleChainImport.ruleChain.name)) {
+ return false;
+ }
+ return true;
+ }
+
// Plugin functions
function exportPlugin(pluginId) {
ui/src/app/layout/index.js 10(+9 -1)
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
index e5ca958..6ec7ef7 100644
--- a/ui/src/app/layout/index.js
+++ b/ui/src/app/layout/index.js
@@ -29,6 +29,9 @@ import thingsboardNoAnimate from '../components/no-animate.directive';
import thingsboardOnFinishRender from '../components/finish-render.directive';
import thingsboardSideMenu from '../components/side-menu.directive';
import thingsboardDashboardAutocomplete from '../components/dashboard-autocomplete.directive';
+import thingsboardKvMap from '../components/kv-map.directive';
+import thingsboardJsonObjectEdit from '../components/json-object-edit.directive';
+import thingsboardJsonContent from '../components/json-content.directive';
import thingsboardUserMenu from './user-menu.directive';
@@ -49,6 +52,7 @@ import thingsboardWidgetLibrary from '../widget';
import thingsboardDashboard from '../dashboard';
import thingsboardPlugin from '../plugin';
import thingsboardRule from '../rule';
+import thingsboardRuleChain from '../rulechain';
import thingsboardJsonForm from '../jsonform';
@@ -81,6 +85,7 @@ export default angular.module('thingsboard.home', [
thingsboardDashboard,
thingsboardPlugin,
thingsboardRule,
+ thingsboardRuleChain,
thingsboardJsonForm,
thingsboardApiDevice,
thingsboardApiLogin,
@@ -88,7 +93,10 @@ export default angular.module('thingsboard.home', [
thingsboardNoAnimate,
thingsboardOnFinishRender,
thingsboardSideMenu,
- thingsboardDashboardAutocomplete
+ thingsboardDashboardAutocomplete,
+ thingsboardKvMap,
+ thingsboardJsonObjectEdit,
+ thingsboardJsonContent
])
.config(HomeRoutes)
.controller('HomeController', HomeController)
ui/src/app/locale/locale.constant.js 109(+109 -0)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 252884d..c515e3e 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -43,6 +43,7 @@ export default angular.module('thingsboard.locale', [])
"update": "Update",
"remove": "Remove",
"search": "Search",
+ "clear-search": "Clear search",
"assign": "Assign",
"unassign": "Unassign",
"share": "Share",
@@ -341,6 +342,11 @@ export default angular.module('thingsboard.locale', [])
"enter-password": "Enter password",
"enter-search": "Enter search"
},
+ "content-type": {
+ "json": "Json",
+ "text": "Text",
+ "binary": "Binary (Base64)"
+ },
"customer": {
"customer": "Customer",
"customers": "Customers",
@@ -745,6 +751,10 @@ export default angular.module('thingsboard.locale', [])
"type-alarms": "Alarms",
"list-of-alarms": "{ count, select, 1 {One alarms} other {List of # alarms} }",
"alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'",
+ "type-rulechain": "Rule chain",
+ "type-rulechains": "Rule chains",
+ "list-of-rulechains": "{ count, select, 1 {One rule chain} other {List of # rule chains} }",
+ "rulechain-name-starts-with": "Rule chains whose names start with '{{prefix}}'",
"type-current-customer": "Current Customer",
"search": "Search entities",
"selected-entities": "{ count, select, 1 {1 entity} other {# entities} } selected",
@@ -758,6 +768,8 @@ export default angular.module('thingsboard.locale', [])
"type-error": "Error",
"type-lc-event": "Lifecycle event",
"type-stats": "Statistics",
+ "type-debug-rule-node": "Debug",
+ "type-debug-rule-chain": "Debug",
"no-events-prompt": "No events found",
"error": "Error",
"alarm": "Alarm",
@@ -765,6 +777,13 @@ export default angular.module('thingsboard.locale', [])
"server": "Server",
"body": "Body",
"method": "Method",
+ "type": "Type",
+ "entity": "Entity",
+ "message-id": "Message Id",
+ "message-type": "Message Type",
+ "data-type": "Data Type",
+ "metadata": "Metadata",
+ "data": "Data",
"event": "Event",
"status": "Status",
"success": "Success",
@@ -943,6 +962,13 @@ export default angular.module('thingsboard.locale', [])
"no-return-error": "Function must return value!",
"return-type-mismatch": "Function must return value of '{{type}}' type!"
},
+ "key-val": {
+ "key": "Key",
+ "value": "Value",
+ "remove-entry": "Remove entry",
+ "add-entry": "Add entry",
+ "no-data": "No entries"
+ },
"layout": {
"layout": "Layout",
"manage": "Manage layouts",
@@ -1133,6 +1159,89 @@ export default angular.module('thingsboard.locale', [])
"no-rules-matching": "No rules matching '{{entity}}' were found.",
"rule-required": "Rule is required"
},
+ "rulechain": {
+ "rulechain": "Rule chain",
+ "rulechains": "Rule chains",
+ "delete": "Delete rule chain",
+ "name": "Name",
+ "name-required": "Name is required.",
+ "description": "Description",
+ "add": "Add Rule Chain",
+ "delete-rulechain-title": "Are you sure you want to delete the rule chain '{{ruleChainName}}'?",
+ "delete-rulechain-text": "Be careful, after the confirmation the rule chain and all related data will become unrecoverable.",
+ "delete-rulechains-title": "Are you sure you want to delete { count, select, 1 {1 rule chain} other {# rule chains} }?",
+ "delete-rulechains-action-title": "Delete { count, select, 1 {1 rule chain} other {# rule chains} }",
+ "delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.",
+ "add-rulechain-text": "Add new rule chain",
+ "no-rulechains-text": "No rule chains found",
+ "rulechain-details": "Rule chain details",
+ "details": "Details",
+ "events": "Events",
+ "system": "System",
+ "import": "Import rule chain",
+ "export": "Export rule chain",
+ "export-failed-error": "Unable to export rule chain: {{error}}",
+ "create-new-rulechain": "Create new rule chain",
+ "rulechain-file": "Rule chain file",
+ "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.",
+ "copyId": "Copy rule chain Id",
+ "idCopiedMessage": "Rule chain Id has been copied to clipboard",
+ "select-rulechain": "Select rule chain",
+ "no-rulechains-matching": "No rule chains matching '{{entity}}' were found.",
+ "rulechain-required": "Rule chain is required",
+ "management": "Rules management",
+ "debug-mode": "Debug mode"
+ },
+ "rulenode": {
+ "details": "Details",
+ "events": "Events",
+ "search": "Search nodes",
+ "open-node-library": "Open node library",
+ "add": "Add rule node",
+ "name": "Name",
+ "name-required": "Name is required.",
+ "type": "Type",
+ "description": "Description",
+ "delete": "Delete rule node",
+ "select-all-objects": "Select all nodes and connections",
+ "deselect-all-objects": "Deselect all nodes and connections",
+ "delete-selected-objects": "Delete selected nodes and connections",
+ "delete-selected": "Delete selected",
+ "select-all": "Select all",
+ "copy-selected": "Copy selected",
+ "deselect-all": "Deselect all",
+ "rulenode-details": "Rule node details",
+ "debug-mode": "Debug mode",
+ "configuration": "Configuration",
+ "link": "Link",
+ "link-details": "Rule node link details",
+ "add-link": "Add link",
+ "link-label": "Link label",
+ "link-label-required": "Link label is required.",
+ "custom-link-label": "Custom link label",
+ "custom-link-label-required": "Custom link label is required.",
+ "type-filter": "Filter",
+ "type-filter-details": "Filter incoming messages with configured conditions",
+ "type-enrichment": "Enrichment",
+ "type-enrichment-details": "Add additional information into Message Metadata",
+ "type-transformation": "Transformation",
+ "type-transformation-details": "Change Message payload and Metadata",
+ "type-action": "Action",
+ "type-action-details": "Perform special action",
+ "type-rule-chain": "Rule Chain",
+ "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain",
+ "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.",
+ "ui-resources-load-error": "Failed to load configuration ui resources.",
+ "invalid-target-rulechain": "Unable to resolve target rule chain!",
+ "test-script-function": "Test script function",
+ "message": "Message",
+ "message-type": "Message type",
+ "message-type-required": "Message type is required",
+ "metadata": "Metadata",
+ "metadata-required": "Metadata entries can't be empty.",
+ "output": "Output",
+ "test": "Test"
+ },
"rule-plugin": {
"management": "Rules and plugins management"
},
ui/src/app/rulechain/add-link.tpl.html 48(+48 -0)
diff --git a/ui/src/app/rulechain/add-link.tpl.html b/ui/src/app/rulechain/add-link.tpl.html
new file mode 100644
index 0000000..42c0777
--- /dev/null
+++ b/ui/src/app/rulechain/add-link.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'rulenode.add-link' | translate }}" tb-help="'rulechains'" help-container-id="help-container">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>rulenode.add-link</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <tb-rule-node-link link="vm.link" labels="vm.labels" is-edit="true" the-form="theForm"></tb-rule-node-link>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/rulechain/add-rulechain.tpl.html 48(+48 -0)
diff --git a/ui/src/app/rulechain/add-rulechain.tpl.html b/ui/src/app/rulechain/add-rulechain.tpl.html
new file mode 100644
index 0000000..44d0ec3
--- /dev/null
+++ b/ui/src/app/rulechain/add-rulechain.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'rulechain.add' | translate }}" tb-help="'rulechains'" help-container-id="help-container">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>rulechain.add</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <tb-rule-chain rule-chain="vm.item" is-edit="true" the-form="theForm"></tb-rule-chain>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/rulechain/add-rulenode.tpl.html 48(+48 -0)
diff --git a/ui/src/app/rulechain/add-rulenode.tpl.html b/ui/src/app/rulechain/add-rulenode.tpl.html
new file mode 100644
index 0000000..c36b43b
--- /dev/null
+++ b/ui/src/app/rulechain/add-rulenode.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'rulenode.add' | translate }}" tb-help="'rulechains'" help-container-id="help-container" style="min-width: 650px;">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>rulenode.add</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <tb-rule-node rule-node="vm.ruleNode" rule-chain-id="vm.ruleChainId" is-edit="true" the-form="theForm"></tb-rule-node>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/rulechain/index.js 41(+41 -0)
diff --git a/ui/src/app/rulechain/index.js b/ui/src/app/rulechain/index.js
new file mode 100644
index 0000000..7740dd0
--- /dev/null
+++ b/ui/src/app/rulechain/index.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import RuleChainRoutes from './rulechain.routes';
+import RuleChainsController from './rulechains.controller';
+import {RuleChainController, AddRuleNodeController, AddRuleNodeLinkController} from './rulechain.controller';
+import NodeScriptTestController from './script/node-script-test.controller';
+import RuleChainDirective from './rulechain.directive';
+import RuleNodeDefinedConfigDirective from './rulenode-defined-config.directive';
+import RuleNodeConfigDirective from './rulenode-config.directive';
+import RuleNodeDirective from './rulenode.directive';
+import LinkDirective from './link.directive';
+import NodeScriptTest from './script/node-script-test.service';
+
+export default angular.module('thingsboard.ruleChain', [])
+ .config(RuleChainRoutes)
+ .controller('RuleChainsController', RuleChainsController)
+ .controller('RuleChainController', RuleChainController)
+ .controller('AddRuleNodeController', AddRuleNodeController)
+ .controller('AddRuleNodeLinkController', AddRuleNodeLinkController)
+ .controller('NodeScriptTestController', NodeScriptTestController)
+ .directive('tbRuleChain', RuleChainDirective)
+ .directive('tbRuleNodeDefinedConfig', RuleNodeDefinedConfigDirective)
+ .directive('tbRuleNodeConfig', RuleNodeConfigDirective)
+ .directive('tbRuleNode', RuleNodeDirective)
+ .directive('tbRuleNodeLink', LinkDirective)
+ .factory('ruleNodeScriptTest', NodeScriptTest)
+ .name;
ui/src/app/rulechain/link.directive.js 71(+71 -0)
diff --git a/ui/src/app/rulechain/link.directive.js b/ui/src/app/rulechain/link.directive.js
new file mode 100644
index 0000000..b3565a3
--- /dev/null
+++ b/ui/src/app/rulechain/link.directive.js
@@ -0,0 +1,71 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import linkFieldsetTemplate from './link-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function LinkDirective($compile, $templateCache, $filter) {
+ var linker = function (scope, element) {
+ var template = $templateCache.get(linkFieldsetTemplate);
+ element.html(template);
+
+ scope.selectedLabel = null;
+
+ scope.$watch('link', function() {
+ scope.selectedLabel = null;
+ if (scope.link && scope.labels) {
+ if (scope.link.label) {
+ var result = $filter('filter')(scope.labels, {name: scope.link.label});
+ if (result && result.length) {
+ scope.selectedLabel = result[0];
+ } else {
+ result = $filter('filter')(scope.labels, {custom: true});
+ if (result && result.length && result[0].custom) {
+ scope.selectedLabel = result[0];
+ }
+ }
+ }
+ }
+ });
+
+ scope.selectedLabelChanged = function() {
+ if (scope.link && scope.selectedLabel) {
+ if (!scope.selectedLabel.custom) {
+ scope.link.label = scope.selectedLabel.name;
+ } else {
+ scope.link.label = "";
+ }
+ }
+ };
+
+ $compile(element.contents())(scope);
+ }
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ link: '=',
+ labels: '=',
+ isEdit: '=',
+ isReadOnly: '=',
+ theForm: '='
+ }
+ };
+}
ui/src/app/rulechain/link-fieldset.tpl.html 39(+39 -0)
diff --git a/ui/src/app/rulechain/link-fieldset.tpl.html b/ui/src/app/rulechain/link-fieldset.tpl.html
new file mode 100644
index 0000000..13ec6c3
--- /dev/null
+++ b/ui/src/app/rulechain/link-fieldset.tpl.html
@@ -0,0 +1,39 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-content class="md-padding tb-link" layout="column">
+ <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
+ <md-input-container class="md-block">
+ <label translate>rulenode.link-label</label>
+ <md-select ng-model="selectedLabel" ng-change="selectedLabelChanged()">
+ <md-option ng-repeat="label in labels" ng-value="label">
+ {{label.name}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm.linkLabel.$error">
+ <div translate ng-message="required">rulenode.link-label-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="selectedLabel.custom" class="md-block">
+ <label translate>rulenode.link-label</label>
+ <input required name="customLinkLabel" ng-model="link.label">
+ <div ng-messages="theForm.customLinkLabel.$error">
+ <div translate ng-message="required">rulenode.custom-link-label-required</div>
+ </div>
+ </md-input-container>
+ </fieldset>
+</md-content>
ui/src/app/rulechain/rulechain.controller.js 1287(+1287 -0)
diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js
new file mode 100644
index 0000000..7358b44
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.controller.js
@@ -0,0 +1,1287 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './rulechain.scss';
+
+import 'tooltipster/dist/css/tooltipster.bundle.min.css';
+import 'tooltipster/dist/js/tooltipster.bundle.min.js';
+import 'tooltipster/dist/css/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-shadow.min.css';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import addRuleNodeTemplate from './add-rulenode.tpl.html';
+import addRuleNodeLinkTemplate from './add-link.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog,
+ $filter, $translate, hotkeys, types, ruleChainService, itembuffer, Modelfactory, flowchartConstants,
+ ruleChain, ruleChainMetaData, ruleNodeComponents) {
+
+ var vm = this;
+
+ vm.$mdExpansionPanel = $mdExpansionPanel;
+ vm.types = types;
+
+ if ($state.current.data.import && !ruleChain) {
+ $state.go('home.ruleChains');
+ return;
+ }
+
+ vm.isImport = $state.current.data.import;
+ vm.isConfirmOnExit = false;
+
+ $scope.$watch(function() {
+ return vm.isDirty || vm.isImport;
+ }, (val) => {
+ vm.isConfirmOnExit = val;
+ });
+
+ vm.errorTooltips = {};
+
+ vm.isFullscreen = false;
+
+ vm.editingRuleNode = null;
+ vm.isEditingRuleNode = false;
+
+ vm.editingRuleNodeLink = null;
+ vm.isEditingRuleNodeLink = false;
+
+ vm.isLibraryOpen = true;
+ vm.enableHotKeys = true;
+
+ Object.defineProperty(vm, 'isLibraryOpenReadonly', {
+ get: function() { return vm.isLibraryOpen },
+ set: function() {}
+ });
+
+ vm.ruleNodeSearch = '';
+
+ vm.ruleChain = ruleChain;
+ vm.ruleChainMetaData = ruleChainMetaData;
+
+ vm.canvasControl = {};
+
+ vm.ruleChainModel = {
+ nodes: [],
+ edges: []
+ };
+
+ vm.ruleNodeTypesModel = {};
+ vm.ruleNodeTypesCanvasControl = {};
+ vm.ruleChainLibraryLoaded = false;
+ for (var type in types.ruleNodeType) {
+ if (!types.ruleNodeType[type].special) {
+ vm.ruleNodeTypesModel[type] = {
+ model: {
+ nodes: [],
+ edges: []
+ },
+ selectedObjects: []
+ };
+ vm.ruleNodeTypesCanvasControl[type] = {};
+ }
+ }
+
+
+
+ vm.selectedObjects = [];
+
+ vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects);
+
+ vm.saveRuleChain = saveRuleChain;
+ vm.revertRuleChain = revertRuleChain;
+
+ vm.objectsSelected = objectsSelected;
+ vm.deleteSelected = deleteSelected;
+
+ vm.triggerResize = triggerResize;
+
+ vm.openRuleChainContextMenu = openRuleChainContextMenu;
+
+ initHotKeys();
+
+ function openRuleChainContextMenu($event, $mdOpenMousepointMenu) {
+ if (vm.canvasControl.modelservice && !$event.ctrlKey && !$event.metaKey) {
+ var x = $event.clientX;
+ var y = $event.clientY;
+ var item = vm.canvasControl.modelservice.getItemInfoAtPoint(x, y);
+ vm.contextInfo = prepareContextMenu(item);
+ if (vm.contextInfo.items && vm.contextInfo.items.length > 0) {
+ vm.contextMenuEvent = $event;
+ $mdOpenMousepointMenu($event);
+ return false;
+ }
+ }
+ }
+
+ function prepareContextMenu(item) {
+ if (objectsSelected() || (!item.node && !item.edge)) {
+ return prepareRuleChainContextMenu();
+ } else if (item.node) {
+ return prepareRuleNodeContextMenu(item.node);
+ } else if (item.edge) {
+ return prepareEdgeContextMenu(item.edge);
+ }
+ }
+
+ function prepareRuleChainContextMenu() {
+ var contextInfo = {
+ headerClass: 'tb-rulechain',
+ icon: 'settings_ethernet',
+ title: vm.ruleChain.name,
+ subtitle: $translate.instant('rulechain.rulechain')
+ };
+ contextInfo.items = [];
+ if (vm.modelservice.nodes.getSelectedNodes().length) {
+ contextInfo.items.push(
+ {
+ action: function () {
+ copyRuleNodes();
+ },
+ enabled: true,
+ value: "rulenode.copy-selected",
+ icon: "content_copy",
+ shortcut: "M-C"
+ }
+ );
+ }
+ contextInfo.items.push(
+ {
+ action: function ($event) {
+ pasteRuleNodes($event);
+ },
+ enabled: itembuffer.hasRuleNodes(),
+ value: "action.paste",
+ icon: "content_paste",
+ shortcut: "M-V"
+ }
+ );
+ contextInfo.items.push(
+ {
+ divider: true
+ }
+ );
+ if (objectsSelected()) {
+ contextInfo.items.push(
+ {
+ action: function () {
+ vm.modelservice.deselectAll();
+ },
+ enabled: true,
+ value: "rulenode.deselect-all",
+ icon: "tab_unselected",
+ shortcut: "Esc"
+ }
+ );
+ contextInfo.items.push(
+ {
+ action: function () {
+ vm.modelservice.deleteSelected();
+ },
+ enabled: true,
+ value: "rulenode.delete-selected",
+ icon: "clear",
+ shortcut: "Del"
+ }
+ );
+ } else {
+ contextInfo.items.push(
+ {
+ action: function () {
+ vm.modelservice.selectAll();
+ },
+ enabled: true,
+ value: "rulenode.select-all",
+ icon: "select_all",
+ shortcut: "M-A"
+ }
+ );
+ }
+ contextInfo.items.push(
+ {
+ divider: true
+ }
+ );
+ contextInfo.items.push(
+ {
+ action: function () {
+ vm.saveRuleChain();
+ },
+ enabled: !(vm.isInvalid || (!vm.isDirty && !vm.isImport)),
+ value: "action.apply-changes",
+ icon: "done",
+ shortcut: "M-S"
+ }
+ );
+ contextInfo.items.push(
+ {
+ action: function () {
+ vm.revertRuleChain();
+ },
+ enabled: vm.isDirty,
+ value: "action.decline-changes",
+ icon: "close",
+ shortcut: "M-Z"
+ }
+ );
+ return contextInfo;
+ }
+
+ function prepareRuleNodeContextMenu(node) {
+ var contextInfo = {
+ headerClass: node.nodeClass,
+ icon: node.icon,
+ title: node.name,
+ subtitle: node.component.name
+ };
+ contextInfo.items = [];
+ if (!node.readonly) {
+ contextInfo.items.push(
+ {
+ action: function () {
+ openNodeDetails(node);
+ },
+ enabled: true,
+ value: "rulenode.details",
+ icon: "menu"
+ }
+ );
+ contextInfo.items.push(
+ {
+ action: function () {
+ copyNode(node);
+ },
+ enabled: true,
+ value: "action.copy",
+ icon: "content_copy"
+ }
+ );
+ contextInfo.items.push(
+ {
+ action: function () {
+ vm.canvasControl.modelservice.nodes.delete(node);
+ },
+ enabled: true,
+ value: "action.delete",
+ icon: "clear",
+ shortcut: "M-X"
+ }
+ );
+ }
+ return contextInfo;
+ }
+
+ function prepareEdgeContextMenu(edge) {
+ var contextInfo = {
+ headerClass: 'tb-link',
+ icon: 'trending_flat',
+ title: edge.label,
+ subtitle: $translate.instant('rulenode.link')
+ };
+ contextInfo.items = [];
+ var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+ if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
+ contextInfo.items.push(
+ {
+ action: function () {
+ openLinkDetails(edge);
+ },
+ enabled: true,
+ value: "rulenode.details",
+ icon: "menu"
+ }
+ );
+ }
+ contextInfo.items.push(
+ {
+ action: function () {
+ vm.canvasControl.modelservice.edges.delete(edge);
+ },
+ enabled: true,
+ value: "action.delete",
+ icon: "clear",
+ shortcut: "M-X"
+ }
+ );
+ return contextInfo;
+ }
+
+ function initHotKeys() {
+ hotkeys.bindTo($scope)
+ .add({
+ combo: 'ctrl+a',
+ description: $translate.instant('rulenode.select-all-objects'),
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ if (vm.enableHotKeys) {
+ event.preventDefault();
+ vm.modelservice.selectAll();
+ }
+ }
+ })
+ .add({
+ combo: 'ctrl+c',
+ description: $translate.instant('rulenode.copy-selected'),
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ if (vm.enableHotKeys) {
+ event.preventDefault();
+ copyRuleNodes();
+ }
+ }
+ })
+ .add({
+ combo: 'ctrl+v',
+ description: $translate.instant('action.paste'),
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ if (vm.enableHotKeys) {
+ event.preventDefault();
+ if (itembuffer.hasRuleNodes()) {
+ pasteRuleNodes();
+ }
+ }
+ }
+ })
+ .add({
+ combo: 'esc',
+ description: $translate.instant('rulenode.deselect-all-objects'),
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ if (vm.enableHotKeys) {
+ event.preventDefault();
+ event.stopPropagation();
+ vm.modelservice.deselectAll();
+ }
+ }
+ })
+ .add({
+ combo: 'ctrl+s',
+ description: $translate.instant('action.apply'),
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ if (vm.enableHotKeys) {
+ event.preventDefault();
+ vm.saveRuleChain();
+ }
+ }
+ })
+ .add({
+ combo: 'ctrl+z',
+ description: $translate.instant('action.decline-changes'),
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ if (vm.enableHotKeys) {
+ event.preventDefault();
+ vm.revertRuleChain();
+ }
+ }
+ })
+ .add({
+ combo: 'del',
+ description: $translate.instant('rulenode.delete-selected-objects'),
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ if (vm.enableHotKeys) {
+ event.preventDefault();
+ vm.modelservice.deleteSelected();
+ }
+ }
+ })
+ }
+
+ vm.onEditRuleNodeClosed = function() {
+ vm.editingRuleNode = null;
+ };
+
+ vm.onEditRuleNodeLinkClosed = function() {
+ vm.editingRuleNodeLink = null;
+ };
+
+ vm.saveRuleNode = function(theForm) {
+ $scope.$broadcast('form-submit');
+ if (theForm.$valid) {
+ theForm.$setPristine();
+ if (vm.editingRuleNode.error) {
+ delete vm.editingRuleNode.error;
+ }
+ vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
+ vm.editingRuleNode = angular.copy(vm.editingRuleNode);
+ updateRuleNodesHighlight();
+ }
+ };
+
+ vm.saveRuleNodeLink = function(theForm) {
+ theForm.$setPristine();
+ vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex] = vm.editingRuleNodeLink;
+ vm.editingRuleNodeLink = angular.copy(vm.editingRuleNodeLink);
+ };
+
+ vm.onRevertRuleNodeEdit = function(theForm) {
+ theForm.$setPristine();
+ var node = vm.ruleChainModel.nodes[vm.editingRuleNodeIndex];
+ vm.editingRuleNode = angular.copy(node);
+ };
+
+ vm.onRevertRuleNodeLinkEdit = function(theForm) {
+ theForm.$setPristine();
+ var edge = vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex];
+ vm.editingRuleNodeLink = angular.copy(edge);
+ };
+
+ vm.nodeLibCallbacks = {
+ nodeCallbacks: {
+ 'mouseEnter': function (event, node) {
+ displayNodeDescriptionTooltip(event, node);
+ },
+ 'mouseLeave': function () {
+ destroyTooltips();
+ },
+ 'mouseDown': function () {
+ destroyTooltips();
+ }
+ }
+ };
+
+ vm.typeHeaderMouseEnter = function(event, typeId) {
+ var ruleNodeType = types.ruleNodeType[typeId];
+ displayTooltip(event,
+ '<div class="tb-rule-node-tooltip">' +
+ '<div id="tooltip-content" layout="column">' +
+ '<div class="tb-node-title">' + $translate.instant(ruleNodeType.name) + '</div>' +
+ '<div class="tb-node-details">' + $translate.instant(ruleNodeType.details) + '</div>' +
+ '</div>' +
+ '</div>'
+ );
+ };
+
+ vm.destroyTooltips = destroyTooltips;
+
+ function destroyTooltips() {
+ if (vm.tooltipTimeout) {
+ $timeout.cancel(vm.tooltipTimeout);
+ vm.tooltipTimeout = null;
+ }
+ var instances = angular.element.tooltipster.instances();
+ instances.forEach((instance) => {
+ if (!instance.isErrorTooltip) {
+ instance.destroy();
+ }
+ });
+ }
+
+ function displayNodeDescriptionTooltip(event, node) {
+ displayTooltip(event,
+ '<div class="tb-rule-node-tooltip">' +
+ '<div id="tooltip-content" layout="column">' +
+ '<div class="tb-node-title">' + node.component.name + '</div>' +
+ '<div class="tb-node-description">' + node.component.configurationDescriptor.nodeDefinition.description + '</div>' +
+ '<div class="tb-node-details">' + node.component.configurationDescriptor.nodeDefinition.details + '</div>' +
+ '</div>' +
+ '</div>'
+ );
+ }
+
+ function displayTooltip(event, content) {
+ destroyTooltips();
+ vm.tooltipTimeout = $timeout(() => {
+ var element = angular.element(event.target);
+ element.tooltipster(
+ {
+ theme: 'tooltipster-shadow',
+ delay: 100,
+ trigger: 'custom',
+ triggerOpen: {
+ click: false,
+ tap: false
+ },
+ triggerClose: {
+ click: true,
+ tap: true,
+ scroll: true
+ },
+ side: 'right',
+ trackOrigin: true
+ }
+ );
+ var contentElement = angular.element(content);
+ $compile(contentElement)($scope);
+ var tooltip = element.tooltipster('instance');
+ tooltip.content(contentElement);
+ tooltip.open();
+ }, 500);
+ }
+
+ function updateNodeErrorTooltip(node) {
+ if (node.error) {
+ var element = angular.element('#' + node.id);
+ var tooltip = vm.errorTooltips[node.id];
+ if (!tooltip || !element.hasClass("tooltipstered")) {
+ element.tooltipster(
+ {
+ theme: 'tooltipster-shadow',
+ delay: 0,
+ animationDuration: 0,
+ trigger: 'custom',
+ triggerOpen: {
+ click: false,
+ tap: false
+ },
+ triggerClose: {
+ click: false,
+ tap: false,
+ scroll: false
+ },
+ side: 'top',
+ trackOrigin: true
+ }
+ );
+ var content = '<div class="tb-rule-node-error-tooltip">' +
+ '<div id="tooltip-content" layout="column">' +
+ '<div class="tb-node-details">' + node.error + '</div>' +
+ '</div>' +
+ '</div>';
+ var contentElement = angular.element(content);
+ $compile(contentElement)($scope);
+ tooltip = element.tooltipster('instance');
+ tooltip.isErrorTooltip = true;
+ tooltip.content(contentElement);
+ vm.errorTooltips[node.id] = tooltip;
+ }
+ $mdUtil.nextTick(() => {
+ tooltip.open();
+ });
+ } else {
+ if (vm.errorTooltips[node.id]) {
+ tooltip = vm.errorTooltips[node.id];
+ tooltip.destroy();
+ delete vm.errorTooltips[node.id];
+ }
+ }
+ }
+
+ function updateErrorTooltips(hide) {
+ for (var nodeId in vm.errorTooltips) {
+ var tooltip = vm.errorTooltips[nodeId];
+ if (hide) {
+ tooltip.close();
+ } else {
+ tooltip.open();
+ }
+ }
+ }
+
+ $scope.$watch(function() {
+ return vm.isEditingRuleNode || vm.isEditingRuleNodeLink;
+ }, (val) => {
+ vm.enableHotKeys = !val;
+ updateErrorTooltips(val);
+ });
+
+ vm.editCallbacks = {
+ edgeDoubleClick: function (event, edge) {
+ openLinkDetails(edge);
+ },
+ nodeCallbacks: {
+ 'doubleClick': function (event, node) {
+ openNodeDetails(node);
+ }
+ },
+ isValidEdge: function (source, destination) {
+ return source.type === flowchartConstants.rightConnectorType && destination.type === flowchartConstants.leftConnectorType;
+ },
+ createEdge: function (event, edge) {
+ var deferred = $q.defer();
+ var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+ if (sourceNode.component.type == types.ruleNodeType.INPUT.value) {
+ var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
+ if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
+ deferred.reject();
+ } else {
+ var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId}, true);
+ if (res && res.length) {
+ vm.modelservice.edges.delete(res[0]);
+ }
+ deferred.resolve(edge);
+ }
+ } else {
+ var labels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
+ vm.enableHotKeys = false;
+ addRuleNodeLink(event, edge, labels).then(
+ (link) => {
+ deferred.resolve(link);
+ vm.enableHotKeys = true;
+ },
+ () => {
+ deferred.reject();
+ vm.enableHotKeys = true;
+ }
+ );
+ }
+ return deferred.promise;
+ },
+ dropNode: function (event, node) {
+ addRuleNode(event, node);
+ }
+ };
+
+ function openNodeDetails(node) {
+ if (node.component.type != types.ruleNodeType.INPUT.value) {
+ vm.isEditingRuleNodeLink = false;
+ vm.editingRuleNodeLink = null;
+ vm.isEditingRuleNode = true;
+ vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node);
+ vm.editingRuleNode = angular.copy(node);
+ $mdUtil.nextTick(() => {
+ if (vm.ruleNodeForm) {
+ vm.ruleNodeForm.$setPristine();
+ }
+ });
+ }
+ }
+
+ function openLinkDetails(edge) {
+ var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+ if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
+ vm.isEditingRuleNode = false;
+ vm.editingRuleNode = null;
+ vm.editingRuleNodeLinkLabels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
+ vm.isEditingRuleNodeLink = true;
+ vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
+ vm.editingRuleNodeLink = angular.copy(edge);
+ $mdUtil.nextTick(() => {
+ if (vm.ruleNodeLinkForm) {
+ vm.ruleNodeLinkForm.$setPristine();
+ }
+ });
+ }
+ }
+
+ function copyNode(node) {
+ itembuffer.copyRuleNodes([node], []);
+ }
+
+ function copyRuleNodes() {
+ var nodes = vm.modelservice.nodes.getSelectedNodes();
+ var edges = vm.modelservice.edges.getSelectedEdges();
+ var connections = [];
+ for (var i=0;i<edges.length;i++) {
+ var edge = edges[i];
+ var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+ var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
+ var isInputSource = sourceNode.component.type == types.ruleNodeType.INPUT.value;
+ var fromIndex = nodes.indexOf(sourceNode);
+ var toIndex = nodes.indexOf(destNode);
+ if ( (isInputSource || fromIndex > -1) && toIndex > -1 ) {
+ var connection = {
+ isInputSource: isInputSource,
+ fromIndex: fromIndex,
+ toIndex: toIndex,
+ label: edge.label
+ };
+ connections.push(connection);
+ }
+ }
+ itembuffer.copyRuleNodes(nodes, connections);
+ }
+
+ function pasteRuleNodes(event) {
+ var canvas = angular.element(vm.canvasControl.modelservice.getCanvasHtmlElement());
+ var x,y;
+ if (event) {
+ var offset = canvas.offset();
+ x = Math.round(event.clientX - offset.left);
+ y = Math.round(event.clientY - offset.top);
+ } else {
+ var scrollParent = canvas.parent();
+ var scrollTop = scrollParent.scrollTop();
+ var scrollLeft = scrollParent.scrollLeft();
+ x = scrollLeft + scrollParent.width()/2;
+ y = scrollTop + scrollParent.height()/2;
+ }
+ var ruleNodes = itembuffer.pasteRuleNodes(x, y, event);
+ if (ruleNodes) {
+ vm.modelservice.deselectAll();
+ var nodes = [];
+ for (var i=0;i<ruleNodes.nodes.length;i++) {
+ var node = ruleNodes.nodes[i];
+ node.id = 'rule-chain-node-' + vm.nextNodeID++;
+ var component = node.component;
+ if (component.configurationDescriptor.nodeDefinition.inEnabled) {
+ node.connectors.push(
+ {
+ type: flowchartConstants.leftConnectorType,
+ id: vm.nextConnectorID++
+ }
+ );
+ }
+ if (component.configurationDescriptor.nodeDefinition.outEnabled) {
+ node.connectors.push(
+ {
+ type: flowchartConstants.rightConnectorType,
+ id: vm.nextConnectorID++
+ }
+ );
+ }
+ nodes.push(node);
+ vm.ruleChainModel.nodes.push(node);
+ vm.modelservice.nodes.select(node);
+ }
+ for (i=0;i<ruleNodes.connections.length;i++) {
+ var connection = ruleNodes.connections[i];
+ var sourceNode = nodes[connection.fromIndex];
+ var destNode = nodes[connection.toIndex];
+ if ( (connection.isInputSource || sourceNode) && destNode ) {
+ var source, destination;
+ if (connection.isInputSource) {
+ source = vm.inputConnectorId;
+ } else {
+ var sourceConnectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
+ if (sourceConnectors && sourceConnectors.length) {
+ source = sourceConnectors[0].id;
+ }
+ }
+ var destConnectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
+ if (destConnectors && destConnectors.length) {
+ destination = destConnectors[0].id;
+ }
+ if (source && destination) {
+ var edge = {
+ source: source,
+ destination: destination,
+ label: connection.label
+ };
+ vm.ruleChainModel.edges.push(edge);
+ vm.modelservice.edges.select(edge);
+ }
+ }
+ }
+
+ if (vm.canvasControl.adjustCanvasSize) {
+ vm.canvasControl.adjustCanvasSize();
+ }
+
+ updateRuleNodesHighlight();
+
+ validate();
+ }
+ }
+
+ loadRuleChainLibrary(ruleNodeComponents, true);
+
+ $scope.$watch('vm.ruleNodeSearch',
+ function (newVal, oldVal) {
+ if (!angular.equals(newVal, oldVal)) {
+ var res = $filter('filter')(ruleNodeComponents, {name: vm.ruleNodeSearch});
+ loadRuleChainLibrary(res);
+ }
+ }
+ );
+
+ $scope.$on('searchTextUpdated', function () {
+ updateRuleNodesHighlight();
+ });
+
+ function loadRuleChainLibrary(ruleNodeComponents, loadRuleChain) {
+ for (var componentType in vm.ruleNodeTypesModel) {
+ vm.ruleNodeTypesModel[componentType].model.nodes.length = 0;
+ }
+ for (var i=0;i<ruleNodeComponents.length;i++) {
+ var ruleNodeComponent = ruleNodeComponents[i];
+ componentType = ruleNodeComponent.type;
+ var model = vm.ruleNodeTypesModel[componentType].model;
+ var node = {
+ id: 'node-lib-' + componentType + '-' + model.nodes.length,
+ component: ruleNodeComponent,
+ name: '',
+ nodeClass: vm.types.ruleNodeType[componentType].nodeClass,
+ icon: vm.types.ruleNodeType[componentType].icon,
+ x: 30,
+ y: 10+50*model.nodes.length,
+ connectors: []
+ };
+ if (ruleNodeComponent.configurationDescriptor.nodeDefinition.inEnabled) {
+ node.connectors.push(
+ {
+ type: flowchartConstants.leftConnectorType,
+ id: model.nodes.length * 2
+ }
+ );
+ }
+ if (ruleNodeComponent.configurationDescriptor.nodeDefinition.outEnabled) {
+ node.connectors.push(
+ {
+ type: flowchartConstants.rightConnectorType,
+ id: model.nodes.length * 2 + 1
+ }
+ );
+ }
+ model.nodes.push(node);
+ }
+ vm.ruleChainLibraryLoaded = true;
+ if (loadRuleChain) {
+ prepareRuleChain();
+ }
+ $mdUtil.nextTick(() => {
+ for (componentType in vm.ruleNodeTypesCanvasControl) {
+ if (vm.ruleNodeTypesCanvasControl[componentType].adjustCanvasSize) {
+ vm.ruleNodeTypesCanvasControl[componentType].adjustCanvasSize(true);
+ }
+ }
+ for (componentType in vm.ruleNodeTypesModel) {
+ var panel = vm.$mdExpansionPanel(componentType);
+ if (panel) {
+ if (!vm.ruleNodeTypesModel[componentType].model.nodes.length) {
+ panel.collapse();
+ } else {
+ panel.expand();
+ }
+ }
+ }
+ });
+ }
+
+ function prepareRuleChain() {
+
+ if (vm.ruleChainWatch) {
+ vm.ruleChainWatch();
+ vm.ruleChainWatch = null;
+ }
+
+ vm.nextNodeID = 1;
+ vm.nextConnectorID = 1;
+
+ vm.selectedObjects.length = 0;
+ vm.ruleChainModel.nodes.length = 0;
+ vm.ruleChainModel.edges.length = 0;
+
+ vm.inputConnectorId = vm.nextConnectorID++;
+
+ vm.ruleChainModel.nodes.push(
+ {
+ id: 'rule-chain-node-' + vm.nextNodeID++,
+ component: types.inputNodeComponent,
+ name: "",
+ nodeClass: types.ruleNodeType.INPUT.nodeClass,
+ icon: types.ruleNodeType.INPUT.icon,
+ readonly: true,
+ x: 50,
+ y: 150,
+ connectors: [
+ {
+ type: flowchartConstants.rightConnectorType,
+ id: vm.inputConnectorId
+ },
+ ]
+
+ }
+ );
+ ruleChainService.resolveTargetRuleChains(vm.ruleChainMetaData.ruleChainConnections)
+ .then((ruleChainsMap) => {
+ createRuleChainModel(ruleChainsMap);
+ }
+ );
+ }
+
+ function createRuleChainModel(ruleChainsMap) {
+ var nodes = [];
+ for (var i=0;i<vm.ruleChainMetaData.nodes.length;i++) {
+ var ruleNode = vm.ruleChainMetaData.nodes[i];
+ var component = ruleChainService.getRuleNodeComponentByClazz(ruleNode.type);
+ if (component) {
+ var node = {
+ id: 'rule-chain-node-' + vm.nextNodeID++,
+ ruleNodeId: ruleNode.id,
+ additionalInfo: ruleNode.additionalInfo,
+ configuration: ruleNode.configuration,
+ debugMode: ruleNode.debugMode,
+ x: ruleNode.additionalInfo.layoutX,
+ y: ruleNode.additionalInfo.layoutY,
+ component: component,
+ name: ruleNode.name,
+ nodeClass: vm.types.ruleNodeType[component.type].nodeClass,
+ icon: vm.types.ruleNodeType[component.type].icon,
+ connectors: []
+ };
+ if (component.configurationDescriptor.nodeDefinition.inEnabled) {
+ node.connectors.push(
+ {
+ type: flowchartConstants.leftConnectorType,
+ id: vm.nextConnectorID++
+ }
+ );
+ }
+ if (component.configurationDescriptor.nodeDefinition.outEnabled) {
+ node.connectors.push(
+ {
+ type: flowchartConstants.rightConnectorType,
+ id: vm.nextConnectorID++
+ }
+ );
+ }
+ nodes.push(node);
+ vm.ruleChainModel.nodes.push(node);
+ }
+ }
+
+ if (vm.ruleChainMetaData.firstNodeIndex > -1) {
+ var destNode = nodes[vm.ruleChainMetaData.firstNodeIndex];
+ if (destNode) {
+ var connectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
+ if (connectors && connectors.length) {
+ var edge = {
+ source: vm.inputConnectorId,
+ destination: connectors[0].id
+ };
+ vm.ruleChainModel.edges.push(edge);
+ }
+ }
+ }
+
+ if (vm.ruleChainMetaData.connections) {
+ for (i = 0; i < vm.ruleChainMetaData.connections.length; i++) {
+ var connection = vm.ruleChainMetaData.connections[i];
+ var sourceNode = nodes[connection.fromIndex];
+ destNode = nodes[connection.toIndex];
+ if (sourceNode && destNode) {
+ var sourceConnectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
+ var destConnectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
+ if (sourceConnectors && sourceConnectors.length && destConnectors && destConnectors.length) {
+ edge = {
+ source: sourceConnectors[0].id,
+ destination: destConnectors[0].id,
+ label: connection.type
+ };
+ vm.ruleChainModel.edges.push(edge);
+ }
+ }
+ }
+ }
+
+ if (vm.ruleChainMetaData.ruleChainConnections) {
+ var ruleChainNodesMap = {};
+ for (i = 0; i < vm.ruleChainMetaData.ruleChainConnections.length; i++) {
+ var ruleChainConnection = vm.ruleChainMetaData.ruleChainConnections[i];
+ var ruleChain = ruleChainsMap[ruleChainConnection.targetRuleChainId.id];
+ if (ruleChainConnection.additionalInfo && ruleChainConnection.additionalInfo.ruleChainNodeId) {
+ var ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId];
+ if (!ruleChainNode) {
+ ruleChainNode = {
+ id: 'rule-chain-node-' + vm.nextNodeID++,
+ additionalInfo: ruleChainConnection.additionalInfo,
+ x: ruleChainConnection.additionalInfo.layoutX,
+ y: ruleChainConnection.additionalInfo.layoutY,
+ component: types.ruleChainNodeComponent,
+ nodeClass: vm.types.ruleNodeType.RULE_CHAIN.nodeClass,
+ icon: vm.types.ruleNodeType.RULE_CHAIN.icon,
+ connectors: [
+ {
+ type: flowchartConstants.leftConnectorType,
+ id: vm.nextConnectorID++
+ }
+ ]
+ };
+ if (ruleChain.name) {
+ ruleChainNode.name = ruleChain.name;
+ ruleChainNode.targetRuleChainId = ruleChainConnection.targetRuleChainId.id;
+ } else {
+ ruleChainNode.name = "Unresolved";
+ ruleChainNode.targetRuleChainId = null;
+ ruleChainNode.error = $translate.instant('rulenode.invalid-target-rulechain');
+ }
+ ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode;
+ vm.ruleChainModel.nodes.push(ruleChainNode);
+ }
+ sourceNode = nodes[ruleChainConnection.fromIndex];
+ if (sourceNode) {
+ connectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
+ if (connectors && connectors.length) {
+ var ruleChainEdge = {
+ source: connectors[0].id,
+ destination: ruleChainNode.connectors[0].id,
+ label: ruleChainConnection.type
+ };
+ vm.ruleChainModel.edges.push(ruleChainEdge);
+ }
+ }
+ }
+ }
+ }
+
+ if (vm.canvasControl.adjustCanvasSize) {
+ vm.canvasControl.adjustCanvasSize(true);
+ }
+
+ vm.isDirty = false;
+
+ updateRuleNodesHighlight();
+
+ validate();
+
+ $mdUtil.nextTick(() => {
+ vm.ruleChainWatch = $scope.$watch('vm.ruleChainModel',
+ function (newVal, oldVal) {
+ if (!angular.equals(newVal, oldVal)) {
+ validate();
+ if (!vm.isDirty) {
+ vm.isDirty = true;
+ }
+ }
+ }, true
+ );
+ });
+ }
+
+ function updateRuleNodesHighlight() {
+ for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) {
+ vm.ruleChainModel.nodes[i].highlighted = false;
+ }
+ if ($scope.searchConfig.searchText) {
+ var res = $filter('filter')(vm.ruleChainModel.nodes, {name: $scope.searchConfig.searchText});
+ if (res) {
+ for (i = 0; i < res.length; i++) {
+ res[i].highlighted = true;
+ }
+ }
+ }
+ }
+
+ function validate() {
+ $mdUtil.nextTick(() => {
+ vm.isInvalid = false;
+ for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) {
+ if (vm.ruleChainModel.nodes[i].error) {
+ vm.isInvalid = true;
+ }
+ updateNodeErrorTooltip(vm.ruleChainModel.nodes[i]);
+ }
+ });
+ }
+
+ function saveRuleChain() {
+ var saveRuleChainPromise;
+ if (vm.isImport) {
+ saveRuleChainPromise = ruleChainService.saveRuleChain(vm.ruleChain);
+ } else {
+ saveRuleChainPromise = $q.when(vm.ruleChain);
+ }
+ saveRuleChainPromise.then(
+ (ruleChain) => {
+ vm.ruleChain = ruleChain;
+ var ruleChainMetaData = {
+ ruleChainId: vm.ruleChain.id,
+ nodes: [],
+ connections: [],
+ ruleChainConnections: []
+ };
+
+ var nodes = [];
+
+ for (var i=0;i<vm.ruleChainModel.nodes.length;i++) {
+ var node = vm.ruleChainModel.nodes[i];
+ if (node.component.type != types.ruleNodeType.INPUT.value && node.component.type != types.ruleNodeType.RULE_CHAIN.value) {
+ var ruleNode = {};
+ if (node.ruleNodeId) {
+ ruleNode.id = node.ruleNodeId;
+ }
+ ruleNode.type = node.component.clazz;
+ ruleNode.name = node.name;
+ ruleNode.configuration = node.configuration;
+ ruleNode.additionalInfo = node.additionalInfo;
+ ruleNode.debugMode = node.debugMode;
+ if (!ruleNode.additionalInfo) {
+ ruleNode.additionalInfo = {};
+ }
+ ruleNode.additionalInfo.layoutX = node.x;
+ ruleNode.additionalInfo.layoutY = node.y;
+ ruleChainMetaData.nodes.push(ruleNode);
+ nodes.push(node);
+ }
+ }
+ var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId}, true);
+ if (res && res.length) {
+ var firstNodeEdge = res[0];
+ var firstNode = vm.modelservice.nodes.getNodeByConnectorId(firstNodeEdge.destination);
+ ruleChainMetaData.firstNodeIndex = nodes.indexOf(firstNode);
+ }
+ for (i=0;i<vm.ruleChainModel.edges.length;i++) {
+ var edge = vm.ruleChainModel.edges[i];
+ var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+ var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
+ if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
+ var fromIndex = nodes.indexOf(sourceNode);
+ if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
+ var ruleChainConnection = {
+ fromIndex: fromIndex,
+ targetRuleChainId: {entityType: vm.types.entityType.rulechain, id: destNode.targetRuleChainId},
+ additionalInfo: destNode.additionalInfo,
+ type: edge.label
+ };
+ if (!ruleChainConnection.additionalInfo) {
+ ruleChainConnection.additionalInfo = {};
+ }
+ ruleChainConnection.additionalInfo.layoutX = destNode.x;
+ ruleChainConnection.additionalInfo.layoutY = destNode.y;
+ ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id;
+ ruleChainMetaData.ruleChainConnections.push(ruleChainConnection);
+ } else {
+ var toIndex = nodes.indexOf(destNode);
+ var nodeConnection = {
+ fromIndex: fromIndex,
+ toIndex: toIndex,
+ type: edge.label
+ };
+ ruleChainMetaData.connections.push(nodeConnection);
+ }
+ }
+ }
+ ruleChainService.saveRuleChainMetaData(ruleChainMetaData).then(
+ (ruleChainMetaData) => {
+ vm.ruleChainMetaData = ruleChainMetaData;
+ if (vm.isImport) {
+ vm.isDirty = false;
+ vm.isImport = false;
+ $mdUtil.nextTick(() => {
+ $state.go('home.ruleChains.ruleChain', {ruleChainId: vm.ruleChain.id.id});
+ });
+ } else {
+ prepareRuleChain();
+ }
+ }
+ );
+ }
+ );
+ }
+
+ function revertRuleChain() {
+ prepareRuleChain();
+ }
+
+ function addRuleNode($event, ruleNode) {
+
+ ruleNode.configuration = angular.copy(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration);
+
+ var ruleChainId = vm.ruleChain.id ? vm.ruleChain.id.id : null;
+
+ vm.enableHotKeys = false;
+
+ $mdDialog.show({
+ controller: 'AddRuleNodeController',
+ controllerAs: 'vm',
+ templateUrl: addRuleNodeTemplate,
+ parent: angular.element($document[0].body),
+ locals: {ruleNode: ruleNode, ruleChainId: ruleChainId},
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function (ruleNode) {
+ ruleNode.id = 'rule-chain-node-' + vm.nextNodeID++;
+ ruleNode.connectors = [];
+ if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) {
+ ruleNode.connectors.push(
+ {
+ id: vm.nextConnectorID++,
+ type: flowchartConstants.leftConnectorType
+ }
+ );
+ }
+ if (ruleNode.component.configurationDescriptor.nodeDefinition.outEnabled) {
+ ruleNode.connectors.push(
+ {
+ id: vm.nextConnectorID++,
+ type: flowchartConstants.rightConnectorType
+ }
+ );
+ }
+ vm.ruleChainModel.nodes.push(ruleNode);
+ updateRuleNodesHighlight();
+ vm.enableHotKeys = true;
+ }, function () {
+ vm.enableHotKeys = true;
+ });
+ }
+
+ function addRuleNodeLink($event, link, labels) {
+ return $mdDialog.show({
+ controller: 'AddRuleNodeLinkController',
+ controllerAs: 'vm',
+ templateUrl: addRuleNodeLinkTemplate,
+ parent: angular.element($document[0].body),
+ locals: {link: link, labels: labels},
+ fullscreen: true,
+ targetEvent: $event
+ });
+ }
+
+ function objectsSelected() {
+ return vm.modelservice.nodes.getSelectedNodes().length > 0 ||
+ vm.modelservice.edges.getSelectedEdges().length > 0
+ }
+
+ function deleteSelected() {
+ vm.modelservice.deleteSelected();
+ }
+
+ function triggerResize() {
+ var w = angular.element($window);
+ w.triggerHandler('resize');
+ }
+}
+
+/*@ngInject*/
+export function AddRuleNodeController($scope, $mdDialog, ruleNode, ruleChainId, helpLinks) {
+
+ var vm = this;
+
+ vm.helpLinks = helpLinks;
+ vm.ruleNode = ruleNode;
+ vm.ruleChainId = ruleChainId;
+
+ vm.add = add;
+ vm.cancel = cancel;
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function add() {
+ $scope.theForm.$setPristine();
+ $mdDialog.hide(vm.ruleNode);
+ }
+}
+
+/*@ngInject*/
+export function AddRuleNodeLinkController($scope, $mdDialog, link, labels, helpLinks) {
+
+ var vm = this;
+
+ vm.helpLinks = helpLinks;
+ vm.link = link;
+ vm.labels = labels;
+
+ vm.add = add;
+ vm.cancel = cancel;
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function add() {
+ $scope.theForm.$setPristine();
+ $mdDialog.hide(vm.link);
+ }
+}
ui/src/app/rulechain/rulechain.directive.js 47(+47 -0)
diff --git a/ui/src/app/rulechain/rulechain.directive.js b/ui/src/app/rulechain/rulechain.directive.js
new file mode 100644
index 0000000..b23cd98
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.directive.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleChainFieldsetTemplate from './rulechain-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleChainDirective($compile, $templateCache, $mdDialog, $document, $q, $translate, types, toast) {
+ var linker = function (scope, element) {
+ var template = $templateCache.get(ruleChainFieldsetTemplate);
+ element.html(template);
+
+ scope.onRuleChainIdCopied = function() {
+ toast.showSuccess($translate.instant('rulechain.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+ };
+
+ $compile(element.contents())(scope);
+ }
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ ruleChain: '=',
+ isEdit: '=',
+ isReadOnly: '=',
+ theForm: '=',
+ onExportRuleChain: '&',
+ onDeleteRuleChain: '&'
+ }
+ };
+}
ui/src/app/rulechain/rulechain.routes.js 127(+127 -0)
diff --git a/ui/src/app/rulechain/rulechain.routes.js b/ui/src/app/rulechain/rulechain.routes.js
new file mode 100644
index 0000000..2aefd82
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.routes.js
@@ -0,0 +1,127 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleNodeTemplate from './rulenode.tpl.html';
+import ruleChainsTemplate from './rulechains.tpl.html';
+import ruleChainTemplate from './rulechain.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider) {
+
+ NodeTemplatePathProvider.setTemplatePath(ruleNodeTemplate);
+
+ $stateProvider
+ .state('home.ruleChains', {
+ url: '/ruleChains',
+ params: {'topIndex': 0},
+ module: 'private',
+ auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: ruleChainsTemplate,
+ controllerAs: 'vm',
+ controller: 'RuleChainsController'
+ }
+ },
+ data: {
+ searchEnabled: true,
+ pageTitle: 'rulechain.rulechains'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "settings_ethernet", "label": "rulechain.rulechains"}'
+ }
+ }).state('home.ruleChains.ruleChain', {
+ url: '/:ruleChainId',
+ reloadOnSearch: false,
+ module: 'private',
+ auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: ruleChainTemplate,
+ controller: 'RuleChainController',
+ controllerAs: 'vm'
+ }
+ },
+ resolve: {
+ ruleChain:
+ /*@ngInject*/
+ function($stateParams, ruleChainService) {
+ return ruleChainService.getRuleChain($stateParams.ruleChainId);
+ },
+ ruleChainMetaData:
+ /*@ngInject*/
+ function($stateParams, ruleChainService) {
+ return ruleChainService.getRuleChainMetaData($stateParams.ruleChainId);
+ },
+ ruleNodeComponents:
+ /*@ngInject*/
+ function($stateParams, ruleChainService) {
+ return ruleChainService.getRuleNodeComponents();
+ }
+ },
+ data: {
+ import: false,
+ searchEnabled: true,
+ pageTitle: 'rulechain.rulechain'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "settings_ethernet", "label": "{{ vm.ruleChain.name }}", "translate": "false"}'
+ }
+ }).state('home.ruleChains.importRuleChain', {
+ url: '/ruleChain/import',
+ reloadOnSearch: false,
+ module: 'private',
+ auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: ruleChainTemplate,
+ controller: 'RuleChainController',
+ controllerAs: 'vm'
+ }
+ },
+ params: {
+ ruleChainImport: {}
+ },
+ resolve: {
+ ruleChain:
+ /*@ngInject*/
+ function($stateParams) {
+ return $stateParams.ruleChainImport.ruleChain;
+ },
+ ruleChainMetaData:
+ /*@ngInject*/
+ function($stateParams) {
+ return $stateParams.ruleChainImport.metadata;
+ },
+ ruleNodeComponents:
+ /*@ngInject*/
+ function($stateParams, ruleChainService) {
+ return ruleChainService.getRuleNodeComponents();
+ }
+ },
+ data: {
+ import: true,
+ searchEnabled: true,
+ pageTitle: 'rulechain.rulechain'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "settings_ethernet", "label": "{{ (\'rulechain.import\' | translate) + \': \'+ vm.ruleChain.name }}", "translate": "false"}'
+ }
+ });
+}
ui/src/app/rulechain/rulechain.scss 466(+466 -0)
diff --git a/ui/src/app/rulechain/rulechain.scss b/ui/src/app/rulechain/rulechain.scss
new file mode 100644
index 0000000..c9006db
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.scss
@@ -0,0 +1,466 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.tb-rulechain {
+ .tb-fullscreen-button-style {
+ z-index: 1;
+ }
+ section.tb-header-buttons.tb-library-open {
+ pointer-events: none;
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ z-index: 1;
+ .md-button.tb-btn-open-library {
+ left: 0px;
+ top: 0px;
+ line-height: 36px;
+ width: 36px;
+ height: 36px;
+ margin: 4px 0 0 4px;
+ opacity: 0.5;
+ }
+ }
+ .tb-rulechain-library {
+ width: 250px;
+ min-width: 250px;
+ z-index: 1;
+ md-toolbar {
+ min-height: 48px;
+ height: 48px;
+ .md-toolbar-tools>.md-button:last-child {
+ margin-right: 0px;
+ }
+ .md-toolbar-tools {
+ font-size: 14px;
+ padding: 0px 6px;
+ height: 48px;
+ .md-button.md-icon-button {
+ margin: 0px;
+ &.tb-small {
+ height: 32px;
+ min-height: 32px;
+ line-height: 20px;
+ padding: 6px;
+ width: 32px;
+ md-icon {
+ line-height: 20px;
+ font-size: 20px;
+ height: 20px;
+ width: 20px;
+ min-height: 20px;
+ min-width: 20px;
+ }
+ }
+ }
+ }
+ }
+ .tb-rulechain-library-panel-group {
+ overflow-y: auto;
+ overflow-x: hidden;
+ .tb-panel-title {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ min-width: 180px;
+ }
+ .fc-canvas {
+ background: #f9f9f9;
+ }
+ md-icon.md-expansion-panel-icon {
+ margin-right: 0px;
+ }
+ md-expansion-panel-collapsed, .md-expansion-panel-header-container {
+ background: #e6e6e6;
+ border-color: #909090;
+ position: static;
+ }
+ md-expansion-panel {
+ &.md-open {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+ }
+ md-expansion-panel-content {
+ padding: 0px;
+ }
+ }
+ }
+ .tb-rulechain-graph {
+ z-index: 0;
+ overflow: auto;
+ }
+}
+
+#tb-rule-chain-context-menu {
+ padding-top: 0px;
+ border-radius: 8px;
+ max-height: 404px;
+ .tb-context-menu-header {
+ padding: 8px 5px 5px;
+ font-size: 14px;
+ display: flex;
+ flex-direction: row;
+ height: 36px;
+ min-height: 36px;
+ &.tb-rulechain {
+ background-color: #aac7e4;
+ }
+ &.tb-link {
+ background-color: #aac7e4;
+ }
+ md-icon {
+ padding-left: 2px;
+ padding-right: 10px;
+ }
+ .tb-context-menu-title {
+ font-weight: 500;
+ }
+ .tb-context-menu-subtitle {
+ font-size: 12px;
+ }
+ }
+}
+
+.fc-canvas {
+ min-width: 100%;
+ min-height: 100%;
+ outline: none;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.tb-rule-node, #tb-rule-chain-context-menu .tb-context-menu-header {
+ &.tb-filter-type {
+ background-color: #f1e861;
+ }
+ &.tb-enrichment-type {
+ background-color: #cdf14e;
+ }
+ &.tb-transformation-type {
+ background-color: #79cef1;
+ }
+ &.tb-action-type {
+ background-color: #f1928f;
+ }
+ &.tb-rule-chain-type {
+ background-color: #d6c4f1;
+ }
+}
+
+.tb-rule-node {
+ display: flex;
+ flex-direction: row;
+ min-width: 150px;
+ max-width: 150px;
+ min-height: 32px;
+ max-height: 32px;
+ height: 32px;
+ padding: 5px 10px;
+ border-radius: 5px;
+ background-color: #F15B26;
+ pointer-events: none;
+ color: #333;
+ border: solid 1px #777;
+ font-size: 12px;
+ line-height: 16px;
+ &.tb-rule-node-highlighted:not(.tb-rule-node-invalid) {
+ box-shadow: 0 0 10px 6px #51cbee;
+ .tb-node-title {
+ text-decoration: underline;
+ font-weight: bold;
+ }
+ }
+ &.tb-rule-node-invalid {
+ box-shadow: 0 0 10px 6px #ff5c50;
+ }
+ &.tb-input-type {
+ background-color: #a3eaa9;
+ user-select: none;
+ }
+ md-icon {
+ font-size: 20px;
+ width: 20px;
+ height: 20px;
+ min-height: 20px;
+ min-width: 20px;
+ padding-right: 4px;
+ }
+ .tb-node-type {
+
+ }
+ .tb-node-title {
+ font-weight: 500;
+ }
+ .tb-node-type, .tb-node-title {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+}
+
+.fc-node {
+ z-index: 1;
+ outline: none;
+ &.fc-dragging {
+ z-index: 10;
+ }
+ p {
+ padding: 0 15px;
+ text-align: center;
+ }
+ .fc-node-overlay {
+ position: absolute;
+ pointer-events: none;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #000;
+ opacity: 0;
+ }
+ &.fc-hover {
+ .fc-node-overlay {
+ opacity: 0.25;
+ }
+ }
+ &.fc-selected {
+ .fc-node-overlay {
+ opacity: 0.25;
+ }
+ }
+}
+
+.fc-leftConnectors, .fc-rightConnectors {
+ position: absolute;
+ top: 0;
+ height: 100%;
+
+ display: flex;
+ flex-direction: column;
+
+ z-index: 0;
+ .fc-magnet {
+ align-items: center;
+ }
+}
+
+.fc-leftConnectors {
+ left: -20px;
+}
+
+.fc-rightConnectors {
+ right: -20px;
+}
+
+.fc-magnet {
+ display: flex;
+ flex-grow: 1;
+ height: 60px;
+ justify-content: center;
+}
+
+.fc-connector {
+ width: 14px;
+ height: 14px;
+ border: 1px solid #333;
+ margin: 10px;
+ border-radius: 5px;
+ background-color: #ccc;
+ pointer-events: all;
+}
+
+.fc-connector.fc-hover {
+ background-color: #000;
+}
+
+.fc-arrow-marker {
+ polygon {
+ stroke: gray;
+ fill: gray;
+ }
+}
+
+.fc-arrow-marker-selected {
+ polygon {
+ stroke: red;
+ fill: red;
+ }
+}
+
+.fc-edge {
+ outline: none;
+ stroke: gray;
+ stroke-width: 4;
+ fill: transparent;
+ transition: stroke-width .2s;
+ &.fc-selected {
+ stroke: red;
+ stroke-width: 4;
+ fill: transparent;
+ }
+ &.fc-active {
+ animation: dash 3s linear infinite;
+ stroke-dasharray: 20;
+ }
+ &.fc-hover {
+ stroke: gray;
+ stroke-width: 6;
+ fill: transparent;
+ }
+ &.fc-dragging {
+ pointer-events: none;
+ }
+}
+
+.edge-endpoint {
+ fill: gray;
+}
+
+.fc-nodedelete {
+ display: none;
+}
+
+.fc-selected .fc-nodedelete {
+ outline: none;
+ display: block;
+ position: absolute;
+ right: -13px;
+ top: -16px;
+ border: solid 2px white;
+ border-radius: 50%;
+ font-weight: 600;
+ font-size: 18px;
+ line-height: 18px;
+ height: 20px;
+ padding-top: 2px;
+ width: 22px;
+ background: #494949;
+ color: #fff;
+ text-align: center;
+ vertical-align: bottom;
+ cursor: pointer;
+}
+
+.fc-noselect {
+ -webkit-touch-callout: none; /* iOS Safari */
+ -webkit-user-select: none; /* Safari */
+ -khtml-user-select: none; /* Konqueror HTML */
+ -moz-user-select: none; /* Firefox */
+ -ms-user-select: none; /* Internet Explorer/Edge */
+ user-select: none; /* Non-prefixed version, currently
+ supported by Chrome and Opera */
+}
+
+.fc-edge-label {
+ position: absolute;
+ transition: transform .2s;
+ opacity: 0.8;
+ &.ng-leave {
+ transition: 0s none;
+ }
+ &.fc-hover {
+ transform: scale(1.25);
+ }
+ &.fc-selected {
+ .fc-edge-label-text {
+ span {
+ border: solid red;
+ color: red;
+ }
+ }
+ }
+ .fc-nodedelete {
+ right: -13px;
+ top: -30px;
+ }
+ &:focus {
+ outline: 0;
+ }
+}
+
+.fc-edge-label-text {
+ position: absolute;
+ -webkit-transform: translate(-50%, -50%);
+ transform: translate(-50%, -50%);
+ white-space: nowrap;
+ text-align: center;
+ font-size: 14px;
+ font-weight: 600;
+ span {
+ cursor: default;
+ border: solid 2px #003a79;
+ border-radius: 10px;
+ color: #003a79;
+ background-color: #fff;
+ padding: 3px 5px;
+ }
+}
+
+.fc-select-rectangle {
+ border: 2px dashed #5262ff;
+ position: absolute;
+ background: rgba(20,125,255,0.1);
+ z-index: 2;
+}
+
+@keyframes dash {
+ from {
+ stroke-dashoffset: 500;
+ }
+}
+
+.tb-rule-node-tooltip {
+ font-size: 14px;
+ width: 300px;
+ color: #333;
+}
+
+.tb-rule-node-error-tooltip {
+ font-size: 16px;
+ color: #ea0d0d;
+}
+
+.tb-rule-node-tooltip, .tb-rule-node-error-tooltip {
+ #tooltip-content {
+ .tb-node-title {
+ font-weight: 600;
+ }
+ .tb-node-description {
+ font-style: italic;
+ color: #555;
+ }
+ .tb-node-details {
+ padding-top: 10px;
+ padding-bottom: 10px;
+ }
+ code {
+ padding: 0px 3px 2px 3px;
+ margin: 1px;
+ color: #AD1625;
+ white-space: nowrap;
+ background-color: #f7f7f9;
+ border: 1px solid #e1e1e8;
+ border-radius: 2px;
+ font-size: 12px;
+ }
+ }
+}
\ No newline at end of file
ui/src/app/rulechain/rulechain.tpl.html 229(+229 -0)
diff --git a/ui/src/app/rulechain/rulechain.tpl.html b/ui/src/app/rulechain/rulechain.tpl.html
new file mode 100644
index 0000000..59f246a
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.tpl.html
@@ -0,0 +1,229 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+
+<md-content flex tb-expand-fullscreen tb-confirm-on-exit is-dirty="vm.isConfirmOnExit"
+ expand-tooltip-direction="bottom" layout="column" class="tb-rulechain"
+ ng-keydown="vm.keyDown($event)"
+ ng-keyup="vm.keyUp($event)" on-fullscreen-changed="vm.isFullscreen = expanded">
+ <section class="tb-rulechain-container" flex layout="column">
+ <div class="tb-rulechain-layout" flex layout="row">
+ <section layout="row" layout-wrap
+ class="tb-header-buttons md-fab tb-library-open">
+ <md-button ng-show="!vm.isLibraryOpen"
+ class="tb-btn-header tb-btn-open-library md-primary md-fab md-fab-top-left"
+ aria-label="{{ 'rulenode.open-node-library' | translate }}"
+ ng-click="vm.isLibraryOpen = true">
+ <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+ {{ 'rulenode.open-node-library' | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="menu"></ng-md-icon>
+ </md-button>
+ </section>
+ <md-sidenav class="tb-rulechain-library md-sidenav-left md-whiteframe-4dp"
+ md-disable-backdrop
+ md-is-locked-open="vm.isLibraryOpenReadonly"
+ md-is-open="vm.isLibraryOpenReadonly"
+ md-component-id="rulechain-library-sidenav" layout="column">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <md-button class="md-icon-button tb-small" aria-label="{{ 'action.search' | translate }}">
+ <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+ <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+ {{'rulenode.search' | translate}}
+ </md-tooltip>
+ </md-button>
+ <div layout="row" md-theme="tb-dark" flex>
+ <md-input-container flex>
+ <label> </label>
+ <input ng-model="vm.ruleNodeSearch" placeholder="{{'rulenode.search' | translate}}"/>
+ </md-input-container>
+ </div>
+ <md-button class="md-icon-button tb-small" aria-label="Close"
+ ng-show="vm.ruleNodeSearch"
+ ng-click="vm.ruleNodeSearch = ''">
+ <md-icon aria-label="Close" class="material-icons">close</md-icon>
+ <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+ {{ 'action.clear-search' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button class="md-icon-button tb-small" aria-label="Close" ng-click="vm.isLibraryOpen = false">
+ <md-icon aria-label="Close" class="material-icons">chevron_left</md-icon>
+ <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+ {{ 'action.close' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-expansion-panel-group flex
+ ng-if="vm.ruleChainLibraryLoaded" class="tb-rulechain-library-panel-group"
+ md-component-id="libraryPanelGroup" auto-expand="true" multiple>
+ <md-expansion-panel md-component-id="{{typeId}}" id="{{typeId}}" ng-repeat="(typeId, typeModel) in vm.ruleNodeTypesModel">
+ <md-expansion-panel-collapsed ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)"
+ ng-mouseleave="vm.destroyTooltips()">
+ <div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
+ <md-expansion-panel-icon></md-expansion-panel-icon>
+ </md-expansion-panel-collapsed>
+ <md-expansion-panel-expanded>
+ <md-expansion-panel-header ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)"
+ ng-mouseleave="vm.destroyTooltips()"
+ ng-click="vm.$mdExpansionPanel(typeId).collapse()">
+ <div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
+ <md-expansion-panel-icon></md-expansion-panel-icon>
+ </md-expansion-panel-header>
+ <md-expansion-panel-content>
+ <fc-canvas id="tb-rulechain-{{typeId}}"
+ model="vm.ruleNodeTypesModel[typeId].model" selected-objects="vm.ruleNodeTypesModel[typeId].selectedObjects"
+ automatic-resize="false"
+ callbacks="vm.nodeLibCallbacks"
+ node-width="170"
+ node-height="50"
+ control="vm.ruleNodeTypesCanvasControl[typeId]"
+ drop-target-id="'tb-rulchain-canvas'"></fc-canvas>
+ </md-expansion-panel-content>
+ </md-expansion-panel-expanded>
+ </md-expansion-panel>
+ </md-expansion-panel-group>
+ </md-sidenav>
+ <md-menu flex style="position: relative;" md-position-mode="target target" tb-offset-x="-20" tb-offset-y="-45" tb-mousepoint-menu>
+ <div class="tb-absolute-fill tb-rulechain-graph" ng-click="" tb-contextmenu="vm.openRuleChainContextMenu($event, $mdOpenMousepointMenu)">
+ <fc-canvas id="tb-rulchain-canvas"
+ model="vm.ruleChainModel"
+ selected-objects="vm.selectedObjects"
+ edge-style="curved"
+ node-width="170"
+ node-height="50"
+ automatic-resize="true"
+ control="vm.canvasControl"
+ callbacks="vm.editCallbacks">
+ </fc-canvas>
+ </div>
+ <md-menu-content id="tb-rule-chain-context-menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
+ <div class="tb-context-menu-header {{vm.contextInfo.headerClass}}">
+ <md-icon aria-label="node-type-icon"
+ class="material-icons">{{vm.contextInfo.icon}}</md-icon>
+ <div flex>
+ <div class="tb-context-menu-title">{{vm.contextInfo.title}}</div>
+ <div class="tb-context-menu-subtitle">{{vm.contextInfo.subtitle}}</div>
+ </div>
+ </div>
+ <div ng-repeat="item in vm.contextInfo.items">
+ <md-divider ng-if="item.divider"></md-divider>
+ <md-menu-item ng-if="!item.divider">
+ <md-button ng-disabled="!item.enabled" ng-click="item.action(vm.contextMenuEvent)">
+ <span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
+ <md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
+ <span translate>{{item.value}}</span>
+ </md-button>
+ </md-menu-item>
+ </div>
+ </md-menu-content>
+ </md-menu>
+ </div>
+ <tb-details-sidenav class="tb-rulenode-details-sidenav"
+ header-title="{{vm.editingRuleNode.name}}"
+ header-subtitle="{{(vm.types.ruleNodeType[vm.editingRuleNode.component.type].name | translate)
+ + ' - ' + vm.editingRuleNode.component.name}}"
+ is-read-only="vm.selectedRuleNodeTabIndex > 0"
+ is-open="vm.isEditingRuleNode"
+ tb-enable-backdrop
+ is-always-edit="true"
+ on-close-details="vm.onEditRuleNodeClosed()"
+ on-toggle-details-edit-mode="vm.onRevertRuleNodeEdit(vm.ruleNodeForm)"
+ on-apply-details="vm.saveRuleNode(vm.ruleNodeForm)"
+ the-form="vm.ruleNodeForm">
+ <details-buttons tb-help="vm.helpLinkIdForRuleNodeType()" help-container-id="help-container">
+ <div id="help-container"></div>
+ </details-buttons>
+ <md-tabs md-selected="vm.selectedRuleNodeTabIndex"
+ id="ruleNodeTabs" md-border-bottom flex class="tb-absolute-fill" ng-if="vm.isEditingRuleNode">
+ <md-tab label="{{ 'rulenode.details' | translate }}">
+ <form name="vm.ruleNodeForm">
+ <tb-rule-node
+ rule-node="vm.editingRuleNode"
+ rule-chain-id="vm.ruleChain.id.id"
+ is-edit="true"
+ is-read-only="false"
+ on-delete-rule-node="vm.deleteRuleNode(event, vm.editingRuleNode)"
+ the-form="vm.ruleNodeForm">
+ </tb-rule-node>
+ </form>
+ </md-tab>
+ <md-tab ng-if="vm.isEditingRuleNode && vm.editingRuleNode.ruleNodeId"
+ md-on-select="vm.triggerResize()" label="{{ 'rulenode.events' | translate }}">
+ <tb-event-table flex entity-type="vm.types.entityType.rulenode"
+ entity-id="vm.editingRuleNode.ruleNodeId.id"
+ tenant-id="vm.ruleChain.tenantId.id"
+ debug-event-types="{{vm.types.debugEventType.debugRuleNode.value}}"
+ default-event-type="{{vm.types.debugEventType.debugRuleNode.value}}">
+ </tb-event-table>
+ </md-tab>
+ </md-tabs>
+ </tb-details-sidenav>
+ <tb-details-sidenav class="tb-rulenode-link-details-sidenav"
+ header-title="{{vm.editingRuleNodeLink.label}}"
+ header-subtitle="{{'rulenode.link-details' | translate}}"
+ is-read-only="false"
+ is-open="vm.isEditingRuleNodeLink"
+ tb-enable-backdrop
+ is-always-edit="true"
+ on-close-details="vm.onEditRuleNodeLinkClosed()"
+ on-toggle-details-edit-mode="vm.onRevertRuleNodeLinkEdit(vm.ruleNodeLinkForm)"
+ on-apply-details="vm.saveRuleNodeLink(vm.ruleNodeLinkForm)"
+ the-form="vm.ruleNodeLinkForm">
+ <details-buttons tb-help="vm.helpLinkIdForRuleNodeLink()" help-container-id="link-help-container">
+ <div id="link-help-container"></div>
+ </details-buttons>
+ <form name="vm.ruleNodeLinkForm" ng-if="vm.isEditingRuleNodeLink">
+ <tb-rule-node-link
+ link="vm.editingRuleNodeLink"
+ labels="vm.editingRuleNodeLinkLabels"
+ is-edit="true"
+ is-read-only="false"
+ the-form="vm.ruleNodeLinkForm">
+ </tb-rule-node-link>
+ </form>
+ </tb-details-sidenav>
+ </section>
+ <section layout="row" layout-wrap class="tb-footer-buttons md-fab" layout-align="start end">
+ <md-button ng-disabled="$root.loading" ng-show="vm.objectsSelected()" class="tb-btn-footer md-accent md-hue-2 md-fab"
+ ng-click="vm.deleteSelected()" aria-label="{{ 'action.delete' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'rulenode.delete-selected-objects' | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="delete"></ng-md-icon>
+ </md-button>
+ <md-button ng-disabled="$root.loading || vm.isInvalid || (!vm.isDirty && !vm.isImport)"
+ class="tb-btn-footer md-accent md-hue-2 md-fab"
+ aria-label="{{ 'action.apply' | translate }}"
+ ng-click="vm.saveRuleChain()">
+ <md-tooltip md-direction="top">
+ {{ 'action.apply-changes' | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="done"></ng-md-icon>
+ </md-button>
+ <md-button ng-disabled="$root.loading || !vm.isDirty"
+ class="tb-btn-footer md-accent md-hue-2 md-fab"
+ aria-label="{{ 'action.decline-changes' | translate }}"
+ ng-click="vm.revertRuleChain()">
+ <md-tooltip md-direction="top">
+ {{ 'action.decline-changes' | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="close"></ng-md-icon>
+ </md-button>
+ </section>
+</md-content>
ui/src/app/rulechain/rulechain-card.tpl.html 18(+18 -0)
diff --git a/ui/src/app/rulechain/rulechain-card.tpl.html b/ui/src/app/rulechain/rulechain-card.tpl.html
new file mode 100644
index 0000000..48a572c
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain-card.tpl.html
@@ -0,0 +1,18 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div class="tb-uppercase" ng-if="item && parentCtl.types.id.nullUid === item.tenantId.id" translate>rulechain.system</div>
diff --git a/ui/src/app/rulechain/rulechain-fieldset.tpl.html b/ui/src/app/rulechain/rulechain-fieldset.tpl.html
new file mode 100644
index 0000000..2189daa
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain-fieldset.tpl.html
@@ -0,0 +1,54 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-button ng-click="onExportRuleChain({event: $event})"
+ ng-show="!isEdit"
+ class="md-raised md-primary">{{ 'rulechain.export' | translate }}</md-button>
+<md-button ng-click="onDeleteRuleChain({event: $event})"
+ ng-show="!isEdit && !isReadOnly"
+ class="md-raised md-primary">{{ 'rulechain.delete' | translate }}</md-button>
+
+<div layout="row">
+ <md-button ngclipboard data-clipboard-action="copy"
+ ngclipboard-success="onRuleChainIdCopied(e)"
+ data-clipboard-text="{{ruleChain.id.id}}" ng-show="!isEdit"
+ class="md-raised">
+ <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+ <span translate>rulechain.copyId</span>
+ </md-button>
+</div>
+
+<md-content class="md-padding tb-rulechain-fieldset" layout="column">
+ <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
+ <md-input-container class="md-block">
+ <label translate>rulechain.name</label>
+ <input required name="name" ng-model="ruleChain.name">
+ <div ng-messages="theForm.name.$error">
+ <div translate ng-message="required">rulechain.name-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <md-checkbox ng-disabled="$root.loading || !isEdit" aria-label="{{ 'rulechain.debug-mode' | translate }}"
+ ng-model="ruleChain.debugMode">{{ 'rulechain.debug-mode' | translate }}
+ </md-checkbox>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>rulechain.description</label>
+ <textarea ng-model="ruleChain.additionalInfo.description" rows="2"></textarea>
+ </md-input-container>
+ </fieldset>
+</md-content>
ui/src/app/rulechain/rulechains.controller.js 188(+188 -0)
diff --git a/ui/src/app/rulechain/rulechains.controller.js b/ui/src/app/rulechain/rulechains.controller.js
new file mode 100644
index 0000000..7c857f2
--- /dev/null
+++ b/ui/src/app/rulechain/rulechains.controller.js
@@ -0,0 +1,188 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import addRuleChainTemplate from './add-rulechain.tpl.html';
+import ruleChainCard from './rulechain-card.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleChainsController(ruleChainService, userService, importExport, $state, $stateParams, $filter, $translate, types) {
+
+ var ruleChainActionsList = [
+ {
+ onAction: function ($event, item) {
+ vm.grid.openItem($event, item);
+ },
+ name: function() { return $translate.instant('rulechain.details') },
+ details: function() { return $translate.instant('rulechain.rulechain-details') },
+ icon: "edit"
+ },
+ {
+ onAction: function ($event, item) {
+ exportRuleChain($event, item);
+ },
+ name: function() { $translate.instant('action.export') },
+ details: function() { return $translate.instant('rulechain.export') },
+ icon: "file_download"
+ },
+ {
+ onAction: function ($event, item) {
+ vm.grid.deleteItem($event, item);
+ },
+ name: function() { return $translate.instant('action.delete') },
+ details: function() { return $translate.instant('rulechain.delete') },
+ icon: "delete",
+ isEnabled: isRuleChainEditable
+ }
+ ];
+
+ var ruleChainAddItemActionsList = [
+ {
+ onAction: function ($event) {
+ vm.grid.addItem($event);
+ },
+ name: function() { return $translate.instant('action.create') },
+ details: function() { return $translate.instant('rulechain.create-new-rulechain') },
+ icon: "insert_drive_file"
+ },
+ {
+ onAction: function ($event) {
+ importExport.importRuleChain($event).then(
+ function(ruleChainImport) {
+ $state.go('home.ruleChains.importRuleChain', {ruleChainImport:ruleChainImport});
+ }
+ );
+ },
+ name: function() { return $translate.instant('action.import') },
+ details: function() { return $translate.instant('rulechain.import') },
+ icon: "file_upload"
+ }
+ ];
+
+ var vm = this;
+
+ vm.types = types;
+
+ vm.ruleChainGridConfig = {
+
+ refreshParamsFunc: null,
+
+ deleteItemTitleFunc: deleteRuleChainTitle,
+ deleteItemContentFunc: deleteRuleChainText,
+ deleteItemsTitleFunc: deleteRuleChainsTitle,
+ deleteItemsActionTitleFunc: deleteRuleChainsActionTitle,
+ deleteItemsContentFunc: deleteRuleChainsText,
+
+ fetchItemsFunc: fetchRuleChains,
+ saveItemFunc: saveRuleChain,
+ clickItemFunc: openRuleChain,
+ deleteItemFunc: deleteRuleChain,
+
+ getItemTitleFunc: getRuleChainTitle,
+ itemCardTemplateUrl: ruleChainCard,
+ parentCtl: vm,
+
+ actionsList: ruleChainActionsList,
+ addItemActions: ruleChainAddItemActionsList,
+
+ onGridInited: gridInited,
+
+ addItemTemplateUrl: addRuleChainTemplate,
+
+ addItemText: function() { return $translate.instant('rulechain.add-rulechain-text') },
+ noItemsText: function() { return $translate.instant('rulechain.no-rulechains-text') },
+ itemDetailsText: function() { return $translate.instant('rulechain.rulechain-details') },
+ isSelectionEnabled: isRuleChainEditable,
+ isDetailsReadOnly: function(ruleChain) {
+ return !isRuleChainEditable(ruleChain);
+ }
+ };
+
+ if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
+ vm.ruleChainGridConfig.items = $stateParams.items;
+ }
+
+ if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
+ vm.ruleChainGridConfig.topIndex = $stateParams.topIndex;
+ }
+
+ vm.isRuleChainEditable = isRuleChainEditable;
+
+ vm.exportRuleChain = exportRuleChain;
+
+ function deleteRuleChainTitle(ruleChain) {
+ return $translate.instant('rulechain.delete-rulechain-title', {ruleChainName: ruleChain.name});
+ }
+
+ function deleteRuleChainText() {
+ return $translate.instant('rulechain.delete-rulechain-text');
+ }
+
+ function deleteRuleChainsTitle(selectedCount) {
+ return $translate.instant('rulechain.delete-rulechains-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteRuleChainsActionTitle(selectedCount) {
+ return $translate.instant('rulechain.delete-rulechains-action-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteRuleChainsText() {
+ return $translate.instant('rulechain.delete-rulechains-text');
+ }
+
+ function gridInited(grid) {
+ vm.grid = grid;
+ }
+
+ function fetchRuleChains(pageLink) {
+ return ruleChainService.getRuleChains(pageLink);
+ }
+
+ function saveRuleChain(ruleChain) {
+ return ruleChainService.saveRuleChain(ruleChain);
+ }
+
+ function openRuleChain($event, ruleChain) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ $state.go('home.ruleChains.ruleChain', {ruleChainId: ruleChain.id.id});
+ }
+
+ function deleteRuleChain(ruleChainId) {
+ return ruleChainService.deleteRuleChain(ruleChainId);
+ }
+
+ function getRuleChainTitle(ruleChain) {
+ return ruleChain ? ruleChain.name : '';
+ }
+
+ function isRuleChainEditable(ruleChain) {
+ if (userService.getAuthority() === 'TENANT_ADMIN') {
+ return ruleChain && ruleChain.tenantId.id != types.id.nullUid;
+ } else {
+ return userService.getAuthority() === 'SYS_ADMIN';
+ }
+ }
+
+ function exportRuleChain($event, ruleChain) {
+ $event.stopPropagation();
+ importExport.exportRuleChain(ruleChain.id.id);
+ }
+
+}
ui/src/app/rulechain/rulechains.tpl.html 76(+76 -0)
diff --git a/ui/src/app/rulechain/rulechains.tpl.html b/ui/src/app/rulechain/rulechains.tpl.html
new file mode 100644
index 0000000..cf9d256
--- /dev/null
+++ b/ui/src/app/rulechain/rulechains.tpl.html
@@ -0,0 +1,76 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<tb-grid grid-configuration="vm.ruleChainGridConfig">
+ <details-buttons tb-help="'rulechains'" help-container-id="help-container">
+ <div id="help-container"></div>
+ </details-buttons>
+ <md-tabs ng-class="{'tb-headless': (vm.grid.detailsConfig.isDetailsEditMode || !vm.isRuleChainEditable(vm.grid.operatingItem()))}"
+ id="tabs" md-border-bottom flex class="tb-absolute-fill">
+ <md-tab label="{{ 'rulechain.details' | translate }}">
+ <tb-rule-chain rule-chain="vm.grid.operatingItem()"
+ is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+ is-read-only="vm.grid.isDetailsReadOnly(vm.grid.operatingItem())"
+ the-form="vm.grid.detailsForm"
+ on-export-rule-chain="vm.exportRuleChain(event, vm.grid.detailsConfig.currentItem)"
+ on-delete-rule-chain="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-rule-chain>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
+ <tb-attribute-table flex
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.rulechain}}"
+ entity-name="vm.grid.operatingItem().name"
+ default-attribute-scope="{{vm.types.attributesScope.server.value}}">
+ </tb-attribute-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
+ <tb-attribute-table flex
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.rulechain}}"
+ entity-name="vm.grid.operatingItem().name"
+ default-attribute-scope="{{vm.types.latestTelemetry.value}}"
+ disable-attribute-scope-selection="true">
+ </tb-attribute-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
+ <tb-alarm-table flex entity-type="vm.types.entityType.rulechain"
+ entity-id="vm.grid.operatingItem().id.id">
+ </tb-alarm-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'rulechain.events' | translate }}">
+ <tb-event-table flex entity-type="vm.types.entityType.rulechain"
+ entity-id="vm.grid.operatingItem().id.id"
+ tenant-id="vm.grid.operatingItem().tenantId.id"
+ debug-event-types="{{vm.types.debugEventType.debugRuleChain.value}}"
+ default-event-type="{{vm.types.debugEventType.debugRuleChain.value}}">
+ </tb-event-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
+ <tb-relation-table flex
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.rulechain}}">
+ </tb-relation-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem()) && vm.grid.isTenantAdmin()"
+ md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+ <tb-audit-log-table flex entity-type="vm.types.entityType.rulechain"
+ entity-id="vm.grid.operatingItem().id.id"
+ audit-log-mode="{{vm.types.auditLogMode.entity}}">
+ </tb-audit-log-table>
+ </md-tab>
+ </md-tabs>
+</tb-grid>
ui/src/app/rulechain/rulenode.directive.js 81(+81 -0)
diff --git a/ui/src/app/rulechain/rulenode.directive.js b/ui/src/app/rulechain/rulenode.directive.js
new file mode 100644
index 0000000..be3e9c3
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode.directive.js
@@ -0,0 +1,81 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './rulenode.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleNodeFieldsetTemplate from './rulenode-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleNodeDirective($compile, $templateCache, ruleChainService, types) {
+ var linker = function (scope, element) {
+ var template = $templateCache.get(ruleNodeFieldsetTemplate);
+ element.html(template);
+
+ scope.types = types;
+
+ scope.params = {
+ targetRuleChainId: null
+ };
+
+ scope.$watch('ruleNode', function() {
+ if (scope.ruleNode && scope.ruleNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
+ scope.params.targetRuleChainId = scope.ruleNode.targetRuleChainId;
+ watchTargetRuleChain();
+ } else {
+ if (scope.targetRuleChainWatch) {
+ scope.targetRuleChainWatch();
+ scope.targetRuleChainWatch = null;
+ }
+ }
+ });
+
+ function watchTargetRuleChain() {
+ scope.targetRuleChainWatch = scope.$watch('params.targetRuleChainId',
+ function(targetRuleChainId) {
+ if (scope.ruleNode.targetRuleChainId != targetRuleChainId) {
+ scope.ruleNode.targetRuleChainId = targetRuleChainId;
+ if (targetRuleChainId) {
+ ruleChainService.getRuleChain(targetRuleChainId).then(
+ (ruleChain) => {
+ scope.ruleNode.name = ruleChain.name;
+ }
+ );
+ } else {
+ scope.ruleNode.name = "";
+ }
+ }
+ }
+ );
+ }
+ $compile(element.contents())(scope);
+ }
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ ruleChainId: '=',
+ ruleNode: '=',
+ isEdit: '=',
+ isReadOnly: '=',
+ theForm: '=',
+ onDeleteRuleNode: '&'
+ }
+ };
+}
ui/src/app/rulechain/rulenode.tpl.html 49(+49 -0)
diff --git a/ui/src/app/rulechain/rulenode.tpl.html b/ui/src/app/rulechain/rulenode.tpl.html
new file mode 100644
index 0000000..973ea1f
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode.tpl.html
@@ -0,0 +1,49 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div
+ id="{{node.id}}"
+ ng-attr-style="position: absolute; top: {{ node.y }}px; left: {{ node.x }}px;"
+ ng-dblclick="callbacks.doubleClick($event, node)"
+ ng-mousedown="callbacks.mouseDown($event, node)"
+ ng-mouseenter="callbacks.mouseEnter($event, node)"
+ ng-mouseleave="callbacks.mouseLeave($event, node)">
+ <div class="{{flowchartConstants.nodeOverlayClass}}"></div>
+ <div class="tb-rule-node {{node.nodeClass}}" ng-class="{'tb-rule-node-highlighted' : node.highlighted, 'tb-rule-node-invalid': node.error }">
+ <md-icon aria-label="node-type-icon" flex="15"
+ class="material-icons">{{node.icon}}</md-icon>
+ <div layout="column" flex="85" layout-align="center">
+ <span class="tb-node-type">{{ node.component.name }}</span>
+ <span class="tb-node-title" ng-if="node.name">{{ node.name }}</span>
+ </div>
+ <div class="{{flowchartConstants.leftConnectorClass}}">
+ <div fc-magnet
+ ng-repeat="connector in modelservice.nodes.getConnectorsByType(node, flowchartConstants.leftConnectorType)">
+ <div fc-connector></div>
+ </div>
+ </div>
+ <div class="{{flowchartConstants.rightConnectorClass}}">
+ <div fc-magnet
+ ng-repeat="connector in modelservice.nodes.getConnectorsByType(node, flowchartConstants.rightConnectorType)">
+ <div fc-connector></div>
+ </div>
+ </div>
+ </div>
+ <div ng-if="modelservice.isEditable() && !node.readonly" class="fc-nodedelete" ng-click="modelservice.nodes.delete(node)">
+ ×
+ </div>
+</div>
diff --git a/ui/src/app/rulechain/rulenode-config.directive.js b/ui/src/app/rulechain/rulenode-config.directive.js
new file mode 100644
index 0000000..9bb8c48
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-config.directive.js
@@ -0,0 +1,78 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleNodeConfigTemplate from './rulenode-config.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleNodeConfigDirective($compile, $templateCache, $injector, $translate) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+ var template = $templateCache.get(ruleNodeConfigTemplate);
+ element.html(template);
+
+ scope.$watch('configuration', function (newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal)) {
+ ngModelCtrl.$setViewValue(scope.configuration);
+ }
+ });
+
+ ngModelCtrl.$render = function () {
+ scope.configuration = ngModelCtrl.$viewValue;
+ };
+
+ scope.useDefinedDirective = function() {
+ return scope.nodeDefinition &&
+ scope.nodeDefinition.configDirective && !scope.definedDirectiveError;
+ };
+
+ scope.$watch('nodeDefinition', () => {
+ if (scope.nodeDefinition) {
+ validateDefinedDirective();
+ }
+ });
+
+ function validateDefinedDirective() {
+ if (scope.nodeDefinition.uiResourceLoadError && scope.nodeDefinition.uiResourceLoadError.length) {
+ scope.definedDirectiveError = scope.nodeDefinition.uiResourceLoadError;
+ } else {
+ var definedDirective = scope.nodeDefinition.configDirective;
+ if (definedDirective && definedDirective.length) {
+ if (!$injector.has(definedDirective + 'Directive')) {
+ scope.definedDirectiveError = $translate.instant('rulenode.directive-is-not-loaded', {directiveName: definedDirective});
+ }
+ }
+ }
+ }
+
+ $compile(element.contents())(scope);
+ };
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {
+ nodeDefinition:'=',
+ required:'=ngRequired',
+ readonly:'=ngReadonly'
+ },
+ link: linker
+ };
+
+}
diff --git a/ui/src/app/rulechain/rulenode-config.tpl.html b/ui/src/app/rulechain/rulenode-config.tpl.html
new file mode 100644
index 0000000..32d5347
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-config.tpl.html
@@ -0,0 +1,32 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+
+<tb-rule-node-defined-config ng-if="useDefinedDirective()"
+ ng-model="configuration"
+ rule-node-directive="{{nodeDefinition.configDirective}}"
+ ng-required="required"
+ ng-readonly="readonly">
+</tb-rule-node-defined-config>
+<div class="tb-rulenode-directive-error" ng-if="definedDirectiveError">{{definedDirectiveError}}</div>
+<tb-json-object-edit ng-if="!useDefinedDirective()"
+ class="tb-rule-node-configuration-json"
+ ng-model="configuration"
+ label="{{ 'rulenode.configuration' | translate }}"
+ ng-required="required"
+ fill-height="true">
+</tb-json-object-edit>
diff --git a/ui/src/app/rulechain/rulenode-defined-config.directive.js b/ui/src/app/rulechain/rulenode-defined-config.directive.js
new file mode 100644
index 0000000..5100fbb
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-defined-config.directive.js
@@ -0,0 +1,67 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const SNAKE_CASE_REGEXP = /[A-Z]/g;
+
+/*@ngInject*/
+export default function RuleNodeDefinedConfigDirective($compile) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+
+ attrs.$observe('ruleNodeDirective', function() {
+ loadTemplate();
+ });
+
+ scope.$watch('configuration', function (newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal)) {
+ ngModelCtrl.$setViewValue(scope.configuration);
+ }
+ });
+
+ ngModelCtrl.$render = function () {
+ scope.configuration = ngModelCtrl.$viewValue;
+ };
+
+ function loadTemplate() {
+ if (scope.ruleNodeConfigScope) {
+ scope.ruleNodeConfigScope.$destroy();
+ }
+ var directive = snake_case(attrs.ruleNodeDirective, '-');
+ var template = `<${directive} ng-model="configuration" ng-required="required" ng-readonly="readonly"></${directive}>`;
+ element.html(template);
+ scope.ruleNodeConfigScope = scope.$new();
+ $compile(element.contents())(scope.ruleNodeConfigScope);
+ }
+
+ function snake_case(name, separator) {
+ separator = separator || '_';
+ return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
+ return (pos ? separator : '') + letter.toLowerCase();
+ });
+ }
+ };
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {
+ required:'=ngRequired',
+ readonly:'=ngReadonly'
+ },
+ link: linker
+ };
+
+}
diff --git a/ui/src/app/rulechain/rulenode-fieldset.tpl.html b/ui/src/app/rulechain/rulenode-fieldset.tpl.html
new file mode 100644
index 0000000..7b0fae5
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-fieldset.tpl.html
@@ -0,0 +1,63 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-button ng-click="onDeleteRuleNode({event: $event})"
+ ng-show="!isEdit && !isReadOnly"
+ class="md-raised md-primary">{{ 'rulenode.delete' | translate }}</md-button>
+
+<md-content class="md-padding tb-rulenode" layout="column">
+ <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
+ <section ng-if="ruleNode.component.type != types.ruleNodeType.RULE_CHAIN.value">
+ <section layout="column" layout-gt-sm="row">
+ <md-input-container flex class="md-block">
+ <label translate>rulenode.name</label>
+ <input required name="name" ng-model="ruleNode.name">
+ <div ng-messages="theForm.name.$error">
+ <div translate ng-message="required">rulenode.name-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <md-checkbox ng-disabled="$root.loading || !isEdit" aria-label="{{ 'rulenode.debug-mode' | translate }}"
+ ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }}
+ </md-checkbox>
+ </md-input-container>
+ </section>
+ <tb-rule-node-config ng-model="ruleNode.configuration"
+ ng-required="true"
+ node-definition="ruleNode.component.configurationDescriptor.nodeDefinition"
+ ng-readonly="$root.loading || !isEdit || isReadOnly">
+ </tb-rule-node-config>
+ <md-input-container class="md-block">
+ <label translate>rulenode.description</label>
+ <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
+ </md-input-container>
+ </section>
+ <section ng-if="ruleNode.component.type == types.ruleNodeType.RULE_CHAIN.value">
+ <tb-entity-autocomplete the-form="theForm"
+ ng-disabled="$root.loading || !isEdit || isReadOnly"
+ tb-required="true"
+ exclude-entity-ids="[ruleChainId]"
+ entity-type="types.entityType.rulechain"
+ ng-model="params.targetRuleChainId">
+ </tb-entity-autocomplete>
+ <md-input-container class="md-block">
+ <label translate>rulenode.description</label>
+ <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
+ </md-input-container>
+ </section>
+ </fieldset>
+</md-content>
diff --git a/ui/src/app/rulechain/script/node-script-test.controller.js b/ui/src/app/rulechain/script/node-script-test.controller.js
new file mode 100644
index 0000000..487d11f
--- /dev/null
+++ b/ui/src/app/rulechain/script/node-script-test.controller.js
@@ -0,0 +1,177 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './node-script-test.scss';
+
+import Split from 'split.js';
+
+import beautify from 'js-beautify';
+
+const js_beautify = beautify.js;
+
+/*@ngInject*/
+export default function NodeScriptTestController($scope, $mdDialog, $window, $document, $timeout,
+ $q, $mdUtil, $translate, toast, types, utils,
+ ruleChainService, onShowingCallback, msg, msgType, metadata,
+ functionTitle, inputParams) {
+
+ var vm = this;
+
+ vm.types = types;
+ vm.functionTitle = functionTitle;
+ vm.inputParams = inputParams;
+ vm.inputParams.msg = js_beautify(angular.toJson(msg), {indent_size: 4});
+ vm.inputParams.metadata = metadata;
+ vm.inputParams.msgType = msgType;
+
+ vm.output = '';
+
+ vm.test = test;
+ vm.save = save;
+ vm.cancel = cancel;
+
+ $scope.$watch('theForm.metadataForm.$dirty', (newVal) => {
+ if (newVal) {
+ toast.hide();
+ }
+ });
+
+ onShowingCallback.onShowed = () => {
+ vm.nodeScriptTestDialogElement = angular.element('.tb-node-script-test-dialog');
+ var w = vm.nodeScriptTestDialogElement.width();
+ if (w > 0) {
+ initSplitLayout();
+ } else {
+ $scope.$watch(
+ function () {
+ return vm.nodeScriptTestDialogElement[0].offsetWidth || parseInt(vm.nodeScriptTestDialogElement.css('width'), 10);
+ },
+ function (newSize) {
+ if (newSize > 0) {
+ initSplitLayout();
+ }
+ }
+ );
+ }
+ };
+
+ function onDividerDrag() {
+ $scope.$broadcast('update-ace-editor-size');
+ }
+
+ function initSplitLayout() {
+ if (!vm.layoutInited) {
+ Split([angular.element('#top_panel', vm.nodeScriptTestDialogElement)[0], angular.element('#bottom_panel', vm.nodeScriptTestDialogElement)[0]], {
+ sizes: [35, 65],
+ gutterSize: 8,
+ cursor: 'row-resize',
+ direction: 'vertical',
+ onDrag: function () {
+ onDividerDrag()
+ }
+ });
+
+ Split([angular.element('#top_left_panel', vm.nodeScriptTestDialogElement)[0], angular.element('#top_right_panel', vm.nodeScriptTestDialogElement)[0]], {
+ sizes: [50, 50],
+ gutterSize: 8,
+ cursor: 'col-resize',
+ onDrag: function () {
+ onDividerDrag()
+ }
+ });
+
+ Split([angular.element('#bottom_left_panel', vm.nodeScriptTestDialogElement)[0], angular.element('#bottom_right_panel', vm.nodeScriptTestDialogElement)[0]], {
+ sizes: [50, 50],
+ gutterSize: 8,
+ cursor: 'col-resize',
+ onDrag: function () {
+ onDividerDrag()
+ }
+ });
+
+ onDividerDrag();
+
+ $scope.$applyAsync(function () {
+ vm.layoutInited = true;
+ var w = angular.element($window);
+ $timeout(function () {
+ w.triggerHandler('resize')
+ });
+ });
+
+ }
+ }
+
+ function test() {
+ testNodeScript().then(
+ (output) => {
+ vm.output = js_beautify(output, {indent_size: 4});
+ }
+ );
+ }
+
+ function checkInputParamErrors() {
+ $scope.theForm.metadataForm.$setPristine();
+ $scope.$broadcast('form-submit', 'validatePayload');
+ if (!$scope.theForm.payloadForm.$valid) {
+ return false;
+ } else if (!$scope.theForm.metadataForm.$valid) {
+ showMetadataError($translate.instant('rulenode.metadata-required'));
+ return false;
+ }
+ return true;
+ }
+
+ function showMetadataError(error) {
+ var toastParent = angular.element('#metadata-panel', vm.nodeScriptTestDialogElement);
+ toast.showError(error, toastParent, 'bottom left');
+ }
+
+ function testNodeScript() {
+ var deferred = $q.defer();
+ if (checkInputParamErrors()) {
+ $mdUtil.nextTick(() => {
+ ruleChainService.testScript(vm.inputParams).then(
+ (result) => {
+ if (result.error) {
+ toast.showError(result.error);
+ deferred.reject();
+ } else {
+ deferred.resolve(result.output);
+ }
+ },
+ () => {
+ deferred.reject();
+ }
+ );
+ });
+ } else {
+ deferred.reject();
+ }
+ return deferred.promise;
+ }
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function save() {
+ testNodeScript().then(() => {
+ $scope.theForm.funcBodyForm.$setPristine();
+ $mdDialog.hide(vm.inputParams.script);
+ });
+ }
+}
ui/src/app/rulechain/script/node-script-test.scss 112(+112 -0)
diff --git a/ui/src/app/rulechain/script/node-script-test.scss b/ui/src/app/rulechain/script/node-script-test.scss
new file mode 100644
index 0000000..42124fb
--- /dev/null
+++ b/ui/src/app/rulechain/script/node-script-test.scss
@@ -0,0 +1,112 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import '../../../scss/constants';
+@import "~compass-sass-mixins/lib/compass";
+
+md-dialog.tb-node-script-test-dialog {
+ &.md-dialog-fullscreen {
+ min-height: 100%;
+ min-width: 100%;
+ max-height: 100%;
+ max-width: 100%;
+ width: 100%;
+ height: 100%;
+ border-radius: 0;
+ }
+
+ .tb-split {
+ @include box-sizing(border-box);
+ overflow-y: auto;
+ overflow-x: hidden;
+ }
+
+ .ace_editor {
+ font-size: 14px !important;
+ }
+
+ .tb-content {
+ border: 1px solid #C0C0C0;
+ padding-top: 5px;
+ padding-left: 5px;
+ }
+
+ .gutter {
+ background-color: #eeeeee;
+
+ background-repeat: no-repeat;
+ background-position: 50%;
+ }
+
+ .gutter.gutter-horizontal {
+ cursor: col-resize;
+ background-image: url('../../../../node_modules/split.js/grips/vertical.png');
+ }
+
+ .gutter.gutter-vertical {
+ cursor: row-resize;
+ background-image: url('../../../../node_modules/split.js/grips/horizontal.png');
+ }
+
+ .tb-split.tb-split-horizontal, .gutter.gutter-horizontal {
+ height: 100%;
+ float: left;
+ }
+
+ .tb-split.tb-split-vertical {
+ display: flex;
+ .tb-split.tb-content {
+ height: 100%;
+ }
+ }
+
+ div.tb-editor-area-title-panel {
+ position: absolute;
+ font-size: 0.800rem;
+ font-weight: 500;
+ top: 10px;
+ right: 40px;
+ z-index: 5;
+ label {
+ color: #00acc1;
+ background: rgba(220, 220, 220, 0.35);
+ border-radius: 5px;
+ padding: 4px;
+ text-transform: uppercase;
+ }
+ .md-button {
+ color: #7B7B7B;
+ min-width: 32px;
+ min-height: 15px;
+ line-height: 15px;
+ font-size: 0.800rem;
+ margin: 0;
+ padding: 4px;
+ background: rgba(220, 220, 220, 0.35);
+ }
+ }
+
+ .tb-resize-container {
+ overflow-y: auto;
+ height: 100%;
+ width: 100%;
+ position: relative;
+
+ .ace_editor {
+ height: 100%;
+ }
+ }
+
+}
diff --git a/ui/src/app/rulechain/script/node-script-test.service.js b/ui/src/app/rulechain/script/node-script-test.service.js
new file mode 100644
index 0000000..c685634
--- /dev/null
+++ b/ui/src/app/rulechain/script/node-script-test.service.js
@@ -0,0 +1,81 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import nodeScriptTestTemplate from './node-script-test.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function NodeScriptTest($q, $mdDialog, $document) {
+
+ var service = {
+ testNodeScript: testNodeScript
+ };
+
+ return service;
+
+ function testNodeScript($event, script, scriptType, functionTitle, functionName, argNames, msg, metadata, msgType) {
+ var deferred = $q.defer();
+
+ if ($event) {
+ $event.stopPropagation();
+ }
+ var onShowingCallback = {
+ onShowed: () => {
+ }
+ };
+
+ var inputParams = {
+ script: script,
+ scriptType: scriptType,
+ functionName: functionName,
+ argNames: argNames
+ };
+
+ $mdDialog.show({
+ controller: 'NodeScriptTestController',
+ controllerAs: 'vm',
+ templateUrl: nodeScriptTestTemplate,
+ parent: angular.element($document[0].body),
+ locals: {
+ msg: msg,
+ metadata: metadata,
+ msgType: msgType,
+ functionTitle: functionTitle,
+ inputParams: inputParams,
+ onShowingCallback: onShowingCallback
+ },
+ fullscreen: true,
+ skipHide: true,
+ targetEvent: $event,
+ onComplete: () => {
+ onShowingCallback.onShowed();
+ }
+ }).then(
+ (script) => {
+ deferred.resolve(script);
+ },
+ () => {
+ deferred.reject();
+ }
+ );
+
+ return deferred.promise;
+ }
+
+}
\ No newline at end of file
diff --git a/ui/src/app/rulechain/script/node-script-test.tpl.html b/ui/src/app/rulechain/script/node-script-test.tpl.html
new file mode 100644
index 0000000..9337240
--- /dev/null
+++ b/ui/src/app/rulechain/script/node-script-test.tpl.html
@@ -0,0 +1,119 @@
+<!--
+
+ Copyright © 2016-2018 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog class="tb-node-script-test-dialog"
+ aria-label="{{ 'rulenode.test-script-function' | translate }}" style="width: 800px;">
+ <form flex name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2>{{ 'rulenode.test-script-function' | translate }}</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-dialog-content flex style="position: relative;">
+ <div class="tb-absolute-fill">
+ <div id="top_panel" class="tb-split tb-split-vertical">
+ <div id="top_left_panel" class="tb-split tb-content">
+ <div class="tb-resize-container">
+ <div class="tb-editor-area-title-panel">
+ <label translate>rulenode.message</label>
+ </div>
+ <ng-form name="payloadForm">
+ <div layout="column" style="height: 100%;">
+ <div layout="row">
+ <md-input-container class="md-block" style="margin-bottom: 0px; min-width: 200px;">
+ <label translate>rulenode.message-type</label>
+ <input required name="msgType" ng-model="vm.inputParams.msgType">
+ <div ng-messages="payloadForm.msgType.$error">
+ <div translate ng-message="required">rulenode.message-type-required</div>
+ </div>
+ </md-input-container>
+ </div>
+ <tb-json-content flex
+ ng-model="vm.inputParams.msg"
+ label="{{ 'rulenode.message' | translate }}"
+ content-type="vm.types.contentType.JSON.value"
+ validate-content="true"
+ validation-trigger-arg="validatePayload"
+ fill-height="true">
+ </tb-json-content>
+ </div>
+ </ng-form>
+ </div>
+ </div>
+ <div id="top_right_panel" class="tb-split tb-content">
+ <div class="tb-resize-container" id="metadata-panel">
+ <div class="tb-editor-area-title-panel">
+ <label translate>rulenode.metadata</label>
+ </div>
+ <ng-form name="metadataForm">
+ <tb-key-val-map title-text="rulenode.metadata" ng-disabled="$root.loading"
+ key-val-map="vm.inputParams.metadata"></tb-key-val-map>
+ </ng-form>
+ </div>
+ </div>
+ </div>
+ <div id="bottom_panel" class="tb-split tb-split-vertical">
+ <div id="bottom_left_panel" class="tb-split tb-content">
+ <div class="tb-resize-container">
+ <div class="tb-editor-area-title-panel">
+ <label>{{ vm.functionTitle }}</label>
+ </div>
+ <ng-form name="funcBodyForm">
+ <tb-js-func id="funcBodyInput" ng-model="vm.inputParams.script"
+ function-name="{{vm.inputParams.functionName}}"
+ function-args="{{ vm.inputParams.argNames }}"
+ validation-args="{{ [[vm.inputParams.msg, vm.inputParams.metadata, vm.inputParams.msgType]] }}"
+ validation-trigger-arg="validateFuncBody"
+ result-type="object"
+ fill-height="true">
+ </tb-js-func>
+ </ng-form>
+ </div>
+ </div>
+ <div id="bottom_right_panel" class="tb-split tb-content">
+ <div class="tb-resize-container">
+ <div class="tb-editor-area-title-panel">
+ <label translate>rulenode.output</label>
+ </div>
+ <tb-json-content ng-model="vm.output"
+ label="{{ 'rulenode.output' | translate }}"
+ content-type="vm.types.contentType.JSON.value"
+ validate-content="false"
+ ng-readonly="true"
+ fill-height="true">
+ </tb-json-content>
+ </div>
+ </div>
+ </div>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <md-button ng-disabled="$root.loading" ng-click="vm.test()" class="md-raised md-primary">
+ {{ 'rulenode.test' | translate }}
+ </md-button>
+ <span flex></span>
+ <md-button ng-disabled="$root.loading || theForm.funcBodyForm.$invalid || !theForm.funcBodyForm.$dirty" type="submit" class="md-raised md-primary">
+ {{ 'action.save' | translate }}
+ </md-button>
+ <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/services/item-buffer.service.js 83(+81 -2)
diff --git a/ui/src/app/services/item-buffer.service.js b/ui/src/app/services/item-buffer.service.js
index 9fce811..a9fe348 100644
--- a/ui/src/app/services/item-buffer.service.js
+++ b/ui/src/app/services/item-buffer.service.js
@@ -24,10 +24,11 @@ export default angular.module('thingsboard.itembuffer', [angularStorage])
.name;
/*@ngInject*/
-function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
+function ItemBuffer($q, bufferStore, types, utils, dashboardUtils, ruleChainService) {
const WIDGET_ITEM = "widget_item";
const WIDGET_REFERENCE = "widget_reference";
+ const RULE_NODES = "rule_nodes";
var service = {
prepareWidgetItem: prepareWidgetItem,
@@ -37,7 +38,10 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
canPasteWidgetReference: canPasteWidgetReference,
pasteWidget: pasteWidget,
pasteWidgetReference: pasteWidgetReference,
- addWidgetToDashboard: addWidgetToDashboard
+ addWidgetToDashboard: addWidgetToDashboard,
+ copyRuleNodes: copyRuleNodes,
+ hasRuleNodes: hasRuleNodes,
+ pasteRuleNodes: pasteRuleNodes
}
return service;
@@ -151,6 +155,81 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
};
}
+ function copyRuleNodes(nodes, connections) {
+ var ruleNodes = {
+ nodes: [],
+ connections: []
+ };
+ var top = -1, left = -1, bottom = -1, right = -1;
+ for (var i=0;i<nodes.length;i++) {
+ var origNode = nodes[i];
+ var node = {
+ additionalInfo: origNode.additionalInfo,
+ configuration: origNode.configuration,
+ debugMode: origNode.debugMode,
+ x: origNode.x,
+ y: origNode.y,
+ name: origNode.name,
+ componentClazz: origNode.component.clazz,
+ };
+ if (origNode.targetRuleChainId) {
+ node.targetRuleChainId = origNode.targetRuleChainId;
+ }
+ if (origNode.error) {
+ node.error = origNode.error;
+ }
+ ruleNodes.nodes.push(node);
+ if (i==0) {
+ top = node.y;
+ left = node.x;
+ bottom = node.y + 50;
+ right = node.x + 170;
+ } else {
+ top = Math.min(top, node.y);
+ left = Math.min(left, node.x);
+ bottom = Math.max(bottom, node.y + 50);
+ right = Math.max(right, node.x + 170);
+ }
+ }
+ ruleNodes.originX = left + (right-left)/2;
+ ruleNodes.originY = top + (bottom-top)/2;
+ for (i=0;i<connections.length;i++) {
+ var connection = connections[i];
+ ruleNodes.connections.push(connection);
+ }
+ bufferStore.set(RULE_NODES, angular.toJson(ruleNodes));
+ }
+
+ function hasRuleNodes() {
+ return bufferStore.get(RULE_NODES);
+ }
+
+ function pasteRuleNodes(x, y) {
+ var ruleNodesJson = bufferStore.get(RULE_NODES);
+ if (ruleNodesJson) {
+ var ruleNodes = angular.fromJson(ruleNodesJson);
+ var deltaX = x - ruleNodes.originX;
+ var deltaY = y - ruleNodes.originY;
+ for (var i=0;i<ruleNodes.nodes.length;i++) {
+ var node = ruleNodes.nodes[i];
+ var component = ruleChainService.getRuleNodeComponentByClazz(node.componentClazz);
+ if (component) {
+ delete node.componentClazz;
+ node.component = component;
+ node.nodeClass = types.ruleNodeType[component.type].nodeClass;
+ node.icon = types.ruleNodeType[component.type].icon;
+ node.connectors = [];
+ node.x = Math.round(node.x + deltaX);
+ node.y = Math.round(node.y + deltaY);
+ } else {
+ return null;
+ }
+ }
+ return ruleNodes;
+ }
+ return null;
+ }
+
function copyWidget(dashboard, sourceState, sourceLayout, widget) {
var widgetItem = prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem));
ui/src/app/services/menu.service.js 32(+32 -0)
diff --git a/ui/src/app/services/menu.service.js b/ui/src/app/services/menu.service.js
index 9dbddd9..5d97ea6 100644
--- a/ui/src/app/services/menu.service.js
+++ b/ui/src/app/services/menu.service.js
@@ -79,6 +79,12 @@ function Menu(userService, $state, $rootScope) {
icon: 'settings_ethernet'
},
{
+ name: 'rulechain.rulechains',
+ type: 'link',
+ state: 'home.ruleChains',
+ icon: 'settings_ethernet'
+ },
+ {
name: 'tenant.tenants',
type: 'link',
state: 'home.tenants',
@@ -128,6 +134,16 @@ function Menu(userService, $state, $rootScope) {
]
},
{
+ name: 'rulechain.management',
+ places: [
+ {
+ name: 'rulechain.rulechains',
+ icon: 'settings_ethernet',
+ state: 'home.ruleChains'
+ }
+ ]
+ },
+ {
name: 'tenant.management',
places: [
{
@@ -183,6 +199,12 @@ function Menu(userService, $state, $rootScope) {
icon: 'settings_ethernet'
},
{
+ name: 'rulechain.rulechains',
+ type: 'link',
+ state: 'home.ruleChains',
+ icon: 'settings_ethernet'
+ },
+ {
name: 'customer.customers',
type: 'link',
state: 'home.customers',
@@ -236,6 +258,16 @@ function Menu(userService, $state, $rootScope) {
]
},
{
+ name: 'rulechain.management',
+ places: [
+ {
+ name: 'rulechain.rulechains',
+ icon: 'settings_ethernet',
+ state: 'home.ruleChains'
+ }
+ ]
+ },
+ {
name: 'customer.management',
places: [
{
ui/src/scss/main.scss 18(+18 -0)
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 93ff320..6aa662c 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -203,6 +203,12 @@ md-sidenav {
* THINGSBOARD SPECIFIC
***********************/
+$swift-ease-out-duration: 0.4s !default;
+$swift-ease-out-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
+
+$input-label-float-offset: 6px !default;
+$input-label-float-scale: 0.75 !default;
+
label {
&.tb-title {
pointer-events: none;
@@ -213,6 +219,18 @@ label {
&.no-padding {
padding-bottom: 0px;
}
+ &.tb-required:after {
+ content: ' *';
+ font-size: 13px;
+ vertical-align: top;
+ color: rgba(0,0,0,0.54);
+ }
+ &.tb-error {
+ color: rgb(221,44,0);
+ &.tb-required:after {
+ color: rgb(221,44,0);
+ }
+ }
}
}