Browse Source

Merge branch 'develop/1.5' into develop/1.5-no-more-plugins

pull/725/head
Andrew Shvayka 8 years ago
parent
commit
da7955f97d
  1. 2
      application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java
  2. 20
      application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
  3. 9
      application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
  4. 4
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  5. 4
      application/src/main/resources/thingsboard.yml
  6. 6
      common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java
  7. 2
      dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java
  8. 4
      dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java
  9. 6
      dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
  10. 4
      dao/src/main/java/org/thingsboard/server/dao/component/CassandraBaseComponentDescriptorDao.java
  11. 2
      dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java
  12. 19
      dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
  13. 10
      dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractModelDao.java
  14. 148
      dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java
  15. 16
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
  16. 42
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
  17. 52
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  18. 25
      dao/src/main/java/org/thingsboard/server/dao/util/AsyncRateLimiter.java
  19. 164
      dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java
  20. 156
      dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java
  21. 134
      dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java
  22. 5
      dao/src/test/resources/cassandra-test.properties
  23. 22
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeConfiguration.java
  24. 5
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java
  25. 2
      rule-engine/rule-engine-api/src/main/resources/EmptyNodeConfig.json
  26. 2
      rule-engine/rule-engine-api/src/main/resources/EmptyNodeDescriptor.json
  27. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
  28. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java
  29. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
  30. 17
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java
  31. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java
  32. 11
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java
  33. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
  34. 13
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
  35. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
  36. 14
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java
  37. 19
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java
  38. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
  39. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java
  40. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
  41. 14
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java
  42. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
  43. 11
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java
  44. 25
      ui/src/app/api/rule-chain.service.js
  45. 45
      ui/src/app/components/ace-editor-fix.js
  46. 3
      ui/src/app/components/js-func.directive.js
  47. 168
      ui/src/app/components/json-object-edit.directive.js
  48. 35
      ui/src/app/components/json-object-edit.scss
  49. 34
      ui/src/app/components/json-object-edit.tpl.html
  50. 8
      ui/src/app/components/react/json-form-ace-editor.jsx
  51. 5
      ui/src/app/components/widget/widget-config.directive.js
  52. 8
      ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
  53. 10
      ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
  54. 4
      ui/src/app/layout/index.js
  55. 1
      ui/src/app/locale/locale.constant.js
  56. 82
      ui/src/app/rulechain/rulechain.controller.js
  57. 5
      ui/src/app/rulechain/rulenode-fieldset.tpl.html
  58. 2
      ui/src/app/rulechain/rulenode.directive.js
  59. 22
      ui/src/app/rulechain/rulenode.scss
  60. 2
      ui/src/app/rulechain/rulenode.tpl.html
  61. 18
      ui/src/scss/main.scss

2
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 {

20
application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java

@ -28,9 +28,14 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.plugin.PluginMetaData;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
import org.thingsboard.server.common.msg.session.MsgType;
import org.thingsboard.server.extensions.api.plugins.Plugin;
import org.thingsboard.server.extensions.api.plugins.PluginInitializationException;
import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
@ -98,7 +103,20 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
public void onRuleToPluginMsg(RuleToPluginMsgWrapper msg) throws RuleException {
if (state == ComponentLifecycleState.ACTIVE) {
pluginImpl.process(trustedCtx, msg.getRuleTenantId(), msg.getRuleId(), msg.getMsg());
try {
pluginImpl.process(trustedCtx, msg.getRuleTenantId(), msg.getRuleId(), msg.getMsg());
} catch (Exception ex) {
logger.debug("[{}] Failed to process RuleToPlugin msg: [{}] [{}]", tenantId, msg.getMsg(), ex);
RuleToPluginMsg ruleMsg = msg.getMsg();
MsgType responceMsgType = MsgType.RULE_ENGINE_ERROR;
Integer requestId = 0;
if (ruleMsg.getPayload() instanceof FromDeviceRequestMsg) {
requestId = ((FromDeviceRequestMsg) ruleMsg.getPayload()).getRequestId();
}
trustedCtx.reply(
new ResponsePluginToRuleMsg(ruleMsg.getUid(), tenantId, msg.getRuleId(),
BasicStatusCodeResponse.onError(responceMsgType, requestId, ex)));
}
} else {
//TODO: reply with plugin suspended message
}

9
application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java

@ -180,7 +180,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
return scannedComponent;
}
private NodeDefinition prepareNodeDefinition(RuleNode nodeAnnotation) throws IOException {
private NodeDefinition prepareNodeDefinition(RuleNode nodeAnnotation) throws Exception {
NodeDefinition nodeDefinition = new NodeDefinition();
nodeDefinition.setDetails(nodeAnnotation.nodeDetails());
nodeDefinition.setDescription(nodeAnnotation.nodeDescription());
@ -188,9 +188,10 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
nodeDefinition.setOutEnabled(nodeAnnotation.outEnabled());
nodeDefinition.setRelationTypes(nodeAnnotation.relationTypes());
nodeDefinition.setCustomRelations(nodeAnnotation.customRelations());
String defaultConfigResourceName = nodeAnnotation.defaultConfigResource();
nodeDefinition.setDefaultConfiguration(mapper.readTree(
Resources.toString(Resources.getResource(defaultConfigResourceName), Charsets.UTF_8)));
Class<? extends NodeConfiguration> configClazz = nodeAnnotation.configClazz();
NodeConfiguration config = configClazz.newInstance();
NodeConfiguration defaultConfiguration = config.defaultConfiguration();
nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration));
return nodeDefinition;
}

4
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);
}

4
application/src/main/resources/thingsboard.yml

@ -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

6
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;

2
dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java

@ -148,7 +148,7 @@ public class CassandraAssetDao extends CassandraAbstractSearchTextDao<AssetEntit
query.and(eq(ENTITY_SUBTYPE_TENANT_ID_PROPERTY, tenantId));
query.and(eq(ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY, EntityType.ASSET));
query.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
ResultSetFuture resultSetFuture = getSession().executeAsync(query);
ResultSetFuture resultSetFuture = executeAsyncRead(query);
return Futures.transform(resultSetFuture, new Function<ResultSet, List<EntitySubtype>>() {
@Nullable
@Override

4
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 +

6
dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java

@ -244,12 +244,12 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
values.add("?");
}
String statementString = INSERT_INTO + cfName + " (" + String.join(",", columnsList) + ") VALUES (" + values.toString() + ")";
return getSession().prepare(statementString);
return prepare(statementString);
}
private PreparedStatement getPartitionInsertStmt() {
if (partitionInsertStmt == null) {
partitionInsertStmt = getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF +
partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF +
"(" + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY +
"," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" +
" VALUES(?, ?)");
@ -343,7 +343,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
.where(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId));
select.and(QueryBuilder.gte(ModelConstants.PARTITION_COLUMN, minPartition));
select.and(QueryBuilder.lte(ModelConstants.PARTITION_COLUMN, maxPartition));
return getSession().execute(select);
return executeRead(select);
}
}

4
dao/src/main/java/org/thingsboard/server/dao/component/CassandraBaseComponentDescriptorDao.java

