diff --git a/application/pom.xml b/application/pom.xml index 8449246059..6da40a9143 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -53,6 +53,14 @@ org.thingsboard extensions-api + + org.thingsboard.rule-engine + rule-engine-api + + + org.thingsboard.rule-engine + rule-engine-components + org.thingsboard extensions-core 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 aa8b10bdfd..5cdaede0ff 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 0043ed5bb0..ab91166fac 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/ThingsboardServerApplication.java b/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java index c3444d43f7..751bde6303 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/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 77953c9dc1..50d2530b53 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 b475277630..a75158f2dd 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 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 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 = msg.getPluginId(); - Optional ruleId = msg.getRuleId(); - if (pluginId.isPresent()) { - target = pluginManager.getOrCreatePluginActor(this.context(), pluginId.get()); - } else if (ruleId.isPresent()) { - Optional 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 861c405e08..87bc9926d4 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 { @@ -47,13 +47,18 @@ public class DeviceActor extends ContextAwareActor { this.processor = new DeviceActorMessageProcessor(systemContext, logger, deviceId); } + @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 21112bf4df..3644a491a4 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 265da386ff..88278f316d 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; @@ -40,6 +41,12 @@ public class PluginActor extends ComponentActor } @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 } @Override - public void stop() throws Exception { + public void stop(ActorContext context) throws Exception { onStop(); } @@ -98,7 +103,20 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor 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 if (pluginImpl != null) { pluginImpl.stop(trustedCtx); } - start(); + start(context); } } @@ -217,7 +235,7 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor 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 ce95ee4629..10f16ce8b1 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 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 06138a3308..182897025e 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 9290a8fe58..ba20013456 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; @@ -56,6 +57,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) { 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 db029fa13b..a187444ea5 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; @@ -47,6 +48,12 @@ public class RpcSessionActor extends ContextAwareActor { this.sessionId = sessionId; } + @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) { diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingContext.java deleted file mode 100644 index 8723a68516..0000000000 --- a/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingContext.java +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Copyright © 2016-2018 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.actors.rule; - -import akka.actor.ActorRef; -import org.thingsboard.server.common.msg.core.RuleEngineError; -import org.thingsboard.server.common.msg.core.RuleEngineErrorMsg; -import org.thingsboard.server.common.msg.device.ToDeviceActorMsg; -import org.thingsboard.server.common.msg.session.ToDeviceMsg; -import org.thingsboard.server.extensions.api.device.DeviceAttributes; -import org.thingsboard.server.extensions.api.device.DeviceMetaData; - -public class ChainProcessingContext { - - private final ChainProcessingMetaData md; - private final int index; - private final RuleEngineError error; - private ToDeviceMsg response; - - - public ChainProcessingContext(ChainProcessingMetaData md) { - super(); - this.md = md; - this.index = 0; - this.error = RuleEngineError.NO_RULES; - } - - private ChainProcessingContext(ChainProcessingContext other, int indexOffset, RuleEngineError error) { - super(); - this.md = other.md; - this.index = other.index + indexOffset; - this.error = error; - this.response = other.response; - - if (this.index < 0 || this.index >= this.md.chain.size()) { - throw new IllegalArgumentException("Can't apply offset " + indexOffset + " to the chain!"); - } - } - - public ActorRef getDeviceActor() { - return md.originator; - } - - public ActorRef getCurrentActor() { - return md.chain.getRuleActorMd(index).getActorRef(); - } - - public boolean hasNext() { - return (getChainLength() - 1) > index; - } - - public boolean isFailure() { - return (error != null && error.isCritical()) || (response != null && !response.isSuccess()); - } - - public ChainProcessingContext getNext() { - return new ChainProcessingContext(this, 1, this.error); - } - - public ChainProcessingContext withError(RuleEngineError error) { - if (error != null && (this.error == null || this.error.getPriority() < error.getPriority())) { - return new ChainProcessingContext(this, 0, error); - } else { - return this; - } - } - - public int getChainLength() { - return md.chain.size(); - } - - public ToDeviceActorMsg getInMsg() { - return md.inMsg; - } - - public DeviceMetaData getDeviceMetaData() { - return md.deviceMetaData; - } - - public String getDeviceName() { - return md.deviceMetaData.getDeviceName(); - } - - public String getDeviceType() { - return md.deviceMetaData.getDeviceType(); - } - - public DeviceAttributes getAttributes() { - return md.deviceMetaData.getDeviceAttributes(); - } - - public ToDeviceMsg getResponse() { - return response; - } - - public void mergeResponse(ToDeviceMsg response) { - // TODO add merge logic - this.response = response; - } - - public RuleEngineErrorMsg getError() { - return new RuleEngineErrorMsg(md.inMsg.getPayload().getMsgType(), error); - } -} diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingMetaData.java b/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingMetaData.java deleted file mode 100644 index 8080d4ecc9..0000000000 --- a/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingMetaData.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright © 2016-2018 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.actors.rule; - -import akka.actor.ActorRef; -import org.thingsboard.server.common.msg.device.ToDeviceActorMsg; -import org.thingsboard.server.extensions.api.device.DeviceMetaData; - -/** - * Immutable part of chain processing data; - * - * @author ashvayka - */ -public final class ChainProcessingMetaData { - - final RuleActorChain chain; - final ToDeviceActorMsg inMsg; - final ActorRef originator; - final DeviceMetaData deviceMetaData; - - public ChainProcessingMetaData(RuleActorChain chain, ToDeviceActorMsg inMsg, DeviceMetaData deviceMetaData, ActorRef originator) { - super(); - this.chain = chain; - this.inMsg = inMsg; - this.originator = originator; - this.deviceMetaData = deviceMetaData; - } -} diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/ComplexRuleActorChain.java b/application/src/main/java/org/thingsboard/server/actors/rule/ComplexRuleActorChain.java deleted file mode 100644 index 89d31bdc84..0000000000 --- a/application/src/main/java/org/thingsboard/server/actors/rule/ComplexRuleActorChain.java +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright © 2016-2018 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.actors.rule; - -public class ComplexRuleActorChain implements RuleActorChain { - - private final RuleActorChain systemChain; - private final RuleActorChain tenantChain; - - public ComplexRuleActorChain(RuleActorChain systemChain, RuleActorChain tenantChain) { - super(); - this.systemChain = systemChain; - this.tenantChain = tenantChain; - } - - @Override - public int size() { - return systemChain.size() + tenantChain.size(); - } - - @Override - public RuleActorMetaData getRuleActorMd(int index) { - if (index < systemChain.size()) { - return systemChain.getRuleActorMd(index); - } else { - return tenantChain.getRuleActorMd(index - systemChain.size()); - } - } - -} diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActor.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActor.java deleted file mode 100644 index 062da2a3bb..0000000000 --- a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActor.java +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Copyright © 2016-2018 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.actors.rule; - -import org.thingsboard.server.actors.ActorSystemContext; -import org.thingsboard.server.actors.service.ComponentActor; -import org.thingsboard.server.actors.service.ContextBasedCreator; -import org.thingsboard.server.actors.stats.StatsPersistTick; -import org.thingsboard.server.common.data.id.RuleId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.cluster.ClusterEventMsg; -import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; -import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg; - -public class RuleActor extends ComponentActor { - - private RuleActor(ActorSystemContext systemContext, TenantId tenantId, RuleId ruleId) { - super(systemContext, tenantId, ruleId); - setProcessor(new RuleActorMessageProcessor(tenantId, ruleId, systemContext, logger)); - } - - @Override - public void onReceive(Object msg) throws Exception { - logger.debug("[{}] Received message: {}", id, msg); - if (msg instanceof RuleProcessingMsg) { - try { - processor.onRuleProcessingMsg(context(), (RuleProcessingMsg) msg); - increaseMessagesProcessedCount(); - } catch (Exception e) { - logAndPersist("onDeviceMsg", e); - } - } else if (msg instanceof PluginToRuleMsg) { - try { - processor.onPluginMsg(context(), (PluginToRuleMsg) msg); - } catch (Exception e) { - logAndPersist("onPluginMsg", e); - } - } else if (msg instanceof ComponentLifecycleMsg) { - onComponentLifecycleMsg((ComponentLifecycleMsg) msg); - } else if (msg instanceof ClusterEventMsg) { - onClusterEventMsg((ClusterEventMsg) msg); - } else if (msg instanceof RuleToPluginTimeoutMsg) { - try { - processor.onTimeoutMsg(context(), (RuleToPluginTimeoutMsg) msg); - } catch (Exception e) { - logAndPersist("onTimeoutMsg", e); - } - } else if (msg instanceof StatsPersistTick) { - onStatsPersistTick(id); - } else { - logger.debug("[{}][{}] Unknown msg type.", tenantId, id, msg.getClass().getName()); - } - } - - public static class ActorCreator extends ContextBasedCreator { - private static final long serialVersionUID = 1L; - - private final TenantId tenantId; - private final RuleId ruleId; - - public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleId ruleId) { - super(context); - this.tenantId = tenantId; - this.ruleId = ruleId; - } - - @Override - public RuleActor create() throws Exception { - return new RuleActor(context, tenantId, ruleId); - } - } - - @Override - protected long getErrorPersistFrequency() { - return systemContext.getRuleErrorPersistFrequency(); - } -} diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java deleted file mode 100644 index 2ebebfca33..0000000000 --- a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java +++ /dev/null @@ -1,345 +0,0 @@ -/** - * Copyright © 2016-2018 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.actors.rule; - -import java.util.*; - -import com.fasterxml.jackson.core.JsonProcessingException; -import org.springframework.util.StringUtils; -import org.thingsboard.server.actors.ActorSystemContext; -import org.thingsboard.server.actors.plugin.RuleToPluginMsgWrapper; -import org.thingsboard.server.actors.shared.ComponentMsgProcessor; -import org.thingsboard.server.common.data.id.PluginId; -import org.thingsboard.server.common.data.id.RuleId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.plugin.ComponentLifecycleState; -import org.thingsboard.server.common.data.plugin.PluginMetaData; -import org.thingsboard.server.common.data.rule.RuleMetaData; -import org.thingsboard.server.common.msg.cluster.ClusterEventMsg; -import org.thingsboard.server.common.msg.core.BasicRequest; -import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse; -import org.thingsboard.server.common.msg.core.RuleEngineError; -import org.thingsboard.server.common.msg.device.ToDeviceActorMsg; -import org.thingsboard.server.common.msg.session.MsgType; -import org.thingsboard.server.common.msg.session.ToDeviceMsg; -import org.thingsboard.server.common.msg.session.ex.ProcessingTimeoutException; -import org.thingsboard.server.extensions.api.rules.*; -import org.thingsboard.server.extensions.api.plugins.PluginAction; -import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg; -import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg; - -import com.fasterxml.jackson.databind.JsonNode; - -import akka.actor.ActorContext; -import akka.actor.ActorRef; -import akka.event.LoggingAdapter; - -class RuleActorMessageProcessor extends ComponentMsgProcessor { - - private final RuleProcessingContext ruleCtx; - private final Map pendingMsgMap; - - private RuleMetaData ruleMd; - private ComponentLifecycleState state; - private List filters; - private RuleProcessor processor; - private PluginAction action; - - private TenantId pluginTenantId; - private PluginId pluginId; - - protected RuleActorMessageProcessor(TenantId tenantId, RuleId ruleId, ActorSystemContext systemContext, LoggingAdapter logger) { - super(systemContext, logger, tenantId, ruleId); - this.pendingMsgMap = new HashMap<>(); - this.ruleCtx = new RuleProcessingContext(systemContext, ruleId); - } - - @Override - public void start() throws Exception { - logger.info("[{}][{}] Starting rule actor.", entityId, tenantId); - ruleMd = systemContext.getRuleService().findRuleById(entityId); - if (ruleMd == null) { - throw new RuleInitializationException("Rule not found!"); - } - state = ruleMd.getState(); - if (state == ComponentLifecycleState.ACTIVE) { - logger.info("[{}] Rule is active. Going to initialize rule components.", entityId); - initComponent(); - } else { - logger.info("[{}] Rule is suspended. Skipping rule components initialization.", entityId); - } - - logger.info("[{}][{}] Started rule actor.", entityId, tenantId); - } - - @Override - public void stop() throws Exception { - onStop(); - } - - - private void initComponent() throws RuleException { - try { - if (!ruleMd.getFilters().isArray()) { - throw new RuntimeException("Filters are not array!"); - } - fetchPluginInfo(); - initFilters(); - initProcessor(); - initAction(); - } catch (RuntimeException e) { - throw new RuleInitializationException("Unknown runtime exception!", e); - } catch (InstantiationException e) { - throw new RuleInitializationException("No default constructor for rule implementation!", e); - } catch (IllegalAccessException e) { - throw new RuleInitializationException("Illegal Access Exception during rule initialization!", e); - } catch (ClassNotFoundException e) { - throw new RuleInitializationException("Rule Class not found!", e); - } catch (Exception e) { - throw new RuleException(e.getMessage(), e); - } - } - - private void initAction() throws Exception { - if (ruleMd.getAction() != null && !ruleMd.getAction().isNull()) { - action = initComponent(ruleMd.getAction()); - } - } - - private void initProcessor() throws Exception { - if (ruleMd.getProcessor() != null && !ruleMd.getProcessor().isNull()) { - processor = initComponent(ruleMd.getProcessor()); - } - } - - private void initFilters() throws Exception { - filters = new ArrayList<>(ruleMd.getFilters().size()); - for (int i = 0; i < ruleMd.getFilters().size(); i++) { - filters.add(initComponent(ruleMd.getFilters().get(i))); - } - } - - private void fetchPluginInfo() { - if (!StringUtils.isEmpty(ruleMd.getPluginToken())) { - PluginMetaData pluginMd = systemContext.getPluginService().findPluginByApiToken(ruleMd.getPluginToken()); - pluginTenantId = pluginMd.getTenantId(); - pluginId = pluginMd.getId(); - } - } - - protected void onRuleProcessingMsg(ActorContext context, RuleProcessingMsg msg) throws RuleException { - if (state != ComponentLifecycleState.ACTIVE) { - pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_ACTIVE_RULES); - return; - } - ChainProcessingContext chainCtx = msg.getCtx(); - ToDeviceActorMsg inMsg = chainCtx.getInMsg(); - - ruleCtx.update(inMsg, chainCtx.getDeviceMetaData()); - - logger.debug("[{}] Going to filter in msg: {}", entityId, inMsg); - for (RuleFilter filter : filters) { - if (!filter.filter(ruleCtx, inMsg)) { - logger.debug("[{}] In msg is NOT valid for processing by current rule: {}", entityId, inMsg); - pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_FILTERS_MATCHED); - return; - } - } - RuleProcessingMetaData inMsgMd; - if (processor != null) { - logger.debug("[{}] Going to process in msg: {}", entityId, inMsg); - inMsgMd = processor.process(ruleCtx, inMsg); - } else { - inMsgMd = new RuleProcessingMetaData(); - } - logger.debug("[{}] Going to convert in msg: {}", entityId, inMsg); - if (action != null) { - Optional> ruleToPluginMsgOptional = action.convert(ruleCtx, inMsg, inMsgMd); - if (ruleToPluginMsgOptional.isPresent()) { - RuleToPluginMsg ruleToPluginMsg = ruleToPluginMsgOptional.get(); - logger.debug("[{}] Device msg is converted to: {}", entityId, ruleToPluginMsg); - context.parent().tell(new RuleToPluginMsgWrapper(pluginTenantId, pluginId, tenantId, entityId, ruleToPluginMsg), context.self()); - if (action.isOneWayAction()) { - pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_TWO_WAY_ACTIONS); - return; - } else { - pendingMsgMap.put(ruleToPluginMsg.getUid(), msg); - scheduleMsgWithDelay(context, new RuleToPluginTimeoutMsg(ruleToPluginMsg.getUid()), systemContext.getPluginProcessingTimeout()); - return; - } - } - } - logger.debug("[{}] Nothing to send to plugin: {}", entityId, pluginId); - pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_TWO_WAY_ACTIONS); - } - - void onPluginMsg(ActorContext context, PluginToRuleMsg msg) { - RuleProcessingMsg pendingMsg = pendingMsgMap.remove(msg.getUid()); - if (pendingMsg != null) { - ChainProcessingContext ctx = pendingMsg.getCtx(); - Optional ruleResponseOptional = action.convert(msg); - if (ruleResponseOptional.isPresent()) { - ctx.mergeResponse(ruleResponseOptional.get()); - pushToNextRule(context, ctx, null); - } else { - pushToNextRule(context, ctx, RuleEngineError.NO_RESPONSE_FROM_ACTIONS); - } - } else { - logger.warning("[{}] Processing timeout detected: [{}]", entityId, msg.getUid()); - } - } - - void onTimeoutMsg(ActorContext context, RuleToPluginTimeoutMsg msg) { - RuleProcessingMsg pendingMsg = pendingMsgMap.remove(msg.getMsgId()); - if (pendingMsg != null) { - logger.debug("[{}] Processing timeout detected [{}]: {}", entityId, msg.getMsgId(), pendingMsg); - ChainProcessingContext ctx = pendingMsg.getCtx(); - pushToNextRule(context, ctx, RuleEngineError.PLUGIN_TIMEOUT); - } - } - - private void pushToNextRule(ActorContext context, ChainProcessingContext ctx, RuleEngineError error) { - if (error != null) { - ctx = ctx.withError(error); - } - if (ctx.isFailure()) { - logger.debug("[{}][{}] Forwarding processing chain to device actor due to failure.", ruleMd.getId(), ctx.getInMsg().getDeviceId()); - ctx.getDeviceActor().tell(new RulesProcessedMsg(ctx), ActorRef.noSender()); - } else if (!ctx.hasNext()) { - logger.debug("[{}][{}] Forwarding processing chain to device actor due to end of chain.", ruleMd.getId(), ctx.getInMsg().getDeviceId()); - ctx.getDeviceActor().tell(new RulesProcessedMsg(ctx), ActorRef.noSender()); - } else { - logger.debug("[{}][{}] Forwarding processing chain to next rule actor.", ruleMd.getId(), ctx.getInMsg().getDeviceId()); - ChainProcessingContext nextTask = ctx.getNext(); - nextTask.getCurrentActor().tell(new RuleProcessingMsg(nextTask), context.self()); - } - } - - @Override - public void onCreated(ActorContext context) { - logger.info("[{}] Going to process onCreated rule.", entityId); - } - - @Override - public void onUpdate(ActorContext context) throws RuleException { - RuleMetaData oldRuleMd = ruleMd; - ruleMd = systemContext.getRuleService().findRuleById(entityId); - logger.info("[{}] Rule configuration was updated from {} to {}.", entityId, oldRuleMd, ruleMd); - try { - fetchPluginInfo(); - if (filters == null || !Objects.equals(oldRuleMd.getFilters(), ruleMd.getFilters())) { - logger.info("[{}] Rule filters require restart due to json change from {} to {}.", - entityId, mapper.writeValueAsString(oldRuleMd.getFilters()), mapper.writeValueAsString(ruleMd.getFilters())); - stopFilters(); - initFilters(); - } - if (processor == null || !Objects.equals(oldRuleMd.getProcessor(), ruleMd.getProcessor())) { - logger.info("[{}] Rule processor require restart due to configuration change.", entityId); - stopProcessor(); - initProcessor(); - } - if (action == null || !Objects.equals(oldRuleMd.getAction(), ruleMd.getAction())) { - logger.info("[{}] Rule action require restart due to configuration change.", entityId); - stopAction(); - initAction(); - } - } catch (RuntimeException e) { - throw new RuleInitializationException("Unknown runtime exception!", e); - } catch (InstantiationException e) { - throw new RuleInitializationException("No default constructor for rule implementation!", e); - } catch (IllegalAccessException e) { - throw new RuleInitializationException("Illegal Access Exception during rule initialization!", e); - } catch (ClassNotFoundException e) { - throw new RuleInitializationException("Rule Class not found!", e); - } catch (JsonProcessingException e) { - throw new RuleInitializationException("Rule configuration is invalid!", e); - } catch (Exception e) { - throw new RuleInitializationException(e.getMessage(), e); - } - } - - @Override - public void onActivate(ActorContext context) throws Exception { - logger.info("[{}] Going to process onActivate rule.", entityId); - this.state = ComponentLifecycleState.ACTIVE; - if (filters != null) { - filters.forEach(RuleLifecycleComponent::resume); - if (processor != null) { - processor.resume(); - } else { - initProcessor(); - } - if (action != null) { - action.resume(); - } - logger.info("[{}] Rule resumed.", entityId); - } else { - start(); - } - } - - @Override - public void onSuspend(ActorContext context) { - logger.info("[{}] Going to process onSuspend rule.", entityId); - this.state = ComponentLifecycleState.SUSPENDED; - if (filters != null) { - filters.forEach(f -> f.suspend()); - } - if (processor != null) { - processor.suspend(); - } - if (action != null) { - action.suspend(); - } - } - - @Override - public void onStop(ActorContext context) { - logger.info("[{}] Going to process onStop rule.", entityId); - onStop(); - scheduleMsgWithDelay(context, new RuleTerminationMsg(entityId), systemContext.getRuleActorTerminationDelay()); - } - - private void onStop() { - this.state = ComponentLifecycleState.SUSPENDED; - stopFilters(); - stopProcessor(); - stopAction(); - } - - @Override - public void onClusterEventMsg(ClusterEventMsg msg) throws Exception { - //Do nothing - } - - private void stopAction() { - if (action != null) { - action.stop(); - } - } - - private void stopProcessor() { - if (processor != null) { - processor.stop(); - } - } - - private void stopFilters() { - if (filters != null) { - filters.forEach(f -> f.stop()); - } - } -} diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMetaData.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMetaData.java deleted file mode 100644 index df7adac396..0000000000 --- a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMetaData.java +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright © 2016-2018 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.actors.rule; - -import java.util.Comparator; - -import org.thingsboard.server.common.data.id.RuleId; - -import akka.actor.ActorRef; - -public class RuleActorMetaData { - - private final RuleId ruleId; - private final boolean systemRule; - private final int weight; - private final ActorRef actorRef; - - public static final Comparator RULE_ACTOR_MD_COMPARATOR = new Comparator() { - - @Override - public int compare(RuleActorMetaData r1, RuleActorMetaData r2) { - if (r1.isSystemRule() && !r2.isSystemRule()) { - return 1; - } else if (!r1.isSystemRule() && r2.isSystemRule()) { - return -1; - } else { - return Integer.compare(r2.getWeight(), r1.getWeight()); - } - } - }; - - public static RuleActorMetaData systemRule(RuleId ruleId, int weight, ActorRef actorRef) { - return new RuleActorMetaData(ruleId, true, weight, actorRef); - } - - public static RuleActorMetaData tenantRule(RuleId ruleId, int weight, ActorRef actorRef) { - return new RuleActorMetaData(ruleId, false, weight, actorRef); - } - - private RuleActorMetaData(RuleId ruleId, boolean systemRule, int weight, ActorRef actorRef) { - super(); - this.ruleId = ruleId; - this.systemRule = systemRule; - this.weight = weight; - this.actorRef = actorRef; - } - - public RuleId getRuleId() { - return ruleId; - } - - public boolean isSystemRule() { - return systemRule; - } - - public int getWeight() { - return weight; - } - - public ActorRef getActorRef() { - return actorRef; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((ruleId == null) ? 0 : ruleId.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - RuleActorMetaData other = (RuleActorMetaData) obj; - if (ruleId == null) { - if (other.ruleId != null) - return false; - } else if (!ruleId.equals(other.ruleId)) - return false; - return true; - } - - @Override - public String toString() { - return "RuleActorMetaData [ruleId=" + ruleId + ", systemRule=" + systemRule + ", weight=" + weight + ", actorRef=" + actorRef + "]"; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingContext.java deleted file mode 100644 index 179307bb05..0000000000 --- a/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingContext.java +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Copyright © 2016-2018 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.actors.rule; - -import com.google.common.util.concurrent.ListenableFuture; -import org.thingsboard.server.actors.ActorSystemContext; -import org.thingsboard.server.common.data.Event; -import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.alarm.AlarmId; -import org.thingsboard.server.common.data.id.*; -import org.thingsboard.server.dao.alarm.AlarmService; -import org.thingsboard.server.dao.event.EventService; -import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.common.msg.device.ToDeviceActorMsg; -import org.thingsboard.server.extensions.api.device.DeviceMetaData; -import org.thingsboard.server.extensions.api.rules.RuleContext; - -import java.util.Optional; -import java.util.concurrent.ExecutionException; - -public class RuleProcessingContext implements RuleContext { - - private final TimeseriesService tsService; - private final EventService eventService; - private final AlarmService alarmService; - private final RuleId ruleId; - private TenantId tenantId; - private CustomerId customerId; - private DeviceId deviceId; - private DeviceMetaData deviceMetaData; - - RuleProcessingContext(ActorSystemContext systemContext, RuleId ruleId) { - this.tsService = systemContext.getTsService(); - this.eventService = systemContext.getEventService(); - this.alarmService = systemContext.getAlarmService(); - this.ruleId = ruleId; - } - - void update(ToDeviceActorMsg toDeviceActorMsg, DeviceMetaData deviceMetaData) { - this.tenantId = toDeviceActorMsg.getTenantId(); - this.customerId = toDeviceActorMsg.getCustomerId(); - this.deviceId = toDeviceActorMsg.getDeviceId(); - this.deviceMetaData = deviceMetaData; - } - - @Override - public RuleId getRuleId() { - return ruleId; - } - - @Override - public DeviceMetaData getDeviceMetaData() { - return deviceMetaData; - } - - @Override - public Event save(Event event) { - checkEvent(event); - return eventService.save(event); - } - - @Override - public Optional saveIfNotExists(Event event) { - checkEvent(event); - return eventService.saveIfNotExists(event); - } - - @Override - public Optional findEvent(String eventType, String eventUid) { - return eventService.findEvent(tenantId, deviceId, eventType, eventUid); - } - - @Override - public Alarm createOrUpdateAlarm(Alarm alarm) { - alarm.setTenantId(tenantId); - return alarmService.createOrUpdateAlarm(alarm); - } - - public Optional findLatestAlarm(EntityId originator, String alarmType) { - try { - return Optional.ofNullable(alarmService.findLatestByOriginatorAndType(tenantId, originator, alarmType).get()); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException("Failed to lookup alarm!", e); - } - } - - @Override - public ListenableFuture clearAlarm(AlarmId alarmId, long clearTs) { - return alarmService.clearAlarm(alarmId, clearTs); - } - - private void checkEvent(Event event) { - if (event.getTenantId() == null) { - event.setTenantId(tenantId); - } else if (!tenantId.equals(event.getTenantId())) { - throw new IllegalArgumentException("Invalid Tenant id!"); - } - if (event.getEntityId() == null) { - event.setEntityId(deviceId); - } - } -} diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java new file mode 100644 index 0000000000..8ffd378da7 --- /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, ? 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 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 0000000000..f539e32a3c --- /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 { + + 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 { + 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 0000000000..d588a63637 --- /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 { + + private final ActorRef parent; + private final ActorRef self; + private final Map nodeActors; + private final Map> 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 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 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 existingNodes = ruleNodeList.stream().map(RuleNode::getId).collect(Collectors.toSet()); + List 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 ruleNodeList) { + nodeRoutes.clear(); + // Populating the routes map; + for (RuleNode ruleNode : ruleNodeList) { + List 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 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 0000000000..940bd5b0fb --- /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/common/message/src/main/java/org/thingsboard/server/common/msg/RuleMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java similarity index 54% rename from common/message/src/main/java/org/thingsboard/server/common/msg/RuleMsg.java rename to application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java index ee352ad722..e7d866c1eb 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/RuleMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java @@ -13,26 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.msg; +package org.thingsboard.server.actors.ruleChain; -import org.thingsboard.server.common.data.rule.Scope; -import org.thingsboard.server.common.data.rule.RuleType; -import org.thingsboard.server.common.msg.aware.RuleAwareMsg; +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; /** - * Message that is used to deliver some data to the rule instance. - * For example: aggregated statistics or command decoded from http request. - * - * @author ashvayka - * - * @param - payload + * Created by ashvayka on 19.03.18. */ -public interface RuleMsg extends RuleAwareMsg { +@Data +final class RuleChainToRuleNodeMsg implements TbActorMsg { + + private final TbContext ctx; + private final TbMsg msg; - Scope getRuleLevel(); - - RuleType getRuleType(); - - V getPayload(); - + @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 0000000000..f7ca0d8cc7 --- /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 { + + 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 { + 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 0000000000..ea857dbbb4 --- /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 { + + 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 0000000000..10fcc8b606 --- /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/rule/RuleProcessingMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeRelation.java similarity index 67% rename from application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingMsg.java rename to application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeRelation.java index 291a9a7cef..7861e5473c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeRelation.java @@ -13,19 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.actors.rule; +package org.thingsboard.server.actors.ruleChain; -public class RuleProcessingMsg { +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; - private final ChainProcessingContext ctx; +/** + * Created by ashvayka on 19.03.18. + */ - public RuleProcessingMsg(ChainProcessingContext ctx) { - super(); - this.ctx = ctx; - } +@Data +final class RuleNodeRelation { - public ChainProcessingContext getCtx() { - return ctx; - } + 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 0000000000..054284dc59 --- /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 0000000000..e6248f199f --- /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 0000000000..5c5af42207 --- /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 baae376c64..0be0385365 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 76b9be96b3..6aa68d3c5b 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(INITIAL_SESSION_MAP_SIZE); } + @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) { 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 73b221ffc5..e1313d20ef 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 18d32d9476..e25d3a7af9 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 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 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 0000000000..d4a1f34006 --- /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> { + + protected final ActorSystemContext systemContext; + protected final Map actors; + + public EntityActorsManager(ActorSystemContext systemContext) { + this.systemContext = systemContext; + this.actors = new HashMap<>(); + } + + protected abstract TenantId getTenantId(); + + protected abstract String getDispatcherName(); + + protected abstract Creator creator(T entityId); + + protected abstract PageDataIterable.FetchFunction 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 4f5871f585..3345e5fff5 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 { - protected final ActorSystemContext systemContext; protected final PluginService pluginService; - protected final Map pluginActors; public PluginManager(ActorSystemContext systemContext) { - this.systemContext = systemContext; + super(systemContext); this.pluginService = systemContext.getPluginService(); - this.pluginActors = new HashMap<>(); } - public void init(ActorContext context) { - PageDataIterable 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 creator(PluginId entityId){ + return new PluginActor.ActorCreator(systemContext, getTenantId(), entityId); } - abstract FetchFunction 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 0888e23f25..88c52a6043 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 getFetchPluginsFunction() { + protected FetchFunction 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 14ea2aa7f8..09115f0f7d 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 getFetchPluginsFunction() { + protected FetchFunction 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/rule/RuleManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java deleted file mode 100644 index 95d762afdf..0000000000 --- a/application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Copyright © 2016-2018 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.actors.shared.rule; - -import akka.actor.ActorContext; -import akka.actor.ActorRef; -import akka.actor.Props; -import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.actors.ActorSystemContext; -import org.thingsboard.server.actors.rule.RuleActor; -import org.thingsboard.server.actors.rule.RuleActorChain; -import org.thingsboard.server.actors.rule.RuleActorMetaData; -import org.thingsboard.server.actors.rule.SimpleRuleActorChain; -import org.thingsboard.server.actors.service.ContextAwareActor; -import org.thingsboard.server.common.data.id.RuleId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageDataIterable; -import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction; -import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; -import org.thingsboard.server.common.data.plugin.ComponentLifecycleState; -import org.thingsboard.server.common.data.rule.RuleMetaData; -import org.thingsboard.server.dao.rule.RuleService; - -import java.util.*; - -@Slf4j -public abstract class RuleManager { - - protected final ActorSystemContext systemContext; - protected final RuleService ruleService; - protected final Map ruleActors; - protected final TenantId tenantId; - - private Map ruleMap; - private RuleActorChain ruleChain; - - public RuleManager(ActorSystemContext systemContext, TenantId tenantId) { - this.systemContext = systemContext; - this.ruleService = systemContext.getRuleService(); - this.ruleActors = new HashMap<>(); - this.tenantId = tenantId; - } - - public void init(ActorContext context) { - doInit(context); - } - - private void doInit(ActorContext context) { - PageDataIterable ruleIterator = new PageDataIterable<>(getFetchRulesFunction(), - ContextAwareActor.ENTITY_PACK_LIMIT); - ruleMap = new HashMap<>(); - - for (RuleMetaData rule : ruleIterator) { - log.debug("[{}] Creating rule actor {}", rule.getId(), rule); - ActorRef ref = getOrCreateRuleActor(context, rule.getId()); - ruleMap.put(rule, RuleActorMetaData.systemRule(rule.getId(), rule.getWeight(), ref)); - log.debug("[{}] Rule actor created.", rule.getId()); - } - - refreshRuleChain(); - } - - public Optional update(ActorContext context, RuleId ruleId, ComponentLifecycleEvent event) { - if (ruleMap == null) { - doInit(context); - } - RuleMetaData rule; - if (event != ComponentLifecycleEvent.DELETED) { - rule = systemContext.getRuleService().findRuleById(ruleId); - } else { - rule = ruleMap.keySet().stream() - .filter(r -> r.getId().equals(ruleId)) - .peek(r -> r.setState(ComponentLifecycleState.SUSPENDED)) - .findFirst() - .orElse(null); - if (rule != null) { - ruleMap.remove(rule); - ruleActors.remove(ruleId); - } - } - if (rule != null) { - RuleActorMetaData actorMd = ruleMap.get(rule); - if (actorMd == null) { - ActorRef ref = getOrCreateRuleActor(context, rule.getId()); - actorMd = RuleActorMetaData.systemRule(rule.getId(), rule.getWeight(), ref); - ruleMap.put(rule, actorMd); - } - refreshRuleChain(); - return Optional.of(actorMd.getActorRef()); - } else { - log.warn("[{}] Can't process unknown rule!", ruleId); - return Optional.empty(); - } - } - - abstract FetchFunction getFetchRulesFunction(); - - abstract String getDispatcherName(); - - public ActorRef getOrCreateRuleActor(ActorContext context, RuleId ruleId) { - return ruleActors.computeIfAbsent(ruleId, rId -> - context.actorOf(Props.create(new RuleActor.ActorCreator(systemContext, tenantId, rId)) - .withDispatcher(getDispatcherName()), rId.toString())); - } - - public RuleActorChain getRuleChain(ActorContext context) { - if (ruleChain == null) { - doInit(context); - } - return ruleChain; - } - - private void refreshRuleChain() { - Set activeRuleSet = new HashSet<>(); - for (Map.Entry rule : ruleMap.entrySet()) { - if (rule.getKey().getState() == ComponentLifecycleState.ACTIVE) { - activeRuleSet.add(rule.getValue()); - } - } - ruleChain = new SimpleRuleActorChain(activeRuleSet); - } -} diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java new file mode 100644 index 0000000000..ff0c52ef45 --- /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 { + + 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 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/shared/rule/SystemRuleManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/SystemRuleChainManager.java similarity index 57% rename from application/src/main/java/org/thingsboard/server/actors/shared/rule/SystemRuleManager.java rename to application/src/main/java/org/thingsboard/server/actors/shared/rulechain/SystemRuleChainManager.java index d10731caed..a8bb069685 100644 --- a/application/src/main/java/org/thingsboard/server/actors/shared/rule/SystemRuleManager.java +++ b/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/SystemRuleChainManager.java @@ -13,28 +13,35 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.actors.shared.rule; +package org.thingsboard.server.actors.shared.rulechain; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.service.DefaultActorService; +import org.thingsboard.server.actors.shared.plugin.PluginManager; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction; -import org.thingsboard.server.common.data.rule.RuleMetaData; -import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.common.data.plugin.PluginMetaData; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.dao.plugin.BasePluginService; -public class SystemRuleManager extends RuleManager { +public class SystemRuleChainManager extends RuleChainManager { - public SystemRuleManager(ActorSystemContext systemContext) { - super(systemContext, new TenantId(ModelConstants.NULL_UUID)); + public SystemRuleChainManager(ActorSystemContext systemContext) { + super(systemContext); } @Override - FetchFunction getFetchRulesFunction() { - return ruleService::findSystemRules; + protected FetchFunction getFetchEntitiesFunction() { + return service::findSystemRuleChains; } @Override - String getDispatcherName() { + protected TenantId getTenantId() { + return BasePluginService.SYSTEM_TENANT; + } + + @Override + protected String getDispatcherName() { return DefaultActorService.SYSTEM_RULE_DISPATCHER_NAME; } } diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/rule/TenantRuleManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/TenantRuleChainManager.java similarity index 65% rename from application/src/main/java/org/thingsboard/server/actors/shared/rule/TenantRuleManager.java rename to application/src/main/java/org/thingsboard/server/actors/shared/rulechain/TenantRuleChainManager.java index e4d023ca90..731d8d8e6c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/shared/rule/TenantRuleManager.java +++ b/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/TenantRuleChainManager.java @@ -13,19 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.actors.shared.rule; +package org.thingsboard.server.actors.shared.rulechain; 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.FetchFunction; -import org.thingsboard.server.common.data.rule.RuleMetaData; +import org.thingsboard.server.common.data.rule.RuleChain; -public class TenantRuleManager extends RuleManager { - - public TenantRuleManager(ActorSystemContext systemContext, TenantId tenantId) { - super(systemContext, tenantId); +public class TenantRuleChainManager extends RuleChainManager { + + private final TenantId tenantId; + + public TenantRuleChainManager(ActorSystemContext systemContext, TenantId tenantId) { + super(systemContext); + this.tenantId = tenantId; } @Override @@ -36,13 +39,17 @@ public class TenantRuleManager extends RuleManager { } @Override - FetchFunction getFetchRulesFunction() { - return link -> ruleService.findTenantRules(tenantId, link); + protected TenantId getTenantId() { + return tenantId; } @Override - String getDispatcherName() { + protected String getDispatcherName() { return DefaultActorService.TENANT_RULE_DISPATCHER_NAME; } + @Override + protected FetchFunction getFetchEntitiesFunction() { + return link -> service.findTenantRuleChains(tenantId, link); + } } 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 ccc31cca29..8623370896 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 { @@ -35,6 +36,12 @@ public class StatsActor extends ContextAwareActor { super(context); } + @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); diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/RuleChainDeviceMsg.java b/application/src/main/java/org/thingsboard/server/actors/tenant/RuleChainDeviceMsg.java deleted file mode 100644 index a84e0b5fd4..0000000000 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/RuleChainDeviceMsg.java +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright © 2016-2018 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.actors.tenant; - -import org.thingsboard.server.actors.rule.RuleActorChain; -import org.thingsboard.server.common.msg.device.ToDeviceActorMsg; - -public class RuleChainDeviceMsg { - - private final ToDeviceActorMsg toDeviceActorMsg; - private final RuleActorChain ruleChain; - - public RuleChainDeviceMsg(ToDeviceActorMsg toDeviceActorMsg, RuleActorChain ruleChain) { - super(); - this.toDeviceActorMsg = toDeviceActorMsg; - this.ruleChain = ruleChain; - } - - public ToDeviceActorMsg getToDeviceActorMsg() { - return toDeviceActorMsg; - } - - public RuleActorChain getRuleChain() { - return ruleChain; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index b923fe15a3..d53c054414 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 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 = msg.getPluginId(); - Optional 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 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 2952529812..24c533c59d 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 a75ecb1353..2e5050a9ad 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 e9a6ba3816..5a43125bbf 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 1959f4e528..81bcf7e765 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 9b439133d3..0e348f997e 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 75bcf2ab15..e8685c74f9 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 ef38d802ec..96ff5162c7 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 83b304fb8b..29436c1500 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; @@ -84,6 +83,9 @@ public abstract class BaseController { @Autowired private ThingsboardErrorResponseHandler errorResponseHandler; + @Autowired + protected TenantService tenantService; + @Autowired protected CustomerService customerService; @@ -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 checkComponentDescriptorsByTypes(Set types) throws ThingsboardException { + try { + log.debug("[{}] Lookup component descriptors", types); + return componentDescriptorService.getComponents(types); + } catch (Exception e) { + throw handleException(e, false); + } + } + List 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 e63a443769..6313d615a3 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") @@ -51,6 +53,22 @@ public class ComponentDescriptorController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") + @RequestMapping(value = "/components", params = {"componentTypes"}, method = RequestMethod.GET) + @ResponseBody + public List getComponentDescriptorsByTypes(@RequestParam("componentTypes") String[] strComponentTypes) throws ThingsboardException { + checkArrayParameter("componentTypes", strComponentTypes); + try { + Set 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 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 b16470216a..7763f3c00a 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 d2952a16cb..4ec2bbab05 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 bceea54135..f97603e40f 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 getOfflineDevices(@RequestParam("contactType") DeviceStatusQuery.ContactType contactType, + @RequestParam("threshold") long threshold) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + ListenableFuture> 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 getOnlineDevices(@RequestParam("contactType") DeviceStatusQuery.ContactType contactType, + @RequestParam("threshold") long threshold) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + ListenableFuture> 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 03054df985..3a3b78b52f 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 331b15eb09..f67f113144 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 0000000000..fb1f3e7bee --- /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> { + + public HttpValidationCallback(DeferredResult response, FutureCallback> action) { + super(response, action); + } + +} 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 2c69248faa..3bc385d7b2 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 012e0770b3..48c9cd5342 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 @@ -53,6 +68,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 @@ -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); @@ -76,6 +109,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 @@ -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 metadata = objectMapper.convertValue(metadataJson, new TypeReference>() {}); + 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 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 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 e498c8fffc..4528d814a5 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 0000000000..80ddc2ba32 --- /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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 deleteAttributes(EntityId entityIdStr, String scope, String keysStr) throws ThingsboardException { + List 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> future = attributesService.removeAll(entityId, scope, keys); + Futures.addCallback(future, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List 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 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 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() { + @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 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 entries = new ArrayList<>(); + for (Map.Entry> 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() { + @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 result, SecurityUser user, EntityId entityId, String keys) { + ListenableFuture> 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 result, SecurityUser user, EntityId entityId, String scope, String keys) { + List keyList = toKeysList(keys); + FutureCallback> 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>> 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> future = mergeAllAttributesFutures(futures); + + Futures.addCallback(future, callback); + } + } + + private void getAttributeKeysCallback(@Nullable DeferredResult result, EntityId entityId, String scope) { + Futures.addCallback(attributesService.findAll(entityId, scope), getAttributeKeysToResponseCallback(result)); + } + + private void getAttributeKeysCallback(@Nullable DeferredResult result, EntityId entityId) { + List>> futures = new ArrayList<>(); + for (String scope : DataConstants.allScopes()) { + futures.add(attributesService.findAll(entityId, scope)); + } + + ListenableFuture> future = mergeAllAttributesFutures(futures); + + Futures.addCallback(future, getAttributeKeysToResponseCallback(result)); + } + + private FutureCallback> getTsKeysToResponseCallback(final DeferredResult response) { + return new FutureCallback>() { + @Override + public void onSuccess(List values) { + List 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> getAttributeKeysToResponseCallback(final DeferredResult response) { + return new FutureCallback>() { + + @Override + public void onSuccess(List attributes) { + List 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> getAttributeValuesToResponseCallback(final DeferredResult response, + final SecurityUser user, final String scope, + final EntityId entityId, final List keyList) { + return new FutureCallback>() { + @Override + public void onSuccess(List attributes) { + List 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> getTsKvListCallback(final DeferredResult response) { + return new FutureCallback>() { + @Override + public void onSuccess(List data) { + Map> 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 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 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 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> mergeAllAttributesFutures(List>> futures) { + return Futures.transform(Futures.successfulAsList(futures), + (Function>, ? extends List>) input -> { + List tmp = new ArrayList<>(); + if (input != null) { + input.forEach(tmp::addAll); + } + return tmp; + }, executor); + } + + private List toKeysList(String keys) { + List keyList = null; + if (!StringUtils.isEmpty(keys)) { + keyList = Arrays.asList(keys.split(",")); + } + return keyList; + } + + private DeferredResult getImmediateDeferredResult(String message, HttpStatus status) { + DeferredResult result = new DeferredResult<>(); + result.setResult(new ResponseEntity<>(message, status)); + return result; + } + + private List extractRequestAttributes(JsonNode jsonNode) { + long ts = System.currentTimeMillis(); + List 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 5acb4eb216..bf49074f91 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 2a1531ac99..52b207db74 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/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java index 44c7d94b3d..43ece89420 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/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java index 757f765dce..bf89f13a1c 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/plugin/PluginApiController.java b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java index 8e3cee4d5d..8d25db3702 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 processRequest( - @PathVariable("pluginToken") String pluginToken, - RequestEntity requestEntity, - HttpServletRequest request) - throws ThingsboardException { - log.debug("[{}] Going to process requst uri: {}", pluginToken, requestEntity.getUrl()); - DeferredResult result = new DeferredResult(); - 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 processRequest( +// @PathVariable("pluginToken") String pluginToken, +// RequestEntity requestEntity, +// HttpServletRequest request) +// throws ThingsboardException { +// log.debug("[{}] Going to process requst uri: {}", pluginToken, requestEntity.getUrl()); +// DeferredResult result = new DeferredResult(); +// 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/plugin/PluginWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java similarity index 69% rename from application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java rename to application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index 7f835ed5bf..42d6a20d56 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -15,57 +15,42 @@ */ package org.thingsboard.server.controller.plugin; -import java.io.IOException; -import java.net.URI; -import java.security.InvalidParameterException; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.BeanCreationNotAllowedException; -import org.springframework.context.annotation.Lazy; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.server.actors.service.ActorService; -import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.config.WebSocketConfiguration; -import org.thingsboard.server.extensions.api.plugins.PluginConstants; -import org.thingsboard.server.service.security.model.SecurityUser; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.plugin.PluginMetaData; -import org.thingsboard.server.dao.plugin.PluginService; -import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext; -import org.thingsboard.server.extensions.api.plugins.ws.BasicPluginWebsocketSessionRef; -import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef; -import org.thingsboard.server.extensions.api.plugins.ws.SessionEvent; -import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg; -import org.thingsboard.server.extensions.api.plugins.ws.msg.SessionEventPluginWebSocketMsg; -import org.thingsboard.server.extensions.api.plugins.ws.msg.TextPluginWebSocketMsg; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; +import org.thingsboard.server.config.WebSocketConfiguration; +import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef; +import org.thingsboard.server.extensions.api.plugins.ws.SessionEvent; +import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg; +import org.thingsboard.server.extensions.api.plugins.ws.msg.TextPluginWebSocketMsg; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.telemetry.TelemetryWebSocketMsgEndpoint; +import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; +import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; + +import java.io.IOException; +import java.net.URI; +import java.security.InvalidParameterException; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; @Service @Slf4j -public class PluginWebSocketHandler extends TextWebSocketHandler implements PluginWebSocketMsgEndpoint { +public class TbWebSocketHandler extends TextWebSocketHandler implements PluginWebSocketMsgEndpoint, TelemetryWebSocketMsgEndpoint { private static final ConcurrentMap internalSessionMap = new ConcurrentHashMap<>(); private static final ConcurrentMap externalSessionMap = new ConcurrentHashMap<>(); @Autowired @Lazy - private ActorService actorService; - - @Autowired - @Lazy - private PluginService pluginService; + private TelemetryWebSocketService webSocketService; @Override public void handleTextMessage(WebSocketSession session, TextMessage message) { @@ -73,7 +58,7 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug log.info("[{}] Processing {}", session.getId(), message); SessionMetaData sessionMd = internalSessionMap.get(session.getId()); if (sessionMd != null) { - actorService.process(new TextPluginWebSocketMsg(sessionMd.sessionRef, message.getPayload())); + webSocketService.handleWebSocketMsg(sessionMd.sessionRef, message.getPayload()); } else { log.warn("[{}] Failed to find session", session.getId()); session.close(CloseStatus.SERVER_ERROR.withReason("Session not found!")); @@ -88,11 +73,11 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug super.afterConnectionEstablished(session); try { String internalSessionId = session.getId(); - PluginWebsocketSessionRef sessionRef = toRef(session); + TelemetryWebSocketSessionRef sessionRef = toRef(session); String externalSessionId = sessionRef.getSessionId(); internalSessionMap.put(internalSessionId, new SessionMetaData(session, sessionRef)); externalSessionMap.put(externalSessionId, internalSessionId); - actorService.process(new SessionEventPluginWebSocketMsg(sessionRef, SessionEvent.onEstablished())); + processInWebSocketService(sessionRef, SessionEvent.onEstablished()); log.info("[{}][{}] Session is started", externalSessionId, session.getId()); } catch (InvalidParameterException e) { log.warn("[[{}] Failed to start session", session.getId(), e); @@ -108,7 +93,7 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug super.handleTransportError(session, tError); SessionMetaData sessionMd = internalSessionMap.get(session.getId()); if (sessionMd != null) { - processInActorService(new SessionEventPluginWebSocketMsg(sessionMd.sessionRef, SessionEvent.onError(tError))); + processInWebSocketService(sessionMd.sessionRef, SessionEvent.onError(tError)); } else { log.warn("[{}] Failed to find session", session.getId()); } @@ -121,20 +106,20 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug SessionMetaData sessionMd = internalSessionMap.remove(session.getId()); if (sessionMd != null) { externalSessionMap.remove(sessionMd.sessionRef.getSessionId()); - processInActorService(new SessionEventPluginWebSocketMsg(sessionMd.sessionRef, SessionEvent.onClosed())); + processInWebSocketService(sessionMd.sessionRef, SessionEvent.onClosed()); } log.info("[{}] Session is closed", session.getId()); } - private void processInActorService(SessionEventPluginWebSocketMsg msg) { + private void processInWebSocketService(TelemetryWebSocketSessionRef sessionRef, SessionEvent event) { try { - actorService.process(msg); + webSocketService.handleWebSocketSessionEvent(sessionRef, event); } catch (BeanCreationNotAllowedException e) { - log.warn("[{}] Failed to close session due to possible shutdown state", msg.getSessionRef().getSessionId()); + log.warn("[{}] Failed to close session due to possible shutdown state", sessionRef.getSessionId()); } } - private PluginWebsocketSessionRef toRef(WebSocketSession session) throws IOException { + private TelemetryWebSocketSessionRef toRef(WebSocketSession session) throws IOException { URI sessionUri = session.getUri(); String path = sessionUri.getPath(); path = path.substring(WebSocketConfiguration.WS_PLUGIN_PREFIX.length()); @@ -142,39 +127,63 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug throw new IllegalArgumentException("URL should contain plugin token!"); } String[] pathElements = path.split("/"); - String pluginToken = pathElements[0]; - // TODO: cache - PluginMetaData pluginMd = pluginService.findPluginByApiToken(pluginToken); - if (pluginMd == null) { + String serviceToken = pathElements[0]; + if (!"telemetry".equalsIgnoreCase(serviceToken)) { throw new InvalidParameterException("Can't find plugin with specified token!"); } else { SecurityUser currentUser = (SecurityUser) session.getAttributes().get(WebSocketConfiguration.WS_SECURITY_USER_ATTRIBUTE); - TenantId tenantId = currentUser.getTenantId(); - CustomerId customerId = currentUser.getCustomerId(); - if (PluginApiController.validatePluginAccess(pluginMd, tenantId, customerId)) { - UserId userId = currentUser.getId(); - String userName = currentUser.getName(); - PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(), tenantId, - currentUser.getCustomerId(), userId, userName); - return new BasicPluginWebsocketSessionRef(UUID.randomUUID().toString(), securityCtx, session.getUri(), session.getAttributes(), - session.getLocalAddress(), session.getRemoteAddress()); - } else { - throw new SecurityException("Current user is not allowed to use this plugin!"); - } + return new TelemetryWebSocketSessionRef(UUID.randomUUID().toString(), currentUser, session.getLocalAddress(), session.getRemoteAddress()); } } private static class SessionMetaData { private final WebSocketSession session; - private final PluginWebsocketSessionRef sessionRef; + private final TelemetryWebSocketSessionRef sessionRef; - public SessionMetaData(WebSocketSession session, PluginWebsocketSessionRef sessionRef) { + public SessionMetaData(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef) { super(); this.session = session; this.sessionRef = sessionRef; } } + @Override + public void send(TelemetryWebSocketSessionRef sessionRef, String msg) throws IOException { + String externalId = sessionRef.getSessionId(); + log.debug("[{}] Processing {}", externalId, msg); + String internalId = externalSessionMap.get(externalId); + if (internalId != null) { + SessionMetaData sessionMd = internalSessionMap.get(internalId); + if (sessionMd != null) { + synchronized (sessionMd) { + sessionMd.session.sendMessage(new TextMessage(msg)); + } + } else { + log.warn("[{}][{}] Failed to find session by internal id", externalId, internalId); + } + } else { + log.warn("[{}] Failed to find session by external id", externalId); + } + } + + @Override + public void close(TelemetryWebSocketSessionRef sessionRef) throws IOException { + String externalId = sessionRef.getSessionId(); + log.debug("[{}] Processing close request", externalId); + String internalId = externalSessionMap.get(externalId); + if (internalId != null) { + SessionMetaData sessionMd = internalSessionMap.get(internalId); + if (sessionMd != null) { + sessionMd.session.close(CloseStatus.NORMAL); + } else { + log.warn("[{}][{}] Failed to find session by internal id", externalId, internalId); + } + } else { + log.warn("[{}] Failed to find session by external id", externalId); + } + } + + //TODO: remove @Override public void send(PluginWebsocketMsg wsMsg) throws IOException { PluginWebsocketSessionRef sessionRef = wsMsg.getSessionRef(); @@ -196,6 +205,7 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug } } + //TODO: remove @Override public void close(PluginWebsocketSessionRef sessionRef) throws IOException { String externalId = sessionRef.getSessionId(); @@ -212,5 +222,4 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug log.warn("[{}] Failed to find session by external id", externalId); } } - } 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 3b897d6bb3..031073fa97 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 c70c5612c3..63fe17ac4f 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 0a6081d8da..9377756b86 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 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 annotation) { List components = persist(getBeanDefinitions(annotation), type); componentsMap.put(type, components); @@ -79,8 +101,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe private List persist(Set filterDefs, ComponentType type) { List 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 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 getBeanDefinitions(Class 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 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 getComponents(Set types) { + List 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 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 ea27e60646..7a15a1b253 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 getComponents(ComponentType type); + List getComponents(Set types); + Optional getComponent(String clazz); List 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 0000000000..91ef9de66f --- /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 ListenableFuture executeAsync(Callable 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 0000000000..2eec3ed234 --- /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 1ef805f3c9..908faf2315 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 25b911c2d5..818c935595 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 model = new HashMap(); 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 model = new HashMap(); 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 model = new HashMap(); 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 model = new HashMap(); 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 model = new HashMap(); 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 0000000000..e8caab62a6 --- /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 0000000000..c60f45550f --- /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 0000000000..d68f6fed29 --- /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 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>() { + }); + } + 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 executeSwitch(TbMsg msg) throws ScriptException { + JsonNode result = executeScript(msg); + if (result.isTextual()) { + return Collections.singleton(result.asText()); + } else if (result.isArray()) { + Set 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 0000000000..c1f3688e92 --- /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 validateEntityAndCallback(SecurityUser currentUser, String entityType, String entityIdStr, + BiConsumer, EntityId> onSuccess) throws ThingsboardException { + return validateEntityAndCallback(currentUser, entityType, entityIdStr, onSuccess, (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR)); + } + + public DeferredResult validateEntityAndCallback(SecurityUser currentUser, String entityType, String entityIdStr, + BiConsumer, EntityId> onSuccess, + BiConsumer, Throwable> onFailure) throws ThingsboardException { + return validateEntityAndCallback(currentUser, EntityIdFactory.getByTypeAndId(entityType, entityIdStr), + onSuccess, onFailure); + } + + public DeferredResult validateEntityAndCallback(SecurityUser currentUser, EntityId entityId, + BiConsumer, EntityId> onSuccess) throws ThingsboardException { + return validateEntityAndCallback(currentUser, entityId, onSuccess, (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR)); + } + + public DeferredResult validateEntityAndCallback(SecurityUser currentUser, EntityId entityId, + BiConsumer, EntityId> onSuccess, + BiConsumer, Throwable> onFailure) throws ThingsboardException { + + final DeferredResult response = new DeferredResult<>(); + + validate(currentUser, entityId, new HttpValidationCallback(response, + new FutureCallback>() { + @Override + public void onSuccess(@Nullable DeferredResult result) { + onSuccess.accept(response, entityId); + } + + @Override + public void onFailure(Throwable t) { + onFailure.accept(response, t); + } + })); + + return response; + } + + public void validate(SecurityUser currentUser, EntityId entityId, FutureCallback 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 callback) { + if (currentUser.isSystemAdmin()) { + callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); + } else { + ListenableFuture 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 void validateAsset(final SecurityUser currentUser, EntityId entityId, FutureCallback callback) { + if (currentUser.isSystemAdmin()) { + callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); + } else { + ListenableFuture 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 void validateRuleChain(final SecurityUser currentUser, EntityId entityId, FutureCallback callback) { + if (currentUser.isCustomerUser()) { + callback.onSuccess(ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); + } else { + ListenableFuture 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 void validateCustomer(final SecurityUser currentUser, EntityId entityId, FutureCallback callback) { + if (currentUser.isSystemAdmin()) { + callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); + } else { + ListenableFuture 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 void validateTenant(final SecurityUser currentUser, EntityId entityId, FutureCallback 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 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 FutureCallback getCallback(FutureCallback callback, Function transformer) { + return new FutureCallback() { + @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 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/ValidationCallback.java b/application/src/main/java/org/thingsboard/server/service/security/ValidationCallback.java new file mode 100644 index 0000000000..2b91c60a3e --- /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 implements FutureCallback { + + private final T response; + private final FutureCallback action; + + public ValidationCallback(T response, FutureCallback 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/security/model/SecurityUser.java b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java index f4a28a03a6..91ee7bf6d3 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/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java new file mode 100644 index 0000000000..58bbec550e --- /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> subscriptionsByEntityId = new HashMap<>(); + + private final Map> subscriptionsByWsSessionId = new HashMap<>(); + + @Override + public void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub) { + Optional 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 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 ts, FutureCallback callback) { + saveAndNotify(entityId, ts, 0L, callback); + } + + @Override + public void saveAndNotify(EntityId entityId, List ts, long ttl, FutureCallback callback) { + ListenableFuture> saveFuture = tsService.save(entityId, ts, ttl); + addMainCallback(saveFuture, callback); + addWsCallback(saveFuture, success -> onTimeseriesUpdate(entityId, ts)); + } + + @Override + public void saveAndNotify(EntityId entityId, String scope, List attributes, FutureCallback callback) { + ListenableFuture> saveFuture = attrService.save(entityId, scope, attributes); + addMainCallback(saveFuture, callback); + addWsCallback(saveFuture, success -> onAttributesUpdate(entityId, scope, attributes)); + } + + private void onAttributesUpdate(EntityId entityId, String scope, List attributes) { + Optional 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 ts) { + Optional 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 attributes) { + onLocalSubUpdate(entityId, s -> TelemetryFeature.ATTRIBUTES == s.getType() && (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope())), s -> { + List 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 ts) { + onLocalSubUpdate(entityId, s -> TelemetryFeature.TIMESERIES == s.getType(), s -> { + List 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 filter, Function> f) { + Set deviceSubscriptions = subscriptionsByEntityId.get(entityId); + if (deviceSubscriptions != null) { + deviceSubscriptions.stream().filter(filter).forEach(s -> { + String sessionId = s.getWsSessionId(); + List 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 deviceSubscriptions = subscriptionsByEntityId.computeIfAbsent(entityId, k -> new HashSet<>()); + deviceSubscriptions.add(subscription); + Map 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 sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId); + if (sessionSubscriptions != null) { + int sessionSubscriptionSize = sessionSubscriptions.size(); + + for (Subscription subscription : sessionSubscriptions.values()) { + EntityId entityId = subscription.getEntityId(); + Set 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 sessionSubscriptions) { + Set 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 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 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> saveFuture, final FutureCallback callback) { + Futures.addCallback(saveFuture, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List result) { + callback.onSuccess(null); + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(t); + } + }, tsCallBackExecutor); + } + + private void addWsCallback(ListenableFuture> saveFuture, Consumer callback) { + Futures.addCallback(saveFuture, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List 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 0000000000..57f3876366 --- /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 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> keysOptional = getKeys(cmd); + if (keysOptional.isPresent()) { + List 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 keys) { + FutureCallback> callback = new FutureCallback>() { + @Override + public void onSuccess(List data) { + List attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); + sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData)); + + Map 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 keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); + List queries = keys.stream().map(key -> new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))) + .collect(Collectors.toList()); + + FutureCallback> callback = new FutureCallback>() { + @Override + public void onSuccess(List 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> callback = new FutureCallback>() { + @Override + public void onSuccess(List data) { + List attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); + sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData)); + + Map 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> 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 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 queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, cmd.getInterval(), + getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList()); + + final FutureCallback> 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 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> 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> callback = new FutureCallback>() { + @Override + public void onSuccess(List data) { + sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data)); + Map 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> getSubscriptionCallback(final TelemetryWebSocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final EntityId entityId, final long startTs, final List keys) { + return new FutureCallback>() { + @Override + public void onSuccess(List data) { + sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data)); + + Map 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> getKeys(TelemetryPluginCmd cmd) { + if (!StringUtils.isEmpty(cmd.getKeys())) { + Set keys = new HashSet<>(); + Collections.addAll(keys, cmd.getKeys().split(",")); + return Optional.of(keys); + } else { + return Optional.empty(); + } + } + + private ListenableFuture> mergeAllAttributesFutures(List>> futures) { + return Futures.transform(Futures.successfulAsList(futures), + (Function>, ? extends List>) input -> { + List tmp = new ArrayList<>(); + if (input != null) { + input.forEach(tmp::addAll); + } + return tmp; + }, executor); + } + + private FutureCallback getAttributesFetchCallback(final EntityId entityId, final List keys, final FutureCallback> callback) { + return new FutureCallback() { + @Override + public void onSuccess(@Nullable ValidationResult result) { + List>> futures = new ArrayList<>(); + for (String scope : DataConstants.allScopes()) { + futures.add(attributesService.find(entityId, scope, keys)); + } + + ListenableFuture> future = mergeAllAttributesFutures(futures); + Futures.addCallback(future, callback); + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(t); + } + }; + } + + private FutureCallback getAttributesFetchCallback(final EntityId entityId, final String scope, final List keys, final FutureCallback> callback) { + return new FutureCallback() { + @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 FutureCallback getAttributesFetchCallback(final EntityId entityId, final FutureCallback> callback) { + return new FutureCallback() { + @Override + public void onSuccess(@Nullable ValidationResult result) { + List>> futures = new ArrayList<>(); + for (String scope : DataConstants.allScopes()) { + futures.add(attributesService.findAll(entityId, scope)); + } + + ListenableFuture> future = mergeAllAttributesFutures(futures); + Futures.addCallback(future, callback); + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(t); + } + }; + } + + private FutureCallback getAttributesFetchCallback(final EntityId entityId, final String scope, final FutureCallback> callback) { + return new FutureCallback() { + @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 on(Consumer success, Consumer failure) { + return new FutureCallback() { + @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 0000000000..923d06bc7f --- /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/TelemetryWebSocketMsgEndpoint.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java new file mode 100644 index 0000000000..00fb80a522 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 java.io.IOException; + +/** + * Created by ashvayka on 27.03.18. + */ +public interface TelemetryWebSocketMsgEndpoint { + + void send(TelemetryWebSocketSessionRef sessionRef, String msg) throws IOException; + + void close(TelemetryWebSocketSessionRef sessionRef) throws IOException; + +} 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 0000000000..be6fc56b3a --- /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 0000000000..53438c533b --- /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 0000000000..5d4630cb21 --- /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 0000000000..dd15ed33bd --- /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/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 1c842c8a1b..a9e47de87e 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 b92e464a6e..3ec4dc815b 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 0000000000..93fe76752e --- /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 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>() { + }, pageLink, entityId.getEntityType(), entityId.getId(), DataConstants.DEBUG_RULE_NODE, tenantId.getId()); + } +} 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 0000000000..65b4293490 --- /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/rules/flow/AbstractRuleEngineFlowIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java new file mode 100644 index 0000000000..f88eb24336 --- /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 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/flow/RuleEngineFlowSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/flow/RuleEngineFlowSqlIntegrationTest.java new file mode 100644 index 0000000000..18a164ef33 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/rules/flow/RuleEngineFlowSqlIntegrationTest.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.flow; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcIntegrationTest; + +/** + * Created by Valerii Sosliuk on 8/22/2017. + */ +@DaoSqlTest +public class RuleEngineFlowSqlIntegrationTest extends AbstractRuleEngineFlowIntegrationTest { +} 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 0000000000..22d79f090f --- /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 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 0000000000..004958b2ea --- /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/service/mail/TestMailService.java b/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java index ed3750d6ca..ba2bb65acd 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 0000000000..e6a48e2838 --- /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 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 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 0000000000..5a09a68b43 --- /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 devices = doGetTyped("/api/device/offline?contactType=CONNECT&threshold=700", new TypeReference>() { + }); + + 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 devices = doGetTyped("/api/device/offline?contactType=UPLOAD&threshold=700", new TypeReference>() { + }); + + 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 devices = doGetTyped("/api/device/online?contactType=CONNECT&threshold=700", new TypeReference>() { + }); + + 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 devices = doGetTyped("/api/device/online?contactType=UPLOAD&threshold=700", new TypeReference>() { + }); + + 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 4fa61621bc..c3e87c27ff 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/dao/src/test/java/org/thingsboard/server/dao/service/rule/nosql/RuleServiceNoSqlTest.java b/application/src/test/java/org/thingsboard/server/system/nosql/DeviceOfflineNoSqlTest.java similarity index 78% rename from dao/src/test/java/org/thingsboard/server/dao/service/rule/nosql/RuleServiceNoSqlTest.java rename to application/src/test/java/org/thingsboard/server/system/nosql/DeviceOfflineNoSqlTest.java index 7ff9066499..f25ec279a4 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/rule/nosql/RuleServiceNoSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/system/nosql/DeviceOfflineNoSqlTest.java @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.service.rule.nosql; +package org.thingsboard.server.system.nosql; import org.thingsboard.server.dao.service.DaoNoSqlTest; -import org.thingsboard.server.dao.service.rule.BaseRuleServiceTest; +import org.thingsboard.server.system.BaseDeviceOfflineTest; @DaoNoSqlTest -public class RuleServiceNoSqlTest extends BaseRuleServiceTest { +public class DeviceOfflineNoSqlTest extends BaseDeviceOfflineTest { } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/rule/sql/RuleServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/system/sql/DeviceOfflineSqlTest.java similarity index 78% rename from dao/src/test/java/org/thingsboard/server/dao/service/rule/sql/RuleServiceSqlTest.java rename to application/src/test/java/org/thingsboard/server/system/sql/DeviceOfflineSqlTest.java index da3d4a0b7b..a9617bcdb4 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/rule/sql/RuleServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/system/sql/DeviceOfflineSqlTest.java @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.service.rule.sql; +package org.thingsboard.server.system.sql; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.dao.service.rule.BaseRuleServiceTest; +import org.thingsboard.server.system.BaseDeviceOfflineTest; @DaoSqlTest -public class RuleServiceSqlTest extends BaseRuleServiceTest { +public class DeviceOfflineSqlTest extends BaseDeviceOfflineTest { } 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 03115a9940..078c97b1c0 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 implements HasName { +public class Customer extends ContactBased 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 a776d7b6ce..7d4e4807c9 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 13fa011d9d..6d257fc905 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 implements HasName { +public class Device extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId { private static final long serialVersionUID = 2807343040519543363L; @@ -31,6 +31,8 @@ public class Device extends SearchTextBasedWithAdditionalInfo 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 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 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/HasCustomerId.java b/common/data/src/main/java/org/thingsboard/server/common/data/HasCustomerId.java new file mode 100644 index 0000000000..e89eba8bbb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HasCustomerId.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import org.thingsboard.server.common.data.id.CustomerId; + +public interface HasCustomerId { + + CustomerId getCustomerId(); +} diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/HasTenantId.java similarity index 79% rename from extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionType.java rename to common/data/src/main/java/org/thingsboard/server/common/data/HasTenantId.java index 4a3b1e3447..ebba003cc8 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HasTenantId.java @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.extensions.core.plugin.telemetry.sub; +package org.thingsboard.server.common.data; -/** - * @author Andrew Shvayka - */ -public enum SubscriptionType { - ATTRIBUTES, TIMESERIES +import org.thingsboard.server.common.data.id.TenantId; + +public interface HasTenantId { + + TenantId getTenantId(); } 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 c893d644d9..15c52dc49c 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 implements HasName { +public class User extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId { private static final long serialVersionUID = 8250339805336035966L; @@ -138,4 +140,18 @@ public class User extends SearchTextBasedWithAdditionalInfo 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/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 70f5042a75..125406c4a1 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 implements HasName { +public class Alarm extends BaseData 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 cc3c111724..c7b246c1ee 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 implements HasName { +public class Asset extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId { private static final long serialVersionUID = 2807343040519543363L; diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleToPluginTimeoutMsg.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceStatusQuery.java similarity index 61% rename from application/src/main/java/org/thingsboard/server/actors/rule/RuleToPluginTimeoutMsg.java rename to common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceStatusQuery.java index 0258bce130..0d0dad1117 100644 --- a/application/src/main/java/org/thingsboard/server/actors/rule/RuleToPluginTimeoutMsg.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceStatusQuery.java @@ -13,24 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.actors.rule; +package org.thingsboard.server.common.data.device; -import java.io.Serializable; -import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.ToString; -public class RuleToPluginTimeoutMsg implements Serializable { +@Data +@AllArgsConstructor +@ToString +public class DeviceStatusQuery { - private static final long serialVersionUID = 1L; + private Status status; + private ContactType contactType; + private long threshold; - private final UUID msgId; - public RuleToPluginTimeoutMsg(UUID msgId) { - super(); - this.msgId = msgId; + public enum Status { + ONLINE, OFFLINE } - public UUID getMsgId() { - return msgId; + public enum ContactType { + CONNECT, UPLOAD } } diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorCode.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java similarity index 95% rename from application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorCode.java rename to common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java index e78d4f208a..5b7cfb9864 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorCode.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.exception; +package org.thingsboard.server.common.data.exception; import com.fasterxml.jackson.annotation.JsonValue; diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardException.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardException.java similarity index 96% rename from application/src/main/java/org/thingsboard/server/exception/ThingsboardException.java rename to common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardException.java index fcbd65eb27..fe4bd502e0 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardException.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardException.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.exception; +package org.thingsboard.server.common.data.exception; public class ThingsboardException extends Exception { 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 76b3e336d8..31c1cdae90 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 34f8c3aed6..ffd78225c4 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> implements Iterable, Iterator { 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 45fb590ed6..a1030640cb 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 8576264d5b..4c33ffe65c 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 implements HasName { +public class PluginMetaData extends SearchTextBasedWithAdditionalInfo 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 0000000000..0c9fd5feb5 --- /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 e82c850b9d..218061adb1 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 implements HasName { +public class RuleChain extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId { private static final long serialVersionUID = -5656679015121935465L; @@ -37,6 +38,7 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo im private String name; private RuleNodeId firstRuleNodeId; private boolean root; + private boolean debugMode; private transient JsonNode configuration; @JsonIgnore private byte[] configurationBytes; diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleTerminationMsg.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java similarity index 61% rename from application/src/main/java/org/thingsboard/server/actors/rule/RuleTerminationMsg.java rename to common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java index 4458e5217c..a537fe4147 100644 --- a/application/src/main/java/org/thingsboard/server/actors/rule/RuleTerminationMsg.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java @@ -13,18 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.actors.rule; +package org.thingsboard.server.common.data.rule; -import org.thingsboard.server.actors.shared.ActorTerminationMsg; -import org.thingsboard.server.common.data.id.PluginId; -import org.thingsboard.server.common.data.id.RuleId; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import org.thingsboard.server.common.data.id.RuleChainId; /** - * @author Andrew Shvayka + * Created by ashvayka on 21.03.18. */ -public class RuleTerminationMsg extends ActorTerminationMsg { - - public RuleTerminationMsg(RuleId id) { - super(id); - } +@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 af141d6142..7ecd6df3cc 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 98adeb7742..953e5eb6bc 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 implements HasName { +public class RuleMetaData extends SearchTextBasedWithAdditionalInfo 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 d044000117..fbc1103dee 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 impl private String type; private String name; + private boolean debugMode; private transient JsonNode configuration; @JsonIgnore private byte[] configurationBytes; 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 0000000000..f8f204403a --- /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/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorChain.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorMsg.java similarity index 81% rename from application/src/main/java/org/thingsboard/server/actors/rule/RuleActorChain.java rename to common/message/src/main/java/org/thingsboard/server/common/msg/TbActorMsg.java index 3f3bd36f65..c361c11c0e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorChain.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorMsg.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.actors.rule; +package org.thingsboard.server.common.msg; -public interface RuleActorChain { - - int size(); +/** + * Created by ashvayka on 15.03.18. + */ +public interface TbActorMsg { - RuleActorMetaData getRuleActorMd(int index); + MsgType getMsgType(); } 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 5163b6c39d..1c7de3bf10 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/TbMsgDataType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgDataType.java new file mode 100644 index 0000000000..2e367e9536 --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgDataType.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.common.msg; + +/** + * Created by ashvayka on 15.03.18. + */ +public enum TbMsgDataType { + + // Do not change ordering. We use ordinal to save some bytes on serialization + JSON, TEXT, BINARY; + +} 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 1bbc7929e2..4b7314c233 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 data = new ConcurrentHashMap<>(); + public TbMsgMetaData(Map 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 values() { + return new HashMap<>(data); + } + + public TbMsgMetaData copy() { + return new TbMsgMetaData(new ConcurrentHashMap<>(data)); + } } 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 ace51c03a7..87708a759f 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/plugin/ComponentLifecycleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java index d48c3feb10..c104281ffe 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 getPluginId() { - return Optional.ofNullable(pluginId); + return entityId.getEntityType() == EntityType.PLUGIN ? Optional.of((PluginId) entityId) : Optional.empty(); } public Optional getRuleId() { - return Optional.ofNullable(ruleId); + return entityId.getEntityType() == EntityType.RULE ? Optional.of((RuleId) entityId) : Optional.empty(); + } + + public Optional 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/application/src/main/java/org/thingsboard/server/actors/rule/SimpleRuleActorChain.java b/common/message/src/main/java/org/thingsboard/server/common/msg/system/ServiceToRuleEngineMsg.java similarity index 53% rename from application/src/main/java/org/thingsboard/server/actors/rule/SimpleRuleActorChain.java rename to common/message/src/main/java/org/thingsboard/server/common/msg/system/ServiceToRuleEngineMsg.java index 70a26e4430..0792b63c28 100644 --- a/application/src/main/java/org/thingsboard/server/actors/rule/SimpleRuleActorChain.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/system/ServiceToRuleEngineMsg.java @@ -13,27 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.actors.rule; +package org.thingsboard.server.common.msg.system; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; +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; -public class SimpleRuleActorChain implements RuleActorChain { - - private final List rules; - - public SimpleRuleActorChain(Set ruleSet) { - rules = new ArrayList<>(ruleSet); - rules.sort(RuleActorMetaData.RULE_ACTOR_MD_COMPARATOR); - } +/** + * Created by ashvayka on 15.03.18. + */ +@Data +public final class ServiceToRuleEngineMsg implements TbActorMsg { - public int size() { - return rules.size(); - } + private final TenantId tenantId; + private final TbMsg tbMsg; - public RuleActorMetaData getRuleActorMd(int index) { - return rules.get(index); + @Override + public MsgType getMsgType() { + return MsgType.SERVICE_TO_RULE_ENGINE_MSG; } - } diff --git a/common/message/src/main/proto/tbmsg.proto b/common/message/src/main/proto/tbmsg.proto index 90fa2bdbae..4ce1fb69bc 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 data = 1; +} message TbMsgProto { string id = 1; @@ -26,11 +29,8 @@ message TbMsgProto { string entityType = 3; string entityId = 4; - message TbMsgMetaDataProto { - map 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 8d254a08c6..3782ed22ed 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 4f923fe808..64ec718fdd 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>() { @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 932d6b9147..8ae9dc8ad4 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 23fadeba0e..2a491300b0 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 27f7adc669..fd02b5f880 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>() { @Nullable @Override @@ -157,7 +159,7 @@ public class CassandraDeviceDao extends CassandraAbstractSearchTextDao 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> 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> 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 dbc098e575..2b9e5224e0 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 { @@ -52,7 +52,7 @@ public interface DeviceDao extends Dao { * 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 { /** * 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 { /** * 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 findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink); @@ -80,10 +80,10 @@ public interface DeviceDao extends Dao { /** * 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 findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink); @@ -92,9 +92,9 @@ public interface DeviceDao extends Dao { /** * 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> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List deviceIds); @@ -103,7 +103,7 @@ public interface DeviceDao extends Dao { * 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 findDeviceByTenantIdAndName(UUID tenantId, String name); @@ -114,4 +114,31 @@ public interface DeviceDao extends Dao { * @return the list of tenant device type objects */ ListenableFuture> 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> 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> 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 0000000000..3bf3662eab --- /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> findOfflineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold); + + ListenableFuture> 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 0000000000..f4d8e61d98 --- /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> findOfflineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold) { + DeviceStatusQuery statusQuery = new DeviceStatusQuery(OFFLINE, contactType, offlineThreshold); + return deviceDao.findDevicesByTenantIdAndStatus(tenantId, statusQuery); + } + + @Override + public ListenableFuture> 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/exception/BufferLimitException.java b/dao/src/main/java/org/thingsboard/server/dao/exception/BufferLimitException.java new file mode 100644 index 0000000000..3334dc62a9 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/exception/BufferLimitException.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.exception; + +public class BufferLimitException extends RuntimeException { + + private static final long serialVersionUID = 4513762009041887588L; + + public BufferLimitException() { + super("Rate Limit Buffer is full"); + } +} 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 a159b9e6ec..52b15efebd 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 ac90cb7085..ab2e3bce2d 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 { 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 ef0c5fe159..7458e56a2e 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 { @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 { 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 { 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.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 34659a83df..251a68901a 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 { 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 { 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.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 ba96e4b98e..8d3f3c3eaf 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 { 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 { } 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.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 7aaf0ae155..e831c6e5ff 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 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 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 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 471ec7b06b..a48421a96a 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 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 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 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 d96048756c..6a888c2d72 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 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 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 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 c2f709f05b..ba186ccebe 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 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 7e87fa8fec..47d43ba57f 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, D> exte List list = Collections.emptyList(); if (statement != null) { statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel()); - ResultSet resultSet = getSession().execute(statement); + ResultSet resultSet = executeRead(statement); Result result = getMapper().map(resultSet); if (result != null) { list = result.all(); @@ -75,7 +75,7 @@ public abstract class CassandraAbstractModelDao, D> exte protected ListenableFuture> 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>() { @Nullable @Override @@ -97,7 +97,7 @@ public abstract class CassandraAbstractModelDao, D> exte E object = null; if (statement != null) { statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel()); - ResultSet resultSet = getSession().execute(statement); + ResultSet resultSet = executeRead(statement); Result result = getMapper().map(resultSet); if (result != null) { object = result.one(); @@ -109,7 +109,7 @@ public abstract class CassandraAbstractModelDao, D> exte protected ListenableFuture 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() { @Nullable @Override @@ -184,7 +184,7 @@ public abstract class CassandraAbstractModelDao, 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 0000000000..d2505632d7 --- /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 originalFuture; + private final ListenableFuture 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) 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() { + @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 da991fa97d..85f42ae169 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 9e252412b2..55838d6462 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 01f60f8dff..836bd3d314 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 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 deleteRelationAsync(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { @@ -209,9 +211,9 @@ public class BaseRelationService implements RelationService { private void checkFromDeleteSync(Cache cache, List 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> 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 toAndType = new ArrayList<>(); - toAndType.add(entityId); - toAndType.add(relation.getType()); - cache.evict(toAndType); - - List 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 fromToTypeAndTypeGroup = new ArrayList<>(); + fromToTypeAndTypeGroup.add(relation.getFrom()); + fromToTypeAndTypeGroup.add(relation.getTo()); + fromToTypeAndTypeGroup.add(relation.getType()); + fromToTypeAndTypeGroup.add(relation.getTypeGroup()); + cache.evict(fromToTypeAndTypeGroup); + + List fromTypeAndTypeGroup = new ArrayList<>(); + fromTypeAndTypeGroup.add(relation.getFrom()); + fromTypeAndTypeGroup.add(relation.getType()); + fromTypeAndTypeGroup.add(relation.getTypeGroup()); + cache.evict(fromTypeAndTypeGroup); + + List fromAndTypeGroup = new ArrayList<>(); + fromAndTypeGroup.add(relation.getFrom()); + fromAndTypeGroup.add(relation.getTypeGroup()); + cache.evict(fromAndTypeGroup); + + if (!outboundOnly) { + List toAndTypeGroup = new ArrayList<>(); + toAndTypeGroup.add(relation.getTo()); + toAndTypeGroup.add(relation.getTypeGroup()); + cache.evict(toAndTypeGroup); + + List 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 1e791635c9..cdb9a80991 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()); } } } @@ -218,6 +221,18 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC return ruleChainDao.findById(ruleChainId.getId()); } + @Override + public RuleNode findRuleNodeById(RuleNodeId ruleNodeId) { + Validator.validateId(ruleNodeId, "Incorrect rule node id for search request."); + return ruleNodeDao.findById(ruleNodeId.getId()); + } + + @Override + public ListenableFuture 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."); @@ -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 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 f1df09edb9..fff3f6d907 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 6c44090564..da7833d696 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 findRuleChainByIdAsync(RuleChainId ruleChainId); + RuleChain getRootTenantRuleChain(TenantId tenantId); List 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 3bab1c522e..a48805b8be 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 { List findDevicesByTenantIdAndCustomerIdAndIdIn(String tenantId, String customerId, List deviceIds); List findDevicesByTenantIdAndIdIn(String tenantId, List deviceIds); + + @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastConnectTs > :time") + List findConnectOnlineByTenantId(@Param("tenantId") String tenantId, @Param("time") long time); + + @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastConnectTs < :time") + List findConnectOfflineByTenantId(@Param("tenantId") String tenantId, @Param("time") long time); + + @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastUpdateTs > :time") + List findUpdateOnlineByTenantId(@Param("tenantId") String tenantId, @Param("time") long time); + + @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastUpdateTs < :time") + List 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 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 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 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 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 4f3cd7d6fb..baba65952d 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 implements DeviceDao { @Autowired @@ -124,6 +128,73 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(fromTimeUUID(tenantId)))); } + @Override + public ListenableFuture> 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> 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 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 convertTenantDeviceTypesToDto(UUID tenantId, List types) { List 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 d620e11f1d..cda4b1669b 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 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/AsyncRateLimiter.java b/dao/src/main/java/org/thingsboard/server/dao/util/AsyncRateLimiter.java new file mode 100644 index 0000000000..6fb21d6adb --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/util/AsyncRateLimiter.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.ListenableFuture; + +public interface AsyncRateLimiter { + + ListenableFuture acquireAsync(); + + void release(); +} 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 0000000000..04196686c3 --- /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 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 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 future = pool.submit(() -> { + latch.await(); + return null; + }); + return new LockedFuture(latch, future, System.currentTimeMillis()); + } + + private ListenableFuture 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 future; + final long createTime; + + public LockedFuture(CountDownLatch latch, ListenableFuture 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 c221f6880d..79e96554ec 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 106204ab64..08fe7fa529 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 0000000000..f49668d3fd --- /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) 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) 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 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 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) 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 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 future = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1)).submit(() -> { + latch.await(); + return null; + }); + when(rateLimiter.acquireAsync()).thenReturn(future); + resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement); + + ListenableFuture 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 d083a90262..44a1a0927b 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/UnprocessedMsgFilterTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/UnprocessedMsgFilterTest.java index 6302e63f89..3935c9b92e 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 msgs = Lists.newArrayList(msg1, msg2); List acks = Lists.newArrayList(new MsgAck(id2, UUID.randomUUID(), 1L, 1L)); Collection actual = msgFilter.filter(msgs, acks); 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 d17e1f2819..a766aa48ce 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 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 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 future = msgRepository.save(msg, nodeId, 1L, 1L, 1L); future.get(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/rule/BaseRuleServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/rule/BaseRuleServiceTest.java deleted file mode 100644 index b6139df898..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/rule/BaseRuleServiceTest.java +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Copyright © 2016-2018 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.rule; - -import com.datastax.driver.core.utils.UUIDs; -import org.junit.Assert; -import org.junit.Test; -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.PluginMetaData; -import org.thingsboard.server.common.data.rule.RuleMetaData; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.service.AbstractServiceTest; - -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; - -public abstract class BaseRuleServiceTest extends AbstractServiceTest { - - @Test - public void saveRule() throws Exception { - PluginMetaData plugin = generatePlugin(null, "testPluginToken" + ThreadLocalRandom.current().nextInt()); - pluginService.savePlugin(plugin); - RuleMetaData ruleMetaData = ruleService.saveRule(generateRule(plugin.getTenantId(), null, plugin.getApiToken())); - Assert.assertNotNull(ruleMetaData.getId()); - Assert.assertNotNull(ruleMetaData.getAdditionalInfo()); - ruleMetaData.setAdditionalInfo(mapper.readTree("{\"description\":\"test\"}")); - RuleMetaData newRuleMetaData = ruleService.saveRule(ruleMetaData); - Assert.assertEquals(ruleMetaData.getAdditionalInfo(), newRuleMetaData.getAdditionalInfo()); - } - - @Test - public void findRuleById() throws Exception { - PluginMetaData plugin = generatePlugin(null, "testPluginToken" + ThreadLocalRandom.current().nextInt()); - pluginService.savePlugin(plugin); - - RuleMetaData expected = ruleService.saveRule(generateRule(plugin.getTenantId(), null, plugin.getApiToken())); - Assert.assertNotNull(expected.getId()); - RuleMetaData found = ruleService.findRuleById(expected.getId()); - Assert.assertEquals(expected, found); - } - - @Test - public void findPluginRules() throws Exception { - TenantId tenantIdA = new TenantId(UUIDs.timeBased()); - TenantId tenantIdB = new TenantId(UUIDs.timeBased()); - - PluginMetaData pluginA = generatePlugin(tenantIdA, "testPluginToken" + ThreadLocalRandom.current().nextInt()); - PluginMetaData pluginB = generatePlugin(tenantIdB, "testPluginToken" + ThreadLocalRandom.current().nextInt()); - pluginService.savePlugin(pluginA); - pluginService.savePlugin(pluginB); - - ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken())); - ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken())); - ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken())); - - ruleService.saveRule(generateRule(tenantIdB, null, pluginB.getApiToken())); - ruleService.saveRule(generateRule(tenantIdB, null, pluginB.getApiToken())); - - List foundA = ruleService.findPluginRules(pluginA.getApiToken()); - Assert.assertEquals(3, foundA.size()); - - List foundB = ruleService.findPluginRules(pluginB.getApiToken()); - Assert.assertEquals(2, foundB.size()); - } - - @Test - public void findSystemRules() throws Exception { - TenantId systemTenant = new TenantId(ModelConstants.NULL_UUID); // system tenant id - - PluginMetaData plugin = generatePlugin(systemTenant, "testPluginToken" + ThreadLocalRandom.current().nextInt()); - pluginService.savePlugin(plugin); - ruleService.saveRule(generateRule(systemTenant, null, plugin.getApiToken())); - ruleService.saveRule(generateRule(systemTenant, null, plugin.getApiToken())); - ruleService.saveRule(generateRule(systemTenant, null, plugin.getApiToken())); - TextPageData found = ruleService.findSystemRules(new TextPageLink(100)); - Assert.assertEquals(3, found.getData().size()); - } - - @Test - public void findTenantRules() throws Exception { - TenantId tenantIdA = new TenantId(UUIDs.timeBased()); - TenantId tenantIdB = new TenantId(UUIDs.timeBased()); - - PluginMetaData pluginA = generatePlugin(tenantIdA, "testPluginToken" + ThreadLocalRandom.current().nextInt()); - PluginMetaData pluginB = generatePlugin(tenantIdB, "testPluginToken" + ThreadLocalRandom.current().nextInt()); - pluginService.savePlugin(pluginA); - pluginService.savePlugin(pluginB); - - ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken())); - ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken())); - ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken())); - - ruleService.saveRule(generateRule(tenantIdB, null, pluginB.getApiToken())); - ruleService.saveRule(generateRule(tenantIdB, null, pluginB.getApiToken())); - - TextPageData foundA = ruleService.findTenantRules(tenantIdA, new TextPageLink(100)); - Assert.assertEquals(3, foundA.getData().size()); - - TextPageData foundB = ruleService.findTenantRules(tenantIdB, new TextPageLink(100)); - Assert.assertEquals(2, foundB.getData().size()); - } - - @Test - public void deleteRuleById() throws Exception { - PluginMetaData plugin = generatePlugin(null, "testPluginToken" + ThreadLocalRandom.current().nextInt()); - pluginService.savePlugin(plugin); - - RuleMetaData expected = ruleService.saveRule(generateRule(plugin.getTenantId(), null, plugin.getApiToken())); - Assert.assertNotNull(expected.getId()); - RuleMetaData found = ruleService.findRuleById(expected.getId()); - Assert.assertEquals(expected, found); - ruleService.deleteRuleById(expected.getId()); - found = ruleService.findRuleById(expected.getId()); - Assert.assertNull(found); - } - - @Test - public void deleteRulesByTenantId() throws Exception { - TenantId tenantIdA = new TenantId(UUIDs.timeBased()); - TenantId tenantIdB = new TenantId(UUIDs.timeBased()); - - PluginMetaData pluginA = generatePlugin(tenantIdA, "testPluginToken" + ThreadLocalRandom.current().nextInt()); - PluginMetaData pluginB = generatePlugin(tenantIdB, "testPluginToken" + ThreadLocalRandom.current().nextInt()); - pluginService.savePlugin(pluginA); - pluginService.savePlugin(pluginB); - - ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken())); - ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken())); - ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken())); - - ruleService.saveRule(generateRule(tenantIdB, null, pluginB.getApiToken())); - ruleService.saveRule(generateRule(tenantIdB, null, pluginB.getApiToken())); - - TextPageData foundA = ruleService.findTenantRules(tenantIdA, new TextPageLink(100)); - Assert.assertEquals(3, foundA.getData().size()); - - TextPageData foundB = ruleService.findTenantRules(tenantIdB, new TextPageLink(100)); - Assert.assertEquals(2, foundB.getData().size()); - - ruleService.deleteRulesByTenantId(tenantIdA); - - foundA = ruleService.findTenantRules(tenantIdA, new TextPageLink(100)); - Assert.assertEquals(0, foundA.getData().size()); - - foundB = ruleService.findTenantRules(tenantIdB, new TextPageLink(100)); - Assert.assertEquals(2, foundB.getData().size()); - } -} \ No newline at end of file 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 0000000000..67c3ce8d73 --- /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 actual = limiter.acquireAsync(); + assertTrue(actual.isDone()); + } + + @Test + public void notFinishedFutureReturnedIfPermitsAreNotGranted() { + BufferedRateLimiter limiter = new BufferedRateLimiter(10, 1, 100); + ListenableFuture actual1 = limiter.acquireAsync(); + ListenableFuture actual2 = limiter.acquireAsync(); + assertTrue(actual1.isDone()); + assertFalse(actual2.isDone()); + } + + @Test + public void failedFutureReturnedIfQueueIsfull() { + BufferedRateLimiter limiter = new BufferedRateLimiter(1, 1, 100); + ListenableFuture actual1 = limiter.acquireAsync(); + ListenableFuture actual2 = limiter.acquireAsync(); + ListenableFuture 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 actual1 = limiter.acquireAsync(); + ListenableFuture actual2 = limiter.acquireAsync(); + ListenableFuture actual3 = limiter.acquireAsync(); + ListenableFuture 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> submit = pool.submit(limiter::acquireAsync); + Futures.addCallback(submit, new FutureCallback>() { + @Override + public void onSuccess(@Nullable ListenableFuture result) { + Futures.addCallback(result, new FutureCallback() { + @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 82fcbe1949..737687f053 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 ac9600a6a5..d7438f4fbc 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/SubscriptionManager.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java index ec00677af5..0c3b174c45 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 keyStates = subscription.getKeyStates(); ctx.loadAttributes(entityId, DataConstants.CLIENT_SCOPE, keyStates.keySet(), new PluginCallback>() { @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 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> f) { + public void onLocalSubscriptionUpdate(PluginContext ctx, EntityId entityId, TelemetryFeature type, Function> 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 attributes) { Optional 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 subscriptionUpdate = new ArrayList(); 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 entries) { Optional serverAddress = ctx.resolve(entityId); if (!serverAddress.isPresent()) { - onLocalSubscriptionUpdate(ctx, entityId, SubscriptionType.TIMESERIES, s -> { + onLocalSubscriptionUpdate(ctx, entityId, TelemetryFeature.TIMESERIES, s -> { List subscriptionUpdate = new ArrayList(); for (TsKvEntry kv : entries) { if (s.isAllKeys() || s.getKeyStates().containsKey((kv.getKey()))) { @@ -344,9 +354,7 @@ public class SubscriptionManager { } private void checkSubsciptionsPrevAddress(Set subscriptions) { - Iterator 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()); 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 d116b2e46c..f30f1a5978 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 b06476ab9d..7f78abd77d 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 4d64ca70d4..88ecb2d9bb 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 c6e7a54da0..1acc29db05 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 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 booleanValue = attr.getBooleanValue(); - if (booleanValue.isPresent()) { - dataBuilder.setBoolValue(booleanValue.get()); - } + booleanValue.ifPresent(dataBuilder::setBoolValue); break; case LONG: Optional longValue = attr.getLongValue(); - if (longValue.isPresent()) { - dataBuilder.setLongValue(longValue.get()); - } + longValue.ifPresent(dataBuilder::setLongValue); break; case DOUBLE: Optional doubleValue = attr.getDoubleValue(); - if (doubleValue.isPresent()) { - dataBuilder.setDoubleValue(doubleValue.get()); - } + doubleValue.ifPresent(dataBuilder::setDoubleValue); break; case STRING: Optional 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 4fdfe4ab59..9cb67fdffa 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 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 1374ef68ac..8c80e78b8b 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 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 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 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 fc047130b8..98c7632893 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 c9598efbd9..e4a0d260b1 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 keyStates; @Getter private final String scope; diff --git a/pom.xml b/pom.xml index f331e32fb2..dc58627047 100755 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ 1.2.3 1.9.5 0.10 - 3.0.0 + 3.0.7 3.0.0.1 1.2.7 18.0 @@ -284,6 +284,7 @@ src/sh/** src/main/scripts/control/** src/main/scripts/windows/** + src/main/resources/public/static/rulenode/** JAVADOC_STYLE @@ -378,6 +379,11 @@ rule-engine-api ${project.version} + + org.thingsboard.rule-engine + rule-engine-components + ${project.version} + org.thingsboard.common message 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 0000000000..9356be96fc --- /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 { + + ListenableFuture executeAsync(Callable task); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/mail/MailService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java similarity index 80% rename from application/src/main/java/org/thingsboard/server/service/mail/MailService.java rename to rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java index b87da4c106..02ce780293 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/MailService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java @@ -13,12 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.mail; +package org.thingsboard.rule.engine.api; -import org.thingsboard.server.exception.ThingsboardException; +import org.thingsboard.server.common.data.exception.ThingsboardException; import com.fasterxml.jackson.databind.JsonNode; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; + public interface MailService { void updateMailConfiguration(); @@ -34,5 +37,6 @@ public interface MailService { void sendResetPasswordEmail(String passwordResetLink, String email) throws ThingsboardException; void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException; - + + void send(String from, String to, String cc, String bcc, String subject, String body) throws MessagingException; } diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/CompoundRuleActorChain.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeConfiguration.java similarity index 81% rename from application/src/main/java/org/thingsboard/server/actors/rule/CompoundRuleActorChain.java rename to rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeConfiguration.java index b2eb53fe3f..25c14017e5 100644 --- a/application/src/main/java/org/thingsboard/server/actors/rule/CompoundRuleActorChain.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeConfiguration.java @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.actors.rule; +package org.thingsboard.rule.engine.api; -public class CompoundRuleActorChain { +public interface NodeConfiguration { + + T defaultConfiguration(); } 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 0000000000..18b2b941ad --- /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 0000000000..1ba18cd52f --- /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 ts, FutureCallback callback); + + void saveAndNotify(EntityId entityId, List ts, long ttl, FutureCallback callback); + + void saveAndNotify(EntityId entityId, String scope, List attributes, FutureCallback 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 0000000000..eea92ed00a --- /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 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 0000000000..1db046af39 --- /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 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 07cd72c001..6038e6d84f 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 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 89442bb648..2555c999f2 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 d06c0d277b..64053cd61c 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 6766999d50..b42ec8ec13 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 c48b11d387..2c77a69ba3 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 { } diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 9b903b1b00..419b5f6ef3 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -43,6 +43,11 @@ dao provided + + org.thingsboard.common + transport + provided + ch.qos.logback logback-core @@ -66,6 +71,16 @@ com.google.guava guava + + org.apache.velocity + velocity + provided + + + org.apache.velocity + velocity-tools + provided + junit junit @@ -89,9 +104,8 @@ test - org.junit.jupiter - junit-jupiter-api - RELEASE + org.thingsboard.common + transport diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/DonAsynchron.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/DonAsynchron.java new file mode 100644 index 0000000000..4fed574638 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/DonAsynchron.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; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import javax.annotation.Nullable; +import java.util.function.Consumer; + +public class DonAsynchron { + + public static void withCallback(ListenableFuture future, Consumer onSuccess, Consumer onFailure) { + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable T result) { + try { + onSuccess.accept(result); + } catch (Throwable th) { + onFailure(th); + } + + } + + @Override + public void onFailure(Throwable t) { + onFailure.accept(t); + } + }); + } +} 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 0000000000..0549e6af23 --- /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 + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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 msg property. For example 'temperature = ' + msg.temperature ;" + + "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;", + 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 shouldCreate = jsExecutor.executeAsync(() -> createJsEngine.executeFilter(msg)); + ListenableFuture transform = Futures.transform(shouldCreate, (AsyncFunction) 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 createOrUpdate(TbContext ctx, TbMsg msg) { + ListenableFuture latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType()); + return Futures.transform(latest, (AsyncFunction) a -> { + if (a == null || a.getStatus().isCleared()) { + return createNewAlarm(ctx, msg); + } else { + return updateAlarm(ctx, msg, a); + } + }, ctx.getDbCallbackExecutor()); + } + + private ListenableFuture checkForClearIfExist(TbContext ctx, TbMsg msg) { + ListenableFuture latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType()); + return Futures.transform(latest, (AsyncFunction) 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 createNewAlarm(TbContext ctx, TbMsg msg) { + ListenableFuture asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg), + (Function) details -> buildAlarm(msg, details, ctx.getTenantId())); + ListenableFuture asyncCreated = Futures.transform(asyncAlarm, + (Function) alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor()); + return Futures.transform(asyncCreated, (Function) alarm -> new AlarmResult(true, false, false, alarm)); + } + + private ListenableFuture updateAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { + ListenableFuture asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg), (Function) 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) a -> new AlarmResult(false, true, false, a)); + } + + private ListenableFuture clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { + ListenableFuture shouldClear = ctx.getJsExecutor().executeAsync(() -> clearJsEngine.executeFilter(msg)); + return Futures.transform(shouldClear, (AsyncFunction) clear -> { + if (clear) { + ListenableFuture clearFuture = ctx.getAlarmService().clearAlarm(alarm.getId(), System.currentTimeMillis()); + return Futures.transform(clearFuture, (Function) 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 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 0000000000..3575459854 --- /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 0000000000..3cd299dcc6 --- /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 msg property. For example 'temperature = ' + msg.temperature ;" + + "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;", + 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 0000000000..aafb7f12c1 --- /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 0000000000..1d944ec327 --- /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 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 0000000000..65b13710e3 --- /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 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 0000000000..c568e3dd94 --- /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 { + + 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 0000000000..8ad344f87d --- /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 True - send Message via True chain, otherwise False chain is used." + + "Message payload can be accessed via msg property. For example msg.temperature < 10;
" + + "Message metadata can be accessed via metadata property. For example metadata.customerName === 'John';
" + + "Message type can be accessed via msgType 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/application/src/main/java/org/thingsboard/server/actors/rule/RuleContextAwareMsgProcessor.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java similarity index 51% rename from application/src/main/java/org/thingsboard/server/actors/rule/RuleContextAwareMsgProcessor.java rename to rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java index c0d0705c4d..9ab74e890f 100644 --- a/application/src/main/java/org/thingsboard/server/actors/rule/RuleContextAwareMsgProcessor.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java @@ -13,21 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.actors.rule; +package org.thingsboard.rule.engine.filter; -import org.thingsboard.server.actors.ActorSystemContext; -import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor; -import org.thingsboard.server.common.data.id.RuleId; +import lombok.Data; +import org.thingsboard.rule.engine.api.NodeConfiguration; -import akka.event.LoggingAdapter; +@Data +public class TbJsFilterNodeConfiguration implements NodeConfiguration { -public class RuleContextAwareMsgProcessor extends AbstractContextAwareMsgProcessor { + private String jsScript; - private final RuleId ruleId; - - protected RuleContextAwareMsgProcessor(ActorSystemContext systemContext, LoggingAdapter logger, RuleId ruleId) { - super(systemContext, logger); - this.ruleId = ruleId; + @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 0000000000..3c6704b013 --- /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 msg property. For example msg.temperature < 10;
" + + "Message metadata can be accessed via metadata property. For example metadata.customerName === 'John';
" + + "Message type can be accessed via msgType 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 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 0000000000..79b0912c6c --- /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 { + + 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 026da1ba83..225cd99e36 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 Success chain, otherwise Failure 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 3b7ba9055e..ae88aa8b07 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 { private List 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 0000000000..35eaa3b14b --- /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 0000000000..7413cadd48 --- /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 0000000000..cae2058417 --- /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