@ -130,7 +130,7 @@ public class CassandraBaseComponentDescriptorDao extends CassandraAbstractSearch
public boolean removeById(UUID key) {
Statement delete = QueryBuilder.delete().all().from(ModelConstants.COMPONENT_DESCRIPTOR_BY_ID).where(eq(ModelConstants.ID_PROPERTY, key));
log.debug("Remove request: {}", delete.toString());
return getSession().execute(delete).wasApplied();
return executeWrite(delete).wasApplied();
}
@Override
@ -145,7 +145,7 @@ public class CassandraBaseComponentDescriptorDao extends CassandraAbstractSearch
log.debug("Delete plugin meta-data entity by id [{}]", clazz);
Statement delete = QueryBuilder.delete().all().from(getColumnFamilyName()).where(eq(ModelConstants.COMPONENT_DESCRIPTOR_CLASS_PROPERTY, clazz));
log.debug("Remove request: {}", delete.toString());
ResultSet resultSet = getSession().execute(delete);
ResultSet resultSet = executeWrite(delete);
log.debug("Delete result: [{}]", resultSet.wasApplied());
}

2
dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java

@ -148,7 +148,7 @@ public class CassandraDeviceDao extends CassandraAbstractSearchTextDao<DeviceEnt
query.and(eq(ENTITY_SUBTYPE_TENANT_ID_PROPERTY, tenantId));
query.and(eq(ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY, EntityType.DEVICE));
query.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
ResultSetFuture resultSetFuture = getSession().executeAsync(query);
ResultSetFuture resultSetFuture = executeAsyncRead(query);
return Futures.transform(resultSetFuture, new Function<ResultSet, List<EntitySubtype>>() {
@Nullable
@Override

19
dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java

@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.model.type.*;
import org.thingsboard.server.dao.util.BufferedRateLimiter;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@ -33,16 +34,15 @@ public abstract class CassandraAbstractDao {
private ConcurrentMap<String, PreparedStatement> preparedStatementMap = new ConcurrentHashMap<>();
protected PreparedStatement prepare(String query) {
return preparedStatementMap.computeIfAbsent(query, i -> getSession().prepare(i));
}
@Autowired
private BufferedRateLimiter rateLimiter;
private Session session;
private ConsistencyLevel defaultReadLevel;
private ConsistencyLevel defaultWriteLevel;
protected Session getSession() {
private Session getSession() {
if (session == null) {
session = cluster.getSession();
defaultReadLevel = cluster.getDefaultReadConsistencyLevel();
@ -59,6 +59,10 @@ public abstract class CassandraAbstractDao {
return session;
}
protected PreparedStatement prepare(String query) {
return preparedStatementMap.computeIfAbsent(query, i -> getSession().prepare(i));
}
private void registerCodecIfNotFound(CodecRegistry registry, TypeCodec<?> codec) {
try {
registry.codecFor(codec.getCqlType(), codec.getJavaType());
@ -85,10 +89,7 @@ public abstract class CassandraAbstractDao {
private ResultSet execute(Statement statement, ConsistencyLevel level) {
log.debug("Execute cassandra statement {}", statement);
if (statement.getConsistencyLevel() == null) {
statement.setConsistencyLevel(level);
}
return getSession().execute(statement);
return executeAsync(statement, level).getUninterruptibly();
}
private ResultSetFuture executeAsync(Statement statement, ConsistencyLevel level) {
@ -96,6 +97,6 @@ public abstract class CassandraAbstractDao {
if (statement.getConsistencyLevel() == null) {
statement.setConsistencyLevel(level);
}
return getSession().executeAsync(statement);
return new RateLimitedResultSetFuture(getSession(), rateLimiter, statement);
}
}

10
dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractModelDao.java

@ -63,7 +63,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
List<E> list = Collections.emptyList();
if (statement != null) {
statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
ResultSet resultSet = getSession().execute(statement);
ResultSet resultSet = executeRead(statement);
Result<E> result = getMapper().map(resultSet);
if (result != null) {
list = result.all();
@ -75,7 +75,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
protected ListenableFuture<List<D>> findListByStatementAsync(Statement statement) {
if (statement != null) {
statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
ResultSetFuture resultSetFuture = getSession().executeAsync(statement);
ResultSetFuture resultSetFuture = executeAsyncRead(statement);
return Futures.transform(resultSetFuture, new Function<ResultSet, List<D>>() {
@Nullable
@Override
@ -97,7 +97,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
E object = null;
if (statement != null) {
statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
ResultSet resultSet = getSession().execute(statement);
ResultSet resultSet = executeRead(statement);
Result<E> result = getMapper().map(resultSet);
if (result != null) {
object = result.one();
@ -109,7 +109,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
protected ListenableFuture<D> findOneByStatementAsync(Statement statement) {
if (statement != null) {
statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
ResultSetFuture resultSetFuture = getSession().executeAsync(statement);
ResultSetFuture resultSetFuture = executeAsyncRead(statement);
return Futures.transform(resultSetFuture, new Function<ResultSet, D>() {
@Nullable
@Override
@ -184,7 +184,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
public boolean removeById(UUID key) {
Statement delete = QueryBuilder.delete().all().from(getColumnFamilyName()).where(eq(ModelConstants.ID_PROPERTY, key));
log.debug("Remove request: {}", delete.toString());
return getSession().execute(delete).wasApplied();
return executeWrite(delete).wasApplied();
}
@Override

148
dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java

@ -0,0 +1,148 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.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.util.AsyncRateLimiter;
import javax.annotation.Nullable;
import java.util.concurrent.*;
public class RateLimitedResultSetFuture implements ResultSetFuture {
private final ListenableFuture<ResultSetFuture> originalFuture;
private final ListenableFuture<Void> rateLimitFuture;
public RateLimitedResultSetFuture(Session session, AsyncRateLimiter rateLimiter, Statement statement) {
this.rateLimitFuture = rateLimiter.acquireAsync();
this.originalFuture = Futures.transform(rateLimitFuture,
(Function<Void, ResultSetFuture>) i -> executeAsyncWithRelease(rateLimiter, session, statement));
}
@Override
public ResultSet getUninterruptibly() {
return safeGet().getUninterruptibly();
}
@Override
public ResultSet getUninterruptibly(long timeout, TimeUnit unit) throws TimeoutException {
long rateLimitStart = System.nanoTime();
ResultSetFuture resultSetFuture = null;
try {
resultSetFuture = originalFuture.get(timeout, unit);
} catch (InterruptedException | ExecutionException e) {
throw new IllegalStateException(e);
}
long rateLimitDurationNano = System.nanoTime() - rateLimitStart;
long innerTimeoutNano = unit.toNanos(timeout) - rateLimitDurationNano;
if (innerTimeoutNano > 0) {
return resultSetFuture.getUninterruptibly(innerTimeoutNano, TimeUnit.NANOSECONDS);
}
throw new TimeoutException("Timeout waiting for task.");
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
if (originalFuture.isDone()) {
return safeGet().cancel(mayInterruptIfRunning);
} else {
return originalFuture.cancel(mayInterruptIfRunning);
}
}
@Override
public boolean isCancelled() {
if (originalFuture.isDone()) {
return safeGet().isCancelled();
}
return originalFuture.isCancelled();
}
@Override
public boolean isDone() {
return originalFuture.isDone() && safeGet().isDone();
}
@Override
public ResultSet get() throws InterruptedException, ExecutionException {
return safeGet().get();
}
@Override
public ResultSet get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
long rateLimitStart = System.nanoTime();
ResultSetFuture resultSetFuture = originalFuture.get(timeout, unit);
long rateLimitDurationNano = System.nanoTime() - rateLimitStart;
long innerTimeoutNano = unit.toNanos(timeout) - rateLimitDurationNano;
if (innerTimeoutNano > 0) {
return resultSetFuture.get(innerTimeoutNano, TimeUnit.NANOSECONDS);
}
throw new TimeoutException("Timeout waiting for task.");
}
@Override
public void addListener(Runnable listener, Executor executor) {
originalFuture.addListener(() -> {
try {
ResultSetFuture resultSetFuture = Uninterruptibles.getUninterruptibly(originalFuture);
resultSetFuture.addListener(listener, executor);
} catch (CancellationException e) {
cancel(false);
return;
} catch (ExecutionException e) {
Futures.immediateFailedFuture(e).addListener(listener, executor);
}
}, executor);
}
private ResultSetFuture safeGet() {
try {
return originalFuture.get();
} catch (InterruptedException | ExecutionException e) {
throw new IllegalStateException(e);
}
}
private ResultSetFuture executeAsyncWithRelease(AsyncRateLimiter rateLimiter, Session session, Statement statement) {
try {
ResultSetFuture resultSetFuture = session.executeAsync(statement);
Futures.addCallback(resultSetFuture, new FutureCallback<ResultSet>() {
@Override
public void onSuccess(@Nullable ResultSet result) {
rateLimiter.release();
}
@Override
public void onFailure(Throwable t) {
rateLimiter.release();
}
});
return resultSetFuture;
} catch (RuntimeException re) {
rateLimiter.release();
throw re;
}
}
}

16
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 +

42
dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java

@ -82,8 +82,9 @@ public class BaseRelationService implements RelationService {
}
@Caching(evict = {
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}")
})
@ -95,8 +96,9 @@ public class BaseRelationService implements RelationService {
}
@Caching(evict = {
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}")
})
@ -108,11 +110,11 @@ public class BaseRelationService implements RelationService {
}
@Caching(evict = {
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}")
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}")
})
@Override
public boolean deleteRelation(EntityRelation relation) {
@ -122,11 +124,11 @@ public class BaseRelationService implements RelationService {
}
@Caching(evict = {
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.from"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.to"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}")
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}")
})
@Override
public ListenableFuture<Boolean> deleteRelationAsync(EntityRelation relation) {
@ -136,11 +138,11 @@ public class BaseRelationService implements RelationService {
}
@Caching(evict = {
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#from"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#to"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType}")
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType, #typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup}")
})
@Override
public boolean deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
@ -150,11 +152,11 @@ public class BaseRelationService implements RelationService {
}
@Caching(evict = {
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#from"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "#to"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType}")
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType, #typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup}"),
@CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup}")
})
@Override
public ListenableFuture<Boolean> deleteRelationAsync(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {

52
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java

@ -73,7 +73,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
private PreparedStatement partitionInsertStmt;
private PreparedStatement partitionInsertTtlStmt;
private PreparedStatement[] latestInsertStmts;
private PreparedStatement latestInsertStmt;
private PreparedStatement[] saveStmts;
private PreparedStatement[] saveTtlStmts;
private PreparedStatement[] fetchStmts;
@ -306,13 +306,15 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
@Override
public ListenableFuture<Void> saveLatest(EntityId entityId, TsKvEntry tsKvEntry) {
DataType type = tsKvEntry.getDataType();
BoundStatement stmt = getLatestStmt(type).bind()
BoundStatement stmt = getLatestStmt().bind()
.setString(0, entityId.getEntityType().name())
.setUUID(1, entityId.getId())
.setString(2, tsKvEntry.getKey())
.setLong(3, tsKvEntry.getTs());
addValue(tsKvEntry, stmt, 4);
.setLong(3, tsKvEntry.getTs())
.set(4, tsKvEntry.getBooleanValue().orElse(null), Boolean.class)
.set(5, tsKvEntry.getStrValue().orElse(null), String.class)
.set(6, tsKvEntry.getLongValue().orElse(null), Long.class)
.set(7, tsKvEntry.getDoubleValue().orElse(null), Double.class);
return getFuture(executeAsyncWrite(stmt), rs -> null);
}
@ -381,7 +383,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
if (saveStmts == null) {
saveStmts = new PreparedStatement[DataType.values().length];
for (DataType type : DataType.values()) {
saveStmts[type.ordinal()] = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
saveStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.KEY_COLUMN +
@ -398,7 +400,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
if (saveTtlStmts == null) {
saveTtlStmts = new PreparedStatement[DataType.values().length];
for (DataType type : DataType.values()) {
saveTtlStmts[type.ordinal()] = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
saveTtlStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.KEY_COLUMN +
@ -420,7 +422,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
} else if (type == Aggregation.AVG && fetchStmts[Aggregation.SUM.ordinal()] != null) {
fetchStmts[type.ordinal()] = fetchStmts[Aggregation.SUM.ordinal()];
} else {
fetchStmts[type.ordinal()] = getSession().prepare(SELECT_PREFIX +
fetchStmts[type.ordinal()] = prepare(SELECT_PREFIX +
String.join(", ", ModelConstants.getFetchColumnNames(type)) + " FROM " + ModelConstants.TS_KV_CF
+ " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
@ -435,26 +437,26 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
return fetchStmts[aggType.ordinal()];
}
private PreparedStatement getLatestStmt(DataType dataType) {
if (latestInsertStmts == null) {
latestInsertStmts = new PreparedStatement[DataType.values().length];
for (DataType type : DataType.values()) {
latestInsertStmts[type.ordinal()] = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_LATEST_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.KEY_COLUMN +
"," + ModelConstants.TS_COLUMN +
"," + getColumnName(type) + ")" +
" VALUES(?, ?, ?, ?, ?)");
}
private PreparedStatement getLatestStmt() {
if (latestInsertStmt == null) {
latestInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_LATEST_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.KEY_COLUMN +
"," + ModelConstants.TS_COLUMN +
"," + ModelConstants.BOOLEAN_VALUE_COLUMN +
"," + ModelConstants.STRING_VALUE_COLUMN +
"," + ModelConstants.LONG_VALUE_COLUMN +
"," + ModelConstants.DOUBLE_VALUE_COLUMN + ")" +
" VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
}
return latestInsertStmts[dataType.ordinal()];
return latestInsertStmt;
}
private PreparedStatement getPartitionInsertStmt() {
if (partitionInsertStmt == null) {
partitionInsertStmt = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
@ -466,7 +468,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
private PreparedStatement getPartitionInsertTtlStmt() {
if (partitionInsertTtlStmt == null) {
partitionInsertTtlStmt = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
partitionInsertTtlStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
@ -479,7 +481,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
private PreparedStatement getFindLatestStmt() {
if (findLatestStmt == null) {
findLatestStmt = getSession().prepare(SELECT_PREFIX +
findLatestStmt = prepare(SELECT_PREFIX +
ModelConstants.KEY_COLUMN + "," +
ModelConstants.TS_COLUMN + "," +
ModelConstants.STRING_VALUE_COLUMN + "," +
@ -496,7 +498,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
private PreparedStatement getFindAllLatestStmt() {
if (findAllLatestStmt == null) {
findAllLatestStmt = getSession().prepare(SELECT_PREFIX +
findAllLatestStmt = prepare(SELECT_PREFIX +
ModelConstants.KEY_COLUMN + "," +
ModelConstants.TS_COLUMN + "," +
ModelConstants.STRING_VALUE_COLUMN + "," +

25
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<Void> acquireAsync();
void release();
}

164
dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java

@ -0,0 +1,164 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF 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 java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
@Component
@Slf4j
@NoSqlDao
public class BufferedRateLimiter implements AsyncRateLimiter {
private final ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
private final int permitsLimit;
private final int maxPermitWaitTime;
private final AtomicInteger permits;
private final BlockingQueue<LockedFuture> queue;
private final AtomicInteger maxQueueSize = new AtomicInteger();
private final AtomicInteger maxGrantedPermissions = new AtomicInteger();
public BufferedRateLimiter(@Value("${cassandra.query.buffer_size}") int queueLimit,
@Value("${cassandra.query.concurrent_limit}") int permitsLimit,
@Value("${cassandra.query.permit_max_wait_time}") int maxPermitWaitTime) {
this.permitsLimit = permitsLimit;
this.maxPermitWaitTime = maxPermitWaitTime;
this.permits = new AtomicInteger();
this.queue = new LinkedBlockingQueue<>(queueLimit);
}
@Override
public ListenableFuture<Void> acquireAsync() {
if (queue.isEmpty()) {
if (permits.incrementAndGet() <= permitsLimit) {
if (permits.get() > maxGrantedPermissions.get()) {
maxGrantedPermissions.set(permits.get());
}
return Futures.immediateFuture(null);
}
permits.decrementAndGet();
}
return putInQueue();
}
@Override
public void release() {
permits.decrementAndGet();
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) {
lockedFuture.latch.countDown();
} else {
permits.decrementAndGet();
break;
}
} else {
permits.decrementAndGet();
}
}
}
private LockedFuture createLockedFuture() {
CountDownLatch latch = new CountDownLatch(1);
ListenableFuture<Void> future = pool.submit(() -> {
latch.await();
return null;
});
return new LockedFuture(latch, future, System.currentTimeMillis());
}
private ListenableFuture<Void> putInQueue() {
int size = queue.size();
if (size > maxQueueSize.get()) {
maxQueueSize.set(size);
}
if (queue.remainingCapacity() > 0) {
try {
LockedFuture lockedFuture = createLockedFuture();
if (!queue.offer(lockedFuture, 1, TimeUnit.SECONDS)) {
lockedFuture.cancelFuture();
return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Buffer is full. Reject"));
}
if(permits.get() < permitsLimit) {
reprocessQueue();
}
return lockedFuture.future;
} catch (InterruptedException e) {
return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Task interrupted. Reject"));
}
}
return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Buffer is full. Reject"));
}
@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 is [{}] max concurrent [{}] expired [{}] current granted [{}]", maxQueueSize.getAndSet(0),
maxGrantedPermissions.getAndSet(0), expiredCount, permits.get());
}
private class LockedFuture {
final CountDownLatch latch;
final ListenableFuture<Void> future;
final long createTime;
public LockedFuture(CountDownLatch latch, ListenableFuture<Void> future, long createTime) {
this.latch = latch;
this.future = future;
this.createTime = createTime;
}
void cancelFuture() {
future.cancel(false);
latch.countDown();
}
boolean isExpired() {
return (System.currentTimeMillis() - createTime) > maxPermitWaitTime;
}
}
}

156
dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java

@ -0,0 +1,156 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF 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 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.util.AsyncRateLimiter;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
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 IllegalArgumentException()));
resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
Thread.sleep(1000L);
verify(rateLimiter).acquireAsync();
try {
assertTrue(resultSetFuture.isDone());
fail();
} catch (Exception e) {
assertTrue(e instanceof IllegalStateException);
Throwable actualCause = e.getCause();
assertTrue(actualCause instanceof ExecutionException);
}
verifyNoMoreInteractions(session, rateLimiter, statement);
}
@Test
public void getUninterruptiblyDelegateToCassandra() throws InterruptedException, ExecutionException {
when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null));
when(session.executeAsync(statement)).thenReturn(realFuture);
Mockito.doAnswer((Answer<Void>) invocation -> {
Object[] args = invocation.getArguments();
Runnable task = (Runnable) args[0];
task.run();
return null;
}).when(realFuture).addListener(Mockito.any(), Mockito.any());
when(realFuture.getUninterruptibly()).thenReturn(rows);
resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
ResultSet actual = resultSetFuture.getUninterruptibly();
assertSame(rows, actual);
verify(rateLimiter, times(1)).acquireAsync();
verify(rateLimiter, times(1)).release();
}
@Test
public void addListenerAllowsFutureTransformation() throws InterruptedException, ExecutionException {
when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null));
when(session.executeAsync(statement)).thenReturn(realFuture);
Mockito.doAnswer((Answer<Void>) invocation -> {
Object[] args = invocation.getArguments();
Runnable task = (Runnable) args[0];
task.run();
return null;
}).when(realFuture).addListener(Mockito.any(), Mockito.any());
when(realFuture.get()).thenReturn(rows);
when(rows.one()).thenReturn(row);
resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
ListenableFuture<Row> transform = Futures.transform(resultSetFuture, ResultSet::one);
Row actualRow = transform.get();
assertSame(row, actualRow);
verify(rateLimiter, times(1)).acquireAsync();
verify(rateLimiter, times(1)).release();
}
@Test
public void immidiateCassandraExceptionReturnsPermit() throws InterruptedException, ExecutionException {
when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null));
when(session.executeAsync(statement)).thenThrow(new UnsupportedFeatureException(ProtocolVersion.V3, "hjg"));
resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
ListenableFuture<Row> transform = Futures.transform(resultSetFuture, ResultSet::one);
try {
transform.get();
fail();
} catch (Exception e) {
assertTrue(e instanceof ExecutionException);
}
verify(rateLimiter, times(1)).acquireAsync();
verify(rateLimiter, times(1)).release();
}
@Test
public void queryTimeoutReturnsPermit() throws InterruptedException, ExecutionException {
when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null));
when(session.executeAsync(statement)).thenReturn(realFuture);
Mockito.doAnswer((Answer<Void>) invocation -> {
Object[] args = invocation.getArguments();
Runnable task = (Runnable) args[0];
task.run();
return null;
}).when(realFuture).addListener(Mockito.any(), Mockito.any());
when(realFuture.get()).thenThrow(new ExecutionException("Fail", new TimeoutException("timeout")));
resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
ListenableFuture<Row> transform = Futures.transform(resultSetFuture, ResultSet::one);
try {
transform.get();
fail();
} catch (Exception e) {
assertTrue(e instanceof ExecutionException);
}
verify(rateLimiter, times(1)).acquireAsync();
verify(rateLimiter, times(1)).release();
}
}

134
dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java

@ -0,0 +1,134 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF 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 javax.annotation.Nullable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.*;
public class BufferedRateLimiterTest {
@Test
public void finishedFutureReturnedIfPermitsAreGranted() {
BufferedRateLimiter limiter = new BufferedRateLimiter(10, 10, 100);
ListenableFuture<Void> actual = limiter.acquireAsync();
assertTrue(actual.isDone());
}
@Test
public void notFinishedFutureReturnedIfPermitsAreNotGranted() {
BufferedRateLimiter limiter = new BufferedRateLimiter(10, 1, 100);
ListenableFuture<Void> actual1 = limiter.acquireAsync();
ListenableFuture<Void> actual2 = limiter.acquireAsync();
assertTrue(actual1.isDone());
assertFalse(actual2.isDone());
}
@Test
public void failedFutureReturnedIfQueueIsfull() {
BufferedRateLimiter limiter = new BufferedRateLimiter(1, 1, 100);
ListenableFuture<Void> actual1 = limiter.acquireAsync();
ListenableFuture<Void> actual2 = limiter.acquireAsync();
ListenableFuture<Void> actual3 = limiter.acquireAsync();
assertTrue(actual1.isDone());
assertFalse(actual2.isDone());
assertTrue(actual3.isDone());
try {
actual3.get();
fail();
} catch (Exception e) {
assertTrue(e instanceof ExecutionException);
Throwable actualCause = e.getCause();
assertTrue(actualCause instanceof IllegalStateException);
assertEquals("Rate Limit Buffer is full. Reject", actualCause.getMessage());
}
}
@Test
public void releasedPermitTriggerTasksFromQueue() throws InterruptedException {
BufferedRateLimiter limiter = new BufferedRateLimiter(10, 2, 100);
ListenableFuture<Void> actual1 = limiter.acquireAsync();
ListenableFuture<Void> actual2 = limiter.acquireAsync();
ListenableFuture<Void> actual3 = limiter.acquireAsync();
ListenableFuture<Void> actual4 = limiter.acquireAsync();
assertTrue(actual1.isDone());
assertTrue(actual2.isDone());
assertFalse(actual3.isDone());
assertFalse(actual4.isDone());
limiter.release();
TimeUnit.MILLISECONDS.sleep(100L);
assertTrue(actual3.isDone());
assertFalse(actual4.isDone());
limiter.release();
TimeUnit.MILLISECONDS.sleep(100L);
assertTrue(actual4.isDone());
}
@Test
public void permitsReleasedInConcurrentMode() throws InterruptedException {
BufferedRateLimiter limiter = new BufferedRateLimiter(10, 2, 100);
AtomicInteger actualReleased = new AtomicInteger();
AtomicInteger actualRejected = new AtomicInteger();
ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(5));
for (int i = 0; i < 100; i++) {
ListenableFuture<ListenableFuture<Void>> submit = pool.submit(limiter::acquireAsync);
Futures.addCallback(submit, new FutureCallback<ListenableFuture<Void>>() {
@Override
public void onSuccess(@Nullable ListenableFuture<Void> result) {
Futures.addCallback(result, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
limiter.release();
actualReleased.incrementAndGet();
}
@Override
public void onFailure(Throwable t) {
actualRejected.incrementAndGet();
}
});
}
@Override
public void onFailure(Throwable t) {
}
});
}
TimeUnit.SECONDS.sleep(2);
assertTrue("Unexpected released count " + actualReleased.get(),
actualReleased.get() > 10 && actualReleased.get() < 20);
assertTrue("Unexpected rejected count " + actualRejected.get(),
actualRejected.get() > 80 && actualRejected.get() < 90);
}
}

5
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

22
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeConfiguration.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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;
public interface NodeConfiguration {
NodeConfiguration defaultConfiguration();
}

5
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java

@ -35,15 +35,16 @@ public @interface RuleNode {
String nodeDetails();
Class<? extends NodeConfiguration> configClazz();
boolean inEnabled() default true;
boolean outEnabled() default true;
ComponentScope scope() default ComponentScope.TENANT;
String defaultConfigResource() default "EmptyNodeConfig.json";
String[] relationTypes() default {"Success", "Failure"};
boolean customRelations() default false;
}

2
rule-engine/rule-engine-api/src/main/resources/EmptyNodeConfig.json

@ -1,2 +0,0 @@
{
}

2
rule-engine/rule-engine-api/src/main/resources/EmptyNodeDescriptor.json

@ -1,2 +0,0 @@
{
}

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java

@ -30,6 +30,7 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
@RuleNode(
type = ComponentType.FILTER,
name = "script", relationTypes = {"True", "False", "Failure"},
configClazz = TbJsFilterNodeConfiguration.class,
nodeDescription = "Filter incoming messages using JS script",
nodeDetails = "Evaluate incoming Message with configured JS condition. " +
"If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." +

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java

@ -16,9 +16,17 @@
package org.thingsboard.rule.engine.filter;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
@Data
public class TbJsFilterNodeConfiguration {
public class TbJsFilterNodeConfiguration implements NodeConfiguration {
private String jsScript;
@Override
public TbJsFilterNodeConfiguration defaultConfiguration() {
TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration();
configuration.setJsScript("msg.passed < 15 && msg.name === 'Vit' && meta.temp == 10 && msg.bigObj.prop == 42;");
return configuration;
}
}

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java

@ -31,6 +31,7 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
@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. " +

17
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java

@ -15,14 +15,29 @@
*/
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 {
public class TbJsSwitchNodeConfiguration implements NodeConfiguration {
private String jsScript;
private Set<String> allowedRelations;
private boolean routeToAllWithNoCheck;
@Override
public TbJsSwitchNodeConfiguration defaultConfiguration() {
TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration();
configuration.setJsScript("function nextRelation(meta, msg) {\n" +
" return ['one','nine'];" +
"};\n" +
"\n" +
"nextRelation(meta, msg);");
configuration.setAllowedRelations(Sets.newHashSet("one", "two"));
configuration.setRouteToAllWithNoCheck(false);
return configuration;
}
}

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.msg.TbMsg;
@RuleNode(
type = ComponentType.FILTER,
name = "message type",
configClazz = TbMsgTypeFilterNodeConfiguration.class,
nodeDescription = "Filter incoming messages by Message Type",
nodeDetails = "Evaluate incoming Message with configured JS condition. " +
"If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.")

11
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<String> messageTypes;
@Override
public TbMsgTypeFilterNodeConfiguration defaultConfiguration() {
TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration();
configuration.setMessageTypes(Arrays.asList("GET_ATTRIBUTES","POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST"));
return configuration;
}
}

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java

@ -38,6 +38,7 @@ import static org.thingsboard.server.common.data.DataConstants.*;
@Slf4j
@RuleNode(type = ComponentType.ENRICHMENT,
name = "originator attributes",
configClazz = TbGetAttributesNodeConfiguration.class,
nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
"with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " +

13
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java

@ -16,14 +16,16 @@
package org.thingsboard.rule.engine.metadata;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import java.util.Collections;
import java.util.List;
/**
* Created by ashvayka on 19.01.18.
*/
@Data
public class TbGetAttributesNodeConfiguration {
public class TbGetAttributesNodeConfiguration implements NodeConfiguration {
private List<String> clientAttributeNames;
private List<String> sharedAttributeNames;
@ -31,4 +33,13 @@ public class TbGetAttributesNodeConfiguration {
private List<String> latestTsKeyNames;
@Override
public TbGetAttributesNodeConfiguration defaultConfiguration() {
TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
configuration.setClientAttributeNames(Collections.emptyList());
configuration.setSharedAttributeNames(Collections.emptyList());
configuration.setServerAttributeNames(Collections.emptyList());
configuration.setLatestTsKeyNames(Collections.emptyList());
return configuration;
}
}

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java

@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
@RuleNode(
type = ComponentType.ENRICHMENT,
name="customer attributes",
configClazz = TbGetEntityAttrNodeConfiguration.class,
nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"To access those attributes in other nodes this template can be used " +

14
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java

@ -16,13 +16,25 @@
package org.thingsboard.rule.engine.metadata;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Data
public class TbGetEntityAttrNodeConfiguration {
public class TbGetEntityAttrNodeConfiguration implements NodeConfiguration {
private Map<String, String> attrMapping;
private boolean isTelemetry = false;
@Override
public TbGetEntityAttrNodeConfiguration defaultConfiguration() {
TbGetEntityAttrNodeConfiguration configuration = new TbGetEntityAttrNodeConfiguration();
Map<String, String> attrMapping = new HashMap<>();
attrMapping.putIfAbsent("temperature", "tempo");
configuration.setAttrMapping(attrMapping);
configuration.setTelemetry(true);
return configuration;
}
}

19
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java

@ -16,11 +16,28 @@
package org.thingsboard.rule.engine.metadata;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import java.util.HashMap;
import java.util.Map;
@Data
public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration {
public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration {
private String relationType;
private EntitySearchDirection direction;
@Override
public TbGetRelatedAttrNodeConfiguration defaultConfiguration() {
TbGetRelatedAttrNodeConfiguration configuration = new TbGetRelatedAttrNodeConfiguration();
Map<String, String> attrMapping = new HashMap<>();
attrMapping.putIfAbsent("temperature", "tempo");
configuration.setAttrMapping(attrMapping);
configuration.setTelemetry(true);
configuration.setRelationType(EntityRelation.CONTAINS_TYPE);
configuration.setDirection(EntitySearchDirection.FROM);
return configuration;
}
}

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java

@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
@RuleNode(
type = ComponentType.ENRICHMENT,
name="related attributes",
configClazz = TbGetRelatedAttrNodeConfiguration.class,
nodeDescription = "Add Originators Related Entity Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
"If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " +

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
@RuleNode(
type = ComponentType.ENRICHMENT,
name="tenant attributes",
configClazz = TbGetEntityAttrNodeConfiguration.class,
nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"To access those attributes in other nodes this template can be used " +

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java

@ -36,6 +36,7 @@ import java.util.HashSet;
@RuleNode(
type = ComponentType.TRANSFORMATION,
name="change originator",
configClazz = TbChangeOriginatorNodeConfiguration.class,
nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity",
nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
"If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ")

14
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java

@ -16,12 +16,24 @@
package org.thingsboard.rule.engine.transform;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
@Data
public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration{
public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
private String originatorSource;
private EntitySearchDirection direction;
private String relationType;
@Override
public TbChangeOriginatorNodeConfiguration defaultConfiguration() {
TbChangeOriginatorNodeConfiguration configuration = new TbChangeOriginatorNodeConfiguration();
configuration.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE);
configuration.setDirection(EntitySearchDirection.FROM);
configuration.setRelationType(EntityRelation.CONTAINS_TYPE);
configuration.setStartNewChain(false);
return configuration;
}
}

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java

@ -27,6 +27,7 @@ import javax.script.Bindings;
@RuleNode(
type = ComponentType.TRANSFORMATION,
name = "script",
configClazz = TbTransformMsgNodeConfiguration.class,
nodeDescription = "Change Message payload and Metadata using JavaScript",
nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " +
"<code>meta</code> - is a Message metadata.<br/>" +

11
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java

@ -16,9 +16,18 @@
package org.thingsboard.rule.engine.transform;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
@Data
public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguration {
public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
private String jsScript;
@Override
public TbTransformMsgNodeConfiguration defaultConfiguration() {
TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration();
configuration.setStartNewChain(false);
configuration.setJsScript("msg.passed = msg.passed * meta.temp; msg.bigObj.newProp = 'Ukraine' ");
return configuration;
}
}

25
ui/src/app/api/rule-chain.service.js

@ -153,16 +153,21 @@ function RuleChainService($http, $q, $filter, types, componentDescriptorService)
return deferred.promise;
}
function getRuleNodeSupportedLinks(nodeType) { //eslint-disable-line
//TODO:
var deferred = $q.defer();
var linkLabels = [
{ name: 'Success', custom: false },
{ name: 'Fail', custom: false },
{ name: 'Custom', custom: true },
];
deferred.resolve(linkLabels);
return deferred.promise;
function getRuleNodeSupportedLinks(component) {
var relationTypes = component.configurationDescriptor.nodeDefinition.relationTypes;
var customRelations = component.configurationDescriptor.nodeDefinition.customRelations;
var linkLabels = [];
for (var i=0;i<relationTypes.length;i++) {
linkLabels.push({
name: relationTypes[i], custom: false
});
}
if (customRelations) {
linkLabels.push(
{ name: 'Custom', custom: true }
);
}
return linkLabels;
}
function getRuleNodeComponents() {

45
ui/src/app/components/ace-editor-fix.js

@ -0,0 +1,45 @@
/*
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default function fixAceEditor(aceEditor) {
aceEditor.$blockScrolling = Infinity;
aceEditor.on("showGutterTooltip", function (tooltip) {
if (!tooltip.isAttachedToBody) {
document.body.appendChild(tooltip.$element); //eslint-disable-line
tooltip.isAttachedToBody = true;
onElementRemoved(tooltip.$parentNode, () => {
if (tooltip.$element.parentNode != null) {
tooltip.$element.parentNode.removeChild(tooltip.$element);
}
});
}
});
}
function onElementRemoved(element, callback) {
if (!document.body.contains(element)) { //eslint-disable-line
callback();
} else {
var observer;
observer = new MutationObserver(function(mutations) { //eslint-disable-line
if (!document.body.contains(element)) { //eslint-disable-line
callback();
observer.disconnect();
}
});
observer.observe(document.body, {childList: true}); //eslint-disable-line
}
}

3
ui/src/app/components/js-func.directive.js

@ -22,6 +22,8 @@ import thingsboardToast from '../services/toast';
import thingsboardUtils from '../common/utils.service';
import thingsboardExpandFullscreen from './expand-fullscreen.directive';
import fixAceEditor from './ace-editor-fix';
/* eslint-disable import/no-unresolved, import/default */
import jsFuncTemplate from './js-func.tpl.html';
@ -83,6 +85,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
scope.js_editor.session.on("change", function () {
scope.cleanupJsErrors();
});
fixAceEditor(_ace);
}
};

168
ui/src/app/components/json-object-edit.directive.js

@ -0,0 +1,168 @@
/*
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import './json-object-edit.scss';
import 'brace/ext/language_tools';
import 'brace/mode/json';
import 'ace-builds/src-min-noconflict/snippets/json';
import fixAceEditor from './ace-editor-fix';
/* eslint-disable import/no-unresolved, import/default */
import jsonObjectEditTemplate from './json-object-edit.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
export default angular.module('thingsboard.directives.jsonObjectEdit', [])
.directive('tbJsonObjectEdit', JsonObjectEdit)
.name;
/*@ngInject*/
function JsonObjectEdit($compile, $templateCache, $document, toast, utils) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(jsonObjectEditTemplate);
element.html(template);
scope.label = attrs.label;
scope.objectValid = true;
scope.validationError = '';
scope.json_editor;
scope.onFullscreenChanged = function () {
updateEditorSize();
};
function updateEditorSize() {
if (scope.json_editor) {
scope.json_editor.resize();
scope.json_editor.renderer.updateFull();
}
}
scope.jsonEditorOptions = {
useWrapMode: true,
mode: 'json',
advanced: {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
},
onLoad: function (_ace) {
scope.json_editor = _ace;
scope.json_editor.session.on("change", function () {
scope.cleanupJsonErrors();
});
fixAceEditor(_ace);
}
};
scope.cleanupJsonErrors = function () {
toast.hide();
};
scope.updateValidity = function () {
ngModelCtrl.$setValidity('objectValid', scope.objectValid);
};
scope.$watch('contentBody', function (newVal, prevVal) {
if (!angular.equals(newVal, prevVal)) {
var object = scope.validate();
ngModelCtrl.$setViewValue(object);
scope.updateValidity();
}
});
ngModelCtrl.$render = function () {
var object = ngModelCtrl.$viewValue;
var content = '';
try {
if (object) {
content = angular.toJson(object, true);
}
} catch (e) {
//
}
scope.contentBody = content;
};
scope.showError = function (error) {
var toastParent = angular.element('#tb-json-panel', element);
toast.showError(error, toastParent, 'bottom left');
};
scope.validate = function () {
if (!scope.contentBody || !scope.contentBody.length) {
if (scope.required) {
scope.validationError = 'Json object is required.';
scope.objectValid = false;
} else {
scope.validationError = '';
scope.objectValid = true;
}
return null;
} else {
try {
var object = angular.fromJson(scope.contentBody);
scope.validationError = '';
scope.objectValid = true;
return object;
} catch (e) {
var details = utils.parseException(e);
var errorInfo = 'Error:';
if (details.name) {
errorInfo += ' ' + details.name + ':';
}
if (details.message) {
errorInfo += ' ' + details.message;
}
scope.validationError = errorInfo;
scope.objectValid = false;
return null;
}
}
};
scope.$on('form-submit', function () {
if (!scope.readonly) {
scope.cleanupJsonErrors();
if (!scope.objectValid) {
scope.showError(scope.validationError);
}
}
});
scope.$on('update-ace-editor-size', function () {
updateEditorSize();
});
$compile(element.contents())(scope);
}
return {
restrict: "E",
require: "^ngModel",
scope: {
required:'=ngRequired',
readonly:'=ngReadonly',
fillHeight:'=?'
},
link: linker
};
}

35
ui/src/app/components/json-object-edit.scss

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
tb-json-object-edit {
position: relative;
.fill-height {
height: 100%;
}
}
.tb-json-object-panel {
margin-left: 15px;
border: 1px solid #C0C0C0;
height: 100%;
#tb-json-input {
min-width: 200px;
width: 100%;
height: 100%;
&:not(.fill-height) {
min-height: 200px;
}
}
}

34
ui/src/app/components/json-object-edit.tpl.html

@ -0,0 +1,34 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
<div layout="row" layout-align="start center">
<label class="tb-title no-padding"
ng-class="{'tb-required': required,
'tb-readonly': readonly,
'tb-error': !objectValid}">{{ label }}</label>
<span flex></span>
<md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
</div>
<div flex id="tb-json-panel" class="tb-json-object-panel" layout="column">
<div flex id="tb-json-input" ng-class="{'fill-height': fillHeight}"
ng-readonly="readonly"
ui-ace="jsonEditorOptions"
ng-model="contentBody">
</div>
</div>
</div>

8
ui/src/app/components/react/json-form-ace-editor.jsx

@ -23,6 +23,8 @@ import FlatButton from 'material-ui/FlatButton';
import 'brace/ext/language_tools';
import 'brace/theme/github';
import fixAceEditor from './../ace-editor-fix';
class ThingsboardAceEditor extends React.Component {
constructor(props) {
@ -31,6 +33,7 @@ class ThingsboardAceEditor extends React.Component {
this.onBlur = this.onBlur.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onTidy = this.onTidy.bind(this);
this.onLoad = this.onLoad.bind(this);
var value = props.value ? props.value + '' : '';
this.state = {
value: value,
@ -72,6 +75,10 @@ class ThingsboardAceEditor extends React.Component {
}
}
onLoad(editor) {
fixAceEditor(editor);
}
render() {
const styles = reactCSS({
@ -117,6 +124,7 @@ class ThingsboardAceEditor extends React.Component {
onChange={this.onValueChanged}
onFocus={this.onFocus}
onBlur={this.onBlur}
onLoad={this.onLoad}
name={this.props.form.title}
value={this.state.value}
readOnly={this.props.form.readonly}

5
ui/src/app/components/widget/widget-config.directive.js

@ -23,6 +23,8 @@ import thingsboardJsonForm from '../json-form.directive';
import thingsboardManageWidgetActions from './action/manage-widget-actions.directive';
import 'angular-ui-ace';
import fixAceEditor from './../ace-editor-fix';
import './widget-config.scss';
/* eslint-disable import/no-unresolved, import/default */
@ -72,6 +74,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
},
onLoad: function (_ace) {
fixAceEditor(_ace);
}
};

8
ui/src/app/extension/extensions-forms/extension-form-opc.directive.js

@ -128,8 +128,8 @@ export default function ExtensionFormOpcDirective($compile, $templateCache, $tra
let addedFile = event.target.result;
if (addedFile && addedFile.length > 0) {
model[options.fileName] = $file.name;
model[options.file] = addedFile.replace(/^data.*base64,/, "");
model[options.location] = $file.name;
model[options.fileContent] = addedFile.replace(/^data.*base64,/, "");
}
}
@ -142,8 +142,8 @@ export default function ExtensionFormOpcDirective($compile, $templateCache, $tra
scope.clearFile = function(model, options) {
scope.theForm.$setDirty();
model[options.fileName] = null;
model[options.file] = null;
model[options.location] = null;
model[options.fileContent] = null;
};

10
ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html

@ -212,8 +212,8 @@
</md-input-container>
<section class="dropdown-section">
<div class="tb-container" ng-class="{'ng-invalid':!server.keystore.file}">
<span ng-init='fieldsToFill = {"fileName":"fileName", "file":"file"}'></span>
<div class="tb-container" ng-class="{'ng-invalid':!server.keystore.fileContent}">
<span ng-init='fieldsToFill = {"location":"location", "fileContent":"fileContent"}'></span>
<label class="tb-label" translate>extension.opc-keystore-location</label>
<div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, server.keystore, fieldsToFill)' class="tb-file-select-container">
<div class="tb-file-clear-container">
@ -231,14 +231,14 @@
class="file-input"
flow-btn id="dropFileKeystore_{{serverIndex}}"
name="keystoreFile"
ng-model="server.keystore.file"
ng-model="server.keystore.fileContent"
>
</div>
</div>
</div>
<div class="dropdown-messages">
<div ng-if="!server.keystore[fieldsToFill.fileName]" class="tb-error-message" translate>extension.no-file</div>
<div ng-if="server.keystore[fieldsToFill.fileName]">{{server.keystore[fieldsToFill.fileName]}}</div>
<div ng-if="!server.keystore[fieldsToFill.location]" class="tb-error-message" translate>extension.no-file</div>
<div ng-if="server.keystore[fieldsToFill.location]">{{server.keystore[fieldsToFill.location]}}</div>
</div>
</section>

4
ui/src/app/layout/index.js

@ -29,6 +29,7 @@ import thingsboardNoAnimate from '../components/no-animate.directive';
import thingsboardOnFinishRender from '../components/finish-render.directive';
import thingsboardSideMenu from '../components/side-menu.directive';
import thingsboardDashboardAutocomplete from '../components/dashboard-autocomplete.directive';
import thingsboardJsonObjectEdit from '../components/json-object-edit.directive';
import thingsboardUserMenu from './user-menu.directive';
@ -90,7 +91,8 @@ export default angular.module('thingsboard.home', [
thingsboardNoAnimate,
thingsboardOnFinishRender,
thingsboardSideMenu,
thingsboardDashboardAutocomplete
thingsboardDashboardAutocomplete,
thingsboardJsonObjectEdit
])
.config(HomeRoutes)
.controller('HomeController', HomeController)

1
ui/src/app/locale/locale.constant.js

@ -1179,6 +1179,7 @@ export default angular.module('thingsboard.locale', [])
"delete": "Delete rule node",
"rulenode-details": "Rule node details",
"debug-mode": "Debug mode",
"configuration": "Configuration",
"link-details": "Rule node link details",
"add-link": "Add link",
"link-label": "Link label",

82
ui/src/app/rulechain/rulechain.controller.js

@ -151,6 +151,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
},
'mouseLeave': function () {
destroyTooltips();
},
'mouseDown': function () {
destroyTooltips();
}
}
};
@ -226,16 +229,12 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
edgeDoubleClick: function (event, edge) {
var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
ruleChainService.getRuleNodeSupportedLinks(sourceNode.component.clazz).then(
(labels) => {
vm.isEditingRuleNode = false;
vm.editingRuleNode = null;
vm.editingRuleNodeLinkLabels = labels;
vm.isEditingRuleNodeLink = true;
vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
vm.editingRuleNodeLink = angular.copy(edge);
}
);
vm.isEditingRuleNode = false;
vm.editingRuleNode = null;
vm.editingRuleNodeLinkLabels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
vm.isEditingRuleNodeLink = true;
vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
vm.editingRuleNodeLink = angular.copy(edge);
}
},
nodeCallbacks: {
@ -267,16 +266,10 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
deferred.resolve(edge);
}
} else {
ruleChainService.getRuleNodeSupportedLinks(sourceNode.component.clazz).then(
(labels) => {
addRuleNodeLink(event, edge, labels).then(
(link) => {
deferred.resolve(link);
},
() => {
deferred.reject();
}
);
var labels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
addRuleNodeLink(event, edge, labels).then(
(link) => {
deferred.resolve(link);
},
() => {
deferred.reject();
@ -309,24 +302,19 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
y: 10+50*model.nodes.length,
connectors: []
};
if (componentType == types.ruleNodeType.RULE_CHAIN.value) {
node.connectors.push(
{
type: flowchartConstants.leftConnectorType,
id: model.nodes.length
}
);
} else {
if (ruleNodeComponent.configurationDescriptor.nodeDefinition.inEnabled) {
node.connectors.push(
{
type: flowchartConstants.leftConnectorType,
id: model.nodes.length*2
id: model.nodes.length * 2
}
);
}
if (ruleNodeComponent.configurationDescriptor.nodeDefinition.outEnabled) {
node.connectors.push(
{
type: flowchartConstants.rightConnectorType,
id: model.nodes.length*2+1
id: model.nodes.length * 2 + 1
}
);
}
@ -398,17 +386,24 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
name: ruleNode.name,
nodeClass: vm.types.ruleNodeType[component.type].nodeClass,
icon: vm.types.ruleNodeType[component.type].icon,
connectors: [
connectors: []
};
if (component.configurationDescriptor.nodeDefinition.inEnabled) {
node.connectors.push(
{
type: flowchartConstants.leftConnectorType,
id: vm.nextConnectorID++
},
}
);
}
if (component.configurationDescriptor.nodeDefinition.outEnabled) {
node.connectors.push(
{
type: flowchartConstants.rightConnectorType,
id: vm.nextConnectorID++
}
]
};
);
}
nodes.push(node);
vm.ruleChainModel.nodes.push(node);
}
@ -590,6 +585,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
}
function addRuleNode($event, ruleNode) {
ruleNode.configuration = angular.copy(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration);
$mdDialog.show({
controller: 'AddRuleNodeController',
controllerAs: 'vm',
@ -601,13 +599,15 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
}).then(function (ruleNode) {
ruleNode.id = vm.nextNodeID++;
ruleNode.connectors = [];
ruleNode.connectors.push(
{
id: vm.nextConnectorID++,
type: flowchartConstants.leftConnectorType
}
);
if (ruleNode.component.type != types.ruleNodeType.RULE_CHAIN.value) {
if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) {
ruleNode.connectors.push(
{
id: vm.nextConnectorID++,
type: flowchartConstants.leftConnectorType
}
);
}
if (ruleNode.component.configurationDescriptor.nodeDefinition.outEnabled) {
ruleNode.connectors.push(
{
id: vm.nextConnectorID++,

5
ui/src/app/rulechain/rulenode-fieldset.tpl.html

@ -38,6 +38,11 @@
ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }}
</md-checkbox>
</md-input-container>
<tb-json-object-edit class="tb-rule-node-configuration-json" ng-model="ruleNode.configuration"
label="{{ 'rulenode.configuration' | translate }}"
ng-required="true"
fill-height="true">
</tb-json-object-edit>
<md-input-container class="md-block">
<label translate>rulenode.description</label>
<textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>

2
ui/src/app/rulechain/rulenode.directive.js

@ -14,6 +14,8 @@
* limitations under the License.
*/
import './rulenode.scss';
/* eslint-disable import/no-unresolved, import/default */
import ruleNodeFieldsetTemplate from './rulenode-fieldset.tpl.html';

22
ui/src/app/rulechain/rulenode.scss

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-rulenode {
tb-json-object-edit.tb-rule-node-configuration-json {
height: 300px;
display: block;
}
}

2
ui/src/app/rulechain/rulenode.tpl.html

@ -19,7 +19,7 @@
id="{{node.id}}"
ng-attr-style="position: absolute; top: {{ node.y }}px; left: {{ node.x }}px;"
ng-dblclick="callbacks.doubleClick($event, node)"
ng-mouseover="callbacks.mouseOver($event, node)"
ng-mousedown="callbacks.mouseDown($event, node)"
ng-mouseenter="callbacks.mouseEnter($event, node)"
ng-mouseleave="callbacks.mouseLeave($event, node)">
<div class="tb-rule-node {{node.nodeClass}}">

18
ui/src/scss/main.scss

@ -203,6 +203,12 @@ md-sidenav {
* THINGSBOARD SPECIFIC
***********************/
$swift-ease-out-duration: 0.4s !default;
$swift-ease-out-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
$input-label-float-offset: 6px !default;
$input-label-float-scale: 0.75 !default;
label {
&.tb-title {
pointer-events: none;
@ -213,6 +219,18 @@ label {
&.no-padding {
padding-bottom: 0px;
}
&.tb-required:after {
content: ' *';
font-size: 13px;
vertical-align: top;
color: rgba(0,0,0,0.54);
}
&.tb-error {
color: rgb(221,44,0);
&.tb-required:after {
color: rgb(221,44,0);
}
}
}
}

Loading…
Cancel
Save