Commit da7955f97d71af4217f3ffb652b57e7effaa0fba

Authored by Andrew Shvayka
2 parents f49d480d 02c4d6b0

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

Showing 61 changed files with 1298 additions and 162 deletions
... ... @@ -19,6 +19,7 @@ import org.springframework.boot.SpringApplication;
19 19 import org.springframework.boot.SpringBootConfiguration;
20 20 import org.springframework.context.annotation.ComponentScan;
21 21 import org.springframework.scheduling.annotation.EnableAsync;
  22 +import org.springframework.scheduling.annotation.EnableScheduling;
22 23 import springfox.documentation.swagger2.annotations.EnableSwagger2;
23 24
24 25 import java.util.Arrays;
... ... @@ -26,6 +27,7 @@ import java.util.Arrays;
26 27 @SpringBootConfiguration
27 28 @EnableAsync
28 29 @EnableSwagger2
  30 +@EnableScheduling
29 31 @ComponentScan({"org.thingsboard.server"})
30 32 public class ThingsboardServerApplication {
31 33
... ...
... ... @@ -28,9 +28,14 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
28 28 import org.thingsboard.server.common.data.plugin.PluginMetaData;
29 29 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
30 30 import org.thingsboard.server.common.msg.cluster.ServerAddress;
  31 +import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
  32 +import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
  33 +import org.thingsboard.server.common.msg.session.MsgType;
31 34 import org.thingsboard.server.extensions.api.plugins.Plugin;
32 35 import org.thingsboard.server.extensions.api.plugins.PluginInitializationException;
33 36 import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
  37 +import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
  38 +import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
34 39 import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
35 40 import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
36 41 import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
... ... @@ -98,7 +103,20 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
98 103
99 104 public void onRuleToPluginMsg(RuleToPluginMsgWrapper msg) throws RuleException {
100 105 if (state == ComponentLifecycleState.ACTIVE) {
101   - pluginImpl.process(trustedCtx, msg.getRuleTenantId(), msg.getRuleId(), msg.getMsg());
  106 + try {
  107 + pluginImpl.process(trustedCtx, msg.getRuleTenantId(), msg.getRuleId(), msg.getMsg());
  108 + } catch (Exception ex) {
  109 + logger.debug("[{}] Failed to process RuleToPlugin msg: [{}] [{}]", tenantId, msg.getMsg(), ex);
  110 + RuleToPluginMsg ruleMsg = msg.getMsg();
  111 + MsgType responceMsgType = MsgType.RULE_ENGINE_ERROR;
  112 + Integer requestId = 0;
  113 + if (ruleMsg.getPayload() instanceof FromDeviceRequestMsg) {
  114 + requestId = ((FromDeviceRequestMsg) ruleMsg.getPayload()).getRequestId();
  115 + }
  116 + trustedCtx.reply(
  117 + new ResponsePluginToRuleMsg(ruleMsg.getUid(), tenantId, msg.getRuleId(),
  118 + BasicStatusCodeResponse.onError(responceMsgType, requestId, ex)));
  119 + }
102 120 } else {
103 121 //TODO: reply with plugin suspended message
104 122 }
... ...
... ... @@ -180,7 +180,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
180 180 return scannedComponent;
181 181 }
182 182
183   - private NodeDefinition prepareNodeDefinition(RuleNode nodeAnnotation) throws IOException {
  183 + private NodeDefinition prepareNodeDefinition(RuleNode nodeAnnotation) throws Exception {
184 184 NodeDefinition nodeDefinition = new NodeDefinition();
185 185 nodeDefinition.setDetails(nodeAnnotation.nodeDetails());
186 186 nodeDefinition.setDescription(nodeAnnotation.nodeDescription());
... ... @@ -188,9 +188,10 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
188 188 nodeDefinition.setOutEnabled(nodeAnnotation.outEnabled());
189 189 nodeDefinition.setRelationTypes(nodeAnnotation.relationTypes());
190 190 nodeDefinition.setCustomRelations(nodeAnnotation.customRelations());
191   - String defaultConfigResourceName = nodeAnnotation.defaultConfigResource();
192   - nodeDefinition.setDefaultConfiguration(mapper.readTree(
193   - Resources.toString(Resources.getResource(defaultConfigResourceName), Charsets.UTF_8)));
  191 + Class<? extends NodeConfiguration> configClazz = nodeAnnotation.configClazz();
  192 + NodeConfiguration config = configClazz.newInstance();
  193 + NodeConfiguration defaultConfiguration = config.defaultConfiguration();
  194 + nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration));
194 195 return nodeDefinition;
195 196 }
196 197
... ...
... ... @@ -187,7 +187,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
187 187
188 188 @Override
189 189 public void loadSystemRules() throws Exception {
190   - loadRules(Paths.get(dataDir, JSON_DIR, SYSTEM_DIR, RULES_DIR), null);
  190 +// loadRules(Paths.get(dataDir, JSON_DIR, SYSTEM_DIR, RULES_DIR), null);
191 191 }
192 192
193 193 @Override
... ... @@ -228,7 +228,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
228 228 "Raspberry Pi GPIO control sample application");
229 229
230 230 loadPlugins(Paths.get(dataDir, JSON_DIR, DEMO_DIR, PLUGINS_DIR), demoTenant.getId());
231   - loadRules(Paths.get(dataDir, JSON_DIR, DEMO_DIR, RULES_DIR), demoTenant.getId());
  231 +// loadRules(Paths.get(dataDir, JSON_DIR, DEMO_DIR, RULES_DIR), demoTenant.getId());
232 232 loadDashboards(Paths.get(dataDir, JSON_DIR, DEMO_DIR, DASHBOARDS_DIR), demoTenant.getId(), null);
233 233 }
234 234
... ...
... ... @@ -181,6 +181,10 @@ cassandra:
181 181 default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
182 182 # Specify partitioning size for timestamp key-value storage. Example MINUTES, HOURS, DAYS, MONTHS
183 183 ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
  184 + buffer_size: "${CASSANDRA_QUERY_BUFFER_SIZE:200000}"
  185 + concurrent_limit: "${CASSANDRA_QUERY_CONCURRENT_LIMIT:1000}"
  186 + permit_max_wait_time: "${PERMIT_MAX_WAIT_TIME:120000}"
  187 + rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:30000}"
184 188
185 189 queue:
186 190 msg.ttl: 604800 # 7 days
... ...
... ... @@ -16,16 +16,16 @@
16 16 package org.thingsboard.server.common.msg.core;
17 17
18 18 import lombok.Data;
19   -import org.thingsboard.server.common.msg.session.FromDeviceMsg;
  19 +import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
20 20 import org.thingsboard.server.common.msg.session.MsgType;
21 21
22 22 /**
23 23 * @author Andrew Shvayka
24 24 */
25 25 @Data
26   -public class ToServerRpcRequestMsg implements FromDeviceMsg {
  26 +public class ToServerRpcRequestMsg implements FromDeviceRequestMsg {
27 27
28   - private final int requestId;
  28 + private final Integer requestId;
29 29 private final String method;
30 30 private final String params;
31 31
... ...
... ... @@ -148,7 +148,7 @@ public class CassandraAssetDao extends CassandraAbstractSearchTextDao<AssetEntit
148 148 query.and(eq(ENTITY_SUBTYPE_TENANT_ID_PROPERTY, tenantId));
149 149 query.and(eq(ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY, EntityType.ASSET));
150 150 query.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
151   - ResultSetFuture resultSetFuture = getSession().executeAsync(query);
  151 + ResultSetFuture resultSetFuture = executeAsyncRead(query);
152 152 return Futures.transform(resultSetFuture, new Function<ResultSet, List<EntitySubtype>>() {
153 153 @Nullable
154 154 @Override
... ...
... ... @@ -147,12 +147,12 @@ public class CassandraBaseAttributesDao extends CassandraAbstractAsyncDao implem
147 147 .and(eq(ATTRIBUTE_TYPE_COLUMN, attributeType))
148 148 .and(eq(ATTRIBUTE_KEY_COLUMN, key));
149 149 log.debug("Remove request: {}", delete.toString());
150   - return getFuture(getSession().executeAsync(delete), rs -> null);
  150 + return getFuture(executeAsyncWrite(delete), rs -> null);
151 151 }
152 152
153 153 private PreparedStatement getSaveStmt() {
154 154 if (saveStmt == null) {
155   - saveStmt = getSession().prepare("INSERT INTO " + ModelConstants.ATTRIBUTES_KV_CF +
  155 + saveStmt = prepare("INSERT INTO " + ModelConstants.ATTRIBUTES_KV_CF +
156 156 "(" + ENTITY_TYPE_COLUMN +
157 157 "," + ENTITY_ID_COLUMN +
158 158 "," + ATTRIBUTE_TYPE_COLUMN +
... ...
... ... @@ -244,12 +244,12 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
244 244 values.add("?");
245 245 }
246 246 String statementString = INSERT_INTO + cfName + " (" + String.join(",", columnsList) + ") VALUES (" + values.toString() + ")";
247   - return getSession().prepare(statementString);
  247 + return prepare(statementString);
248 248 }
249 249
250 250 private PreparedStatement getPartitionInsertStmt() {
251 251 if (partitionInsertStmt == null) {
252   - partitionInsertStmt = getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF +
  252 + partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF +
253 253 "(" + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY +
254 254 "," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" +
255 255 " VALUES(?, ?)");
... ... @@ -343,7 +343,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
343 343 .where(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId));
344 344 select.and(QueryBuilder.gte(ModelConstants.PARTITION_COLUMN, minPartition));
345 345 select.and(QueryBuilder.lte(ModelConstants.PARTITION_COLUMN, maxPartition));
346   - return getSession().execute(select);
  346 + return executeRead(select);
347 347 }
348 348
349 349 }
... ...
... ... @@ -130,7 +130,7 @@ public class CassandraBaseComponentDescriptorDao extends CassandraAbstractSearch
130 130 public boolean removeById(UUID key) {
131 131 Statement delete = QueryBuilder.delete().all().from(ModelConstants.COMPONENT_DESCRIPTOR_BY_ID).where(eq(ModelConstants.ID_PROPERTY, key));
132 132 log.debug("Remove request: {}", delete.toString());
133   - return getSession().execute(delete).wasApplied();
  133 + return executeWrite(delete).wasApplied();
134 134 }
135 135
136 136 @Override
... ... @@ -145,7 +145,7 @@ public class CassandraBaseComponentDescriptorDao extends CassandraAbstractSearch
145 145 log.debug("Delete plugin meta-data entity by id [{}]", clazz);
146 146 Statement delete = QueryBuilder.delete().all().from(getColumnFamilyName()).where(eq(ModelConstants.COMPONENT_DESCRIPTOR_CLASS_PROPERTY, clazz));
147 147 log.debug("Remove request: {}", delete.toString());
148   - ResultSet resultSet = getSession().execute(delete);
  148 + ResultSet resultSet = executeWrite(delete);
149 149 log.debug("Delete result: [{}]", resultSet.wasApplied());
150 150 }
151 151
... ...
... ... @@ -148,7 +148,7 @@ public class CassandraDeviceDao extends CassandraAbstractSearchTextDao<DeviceEnt
148 148 query.and(eq(ENTITY_SUBTYPE_TENANT_ID_PROPERTY, tenantId));
149 149 query.and(eq(ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY, EntityType.DEVICE));
150 150 query.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
151   - ResultSetFuture resultSetFuture = getSession().executeAsync(query);
  151 + ResultSetFuture resultSetFuture = executeAsyncRead(query);
152 152 return Futures.transform(resultSetFuture, new Function<ResultSet, List<EntitySubtype>>() {
153 153 @Nullable
154 154 @Override
... ...
... ... @@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
21 21 import org.springframework.beans.factory.annotation.Autowired;
22 22 import org.thingsboard.server.dao.cassandra.CassandraCluster;
23 23 import org.thingsboard.server.dao.model.type.*;
  24 +import org.thingsboard.server.dao.util.BufferedRateLimiter;
24 25
25 26 import java.util.concurrent.ConcurrentHashMap;
26 27 import java.util.concurrent.ConcurrentMap;
... ... @@ -33,16 +34,15 @@ public abstract class CassandraAbstractDao {
33 34
34 35 private ConcurrentMap<String, PreparedStatement> preparedStatementMap = new ConcurrentHashMap<>();
35 36
36   - protected PreparedStatement prepare(String query) {
37   - return preparedStatementMap.computeIfAbsent(query, i -> getSession().prepare(i));
38   - }
  37 + @Autowired
  38 + private BufferedRateLimiter rateLimiter;
39 39
40 40 private Session session;
41 41
42 42 private ConsistencyLevel defaultReadLevel;
43 43 private ConsistencyLevel defaultWriteLevel;
44 44
45   - protected Session getSession() {
  45 + private Session getSession() {
46 46 if (session == null) {
47 47 session = cluster.getSession();
48 48 defaultReadLevel = cluster.getDefaultReadConsistencyLevel();
... ... @@ -59,6 +59,10 @@ public abstract class CassandraAbstractDao {
59 59 return session;
60 60 }
61 61
  62 + protected PreparedStatement prepare(String query) {
  63 + return preparedStatementMap.computeIfAbsent(query, i -> getSession().prepare(i));
  64 + }
  65 +
62 66 private void registerCodecIfNotFound(CodecRegistry registry, TypeCodec<?> codec) {
63 67 try {
64 68 registry.codecFor(codec.getCqlType(), codec.getJavaType());
... ... @@ -85,10 +89,7 @@ public abstract class CassandraAbstractDao {
85 89
86 90 private ResultSet execute(Statement statement, ConsistencyLevel level) {
87 91 log.debug("Execute cassandra statement {}", statement);
88   - if (statement.getConsistencyLevel() == null) {
89   - statement.setConsistencyLevel(level);
90   - }
91   - return getSession().execute(statement);
  92 + return executeAsync(statement, level).getUninterruptibly();
92 93 }
93 94
94 95 private ResultSetFuture executeAsync(Statement statement, ConsistencyLevel level) {
... ... @@ -96,6 +97,6 @@ public abstract class CassandraAbstractDao {
96 97 if (statement.getConsistencyLevel() == null) {
97 98 statement.setConsistencyLevel(level);
98 99 }
99   - return getSession().executeAsync(statement);
  100 + return new RateLimitedResultSetFuture(getSession(), rateLimiter, statement);
100 101 }
101 102 }
\ No newline at end of file
... ...
... ... @@ -63,7 +63,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
63 63 List<E> list = Collections.emptyList();
64 64 if (statement != null) {
65 65 statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
66   - ResultSet resultSet = getSession().execute(statement);
  66 + ResultSet resultSet = executeRead(statement);
67 67 Result<E> result = getMapper().map(resultSet);
68 68 if (result != null) {
69 69 list = result.all();
... ... @@ -75,7 +75,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
75 75 protected ListenableFuture<List<D>> findListByStatementAsync(Statement statement) {
76 76 if (statement != null) {
77 77 statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
78   - ResultSetFuture resultSetFuture = getSession().executeAsync(statement);
  78 + ResultSetFuture resultSetFuture = executeAsyncRead(statement);
79 79 return Futures.transform(resultSetFuture, new Function<ResultSet, List<D>>() {
80 80 @Nullable
81 81 @Override
... ... @@ -97,7 +97,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
97 97 E object = null;
98 98 if (statement != null) {
99 99 statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
100   - ResultSet resultSet = getSession().execute(statement);
  100 + ResultSet resultSet = executeRead(statement);
101 101 Result<E> result = getMapper().map(resultSet);
102 102 if (result != null) {
103 103 object = result.one();
... ... @@ -109,7 +109,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
109 109 protected ListenableFuture<D> findOneByStatementAsync(Statement statement) {
110 110 if (statement != null) {
111 111 statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
112   - ResultSetFuture resultSetFuture = getSession().executeAsync(statement);
  112 + ResultSetFuture resultSetFuture = executeAsyncRead(statement);
113 113 return Futures.transform(resultSetFuture, new Function<ResultSet, D>() {
114 114 @Nullable
115 115 @Override
... ... @@ -184,7 +184,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
184 184 public boolean removeById(UUID key) {
185 185 Statement delete = QueryBuilder.delete().all().from(getColumnFamilyName()).where(eq(ModelConstants.ID_PROPERTY, key));
186 186 log.debug("Remove request: {}", delete.toString());
187   - return getSession().execute(delete).wasApplied();
  187 + return executeWrite(delete).wasApplied();
188 188 }
189 189
190 190 @Override
... ...
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.nosql;
  17 +
  18 +import com.datastax.driver.core.ResultSet;
  19 +import com.datastax.driver.core.ResultSetFuture;
  20 +import com.datastax.driver.core.Session;
  21 +import com.datastax.driver.core.Statement;
  22 +import com.google.common.base.Function;
  23 +import com.google.common.util.concurrent.FutureCallback;
  24 +import com.google.common.util.concurrent.Futures;
  25 +import com.google.common.util.concurrent.ListenableFuture;
  26 +import com.google.common.util.concurrent.Uninterruptibles;
  27 +import org.thingsboard.server.dao.util.AsyncRateLimiter;
  28 +
  29 +import javax.annotation.Nullable;
  30 +import java.util.concurrent.*;
  31 +
  32 +public class RateLimitedResultSetFuture implements ResultSetFuture {
  33 +
  34 + private final ListenableFuture<ResultSetFuture> originalFuture;
  35 + private final ListenableFuture<Void> rateLimitFuture;
  36 +
  37 + public RateLimitedResultSetFuture(Session session, AsyncRateLimiter rateLimiter, Statement statement) {
  38 + this.rateLimitFuture = rateLimiter.acquireAsync();
  39 + this.originalFuture = Futures.transform(rateLimitFuture,
  40 + (Function<Void, ResultSetFuture>) i -> executeAsyncWithRelease(rateLimiter, session, statement));
  41 + }
  42 +
  43 + @Override
  44 + public ResultSet getUninterruptibly() {
  45 + return safeGet().getUninterruptibly();
  46 + }
  47 +
  48 + @Override
  49 + public ResultSet getUninterruptibly(long timeout, TimeUnit unit) throws TimeoutException {
  50 + long rateLimitStart = System.nanoTime();
  51 + ResultSetFuture resultSetFuture = null;
  52 + try {
  53 + resultSetFuture = originalFuture.get(timeout, unit);
  54 + } catch (InterruptedException | ExecutionException e) {
  55 + throw new IllegalStateException(e);
  56 + }
  57 + long rateLimitDurationNano = System.nanoTime() - rateLimitStart;
  58 + long innerTimeoutNano = unit.toNanos(timeout) - rateLimitDurationNano;
  59 + if (innerTimeoutNano > 0) {
  60 + return resultSetFuture.getUninterruptibly(innerTimeoutNano, TimeUnit.NANOSECONDS);
  61 + }
  62 + throw new TimeoutException("Timeout waiting for task.");
  63 + }
  64 +
  65 + @Override
  66 + public boolean cancel(boolean mayInterruptIfRunning) {
  67 + if (originalFuture.isDone()) {
  68 + return safeGet().cancel(mayInterruptIfRunning);
  69 + } else {
  70 + return originalFuture.cancel(mayInterruptIfRunning);
  71 + }
  72 + }
  73 +
  74 + @Override
  75 + public boolean isCancelled() {
  76 + if (originalFuture.isDone()) {
  77 + return safeGet().isCancelled();
  78 + }
  79 +
  80 + return originalFuture.isCancelled();
  81 + }
  82 +
  83 + @Override
  84 + public boolean isDone() {
  85 + return originalFuture.isDone() && safeGet().isDone();
  86 + }
  87 +
  88 + @Override
  89 + public ResultSet get() throws InterruptedException, ExecutionException {
  90 + return safeGet().get();
  91 + }
  92 +
  93 + @Override
  94 + public ResultSet get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
  95 + long rateLimitStart = System.nanoTime();
  96 + ResultSetFuture resultSetFuture = originalFuture.get(timeout, unit);
  97 + long rateLimitDurationNano = System.nanoTime() - rateLimitStart;
  98 + long innerTimeoutNano = unit.toNanos(timeout) - rateLimitDurationNano;
  99 + if (innerTimeoutNano > 0) {
  100 + return resultSetFuture.get(innerTimeoutNano, TimeUnit.NANOSECONDS);
  101 + }
  102 + throw new TimeoutException("Timeout waiting for task.");
  103 + }
  104 +
  105 + @Override
  106 + public void addListener(Runnable listener, Executor executor) {
  107 + originalFuture.addListener(() -> {
  108 + try {
  109 + ResultSetFuture resultSetFuture = Uninterruptibles.getUninterruptibly(originalFuture);
  110 + resultSetFuture.addListener(listener, executor);
  111 + } catch (CancellationException e) {
  112 + cancel(false);
  113 + return;
  114 + } catch (ExecutionException e) {
  115 + Futures.immediateFailedFuture(e).addListener(listener, executor);
  116 + }
  117 + }, executor);
  118 + }
  119 +
  120 + private ResultSetFuture safeGet() {
  121 + try {
  122 + return originalFuture.get();
  123 + } catch (InterruptedException | ExecutionException e) {
  124 + throw new IllegalStateException(e);
  125 + }
  126 + }
  127 +
  128 + private ResultSetFuture executeAsyncWithRelease(AsyncRateLimiter rateLimiter, Session session, Statement statement) {
  129 + try {
  130 + ResultSetFuture resultSetFuture = session.executeAsync(statement);
  131 + Futures.addCallback(resultSetFuture, new FutureCallback<ResultSet>() {
  132 + @Override
  133 + public void onSuccess(@Nullable ResultSet result) {
  134 + rateLimiter.release();
  135 + }
  136 +
  137 + @Override
  138 + public void onFailure(Throwable t) {
  139 + rateLimiter.release();
  140 + }
  141 + });
  142 + return resultSetFuture;
  143 + } catch (RuntimeException re) {
  144 + rateLimiter.release();
  145 + throw re;
  146 + }
  147 + }
  148 +}
... ...
... ... @@ -242,7 +242,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
242 242
243 243 private PreparedStatement getSaveStmt() {
244 244 if (saveStmt == null) {
245   - saveStmt = getSession().prepare("INSERT INTO " + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
  245 + saveStmt = prepare("INSERT INTO " + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
246 246 "(" + ModelConstants.RELATION_FROM_ID_PROPERTY +
247 247 "," + ModelConstants.RELATION_FROM_TYPE_PROPERTY +
248 248 "," + ModelConstants.RELATION_TO_ID_PROPERTY +
... ... @@ -257,7 +257,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
257 257
258 258 private PreparedStatement getDeleteStmt() {
259 259 if (deleteStmt == null) {
260   - deleteStmt = getSession().prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME +
  260 + deleteStmt = prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME +
261 261 WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ?" +
262 262 AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ?" +
263 263 AND + ModelConstants.RELATION_TO_ID_PROPERTY + " = ?" +
... ... @@ -270,7 +270,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
270 270
271 271 private PreparedStatement getDeleteAllByEntityStmt() {
272 272 if (deleteAllByEntityStmt == null) {
273   - deleteAllByEntityStmt = getSession().prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME +
  273 + deleteAllByEntityStmt = prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME +
274 274 WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ?" +
275 275 AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ?");
276 276 }
... ... @@ -279,7 +279,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
279 279
280 280 private PreparedStatement getFindAllByFromStmt() {
281 281 if (findAllByFromStmt == null) {
282   - findAllByFromStmt = getSession().prepare(SELECT_COLUMNS + " " +
  282 + findAllByFromStmt = prepare(SELECT_COLUMNS + " " +
283 283 FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
284 284 WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + EQUAL_TO_PARAM +
285 285 AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + EQUAL_TO_PARAM +
... ... @@ -290,7 +290,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
290 290
291 291 private PreparedStatement getFindAllByFromAndTypeStmt() {
292 292 if (findAllByFromAndTypeStmt == null) {
293   - findAllByFromAndTypeStmt = getSession().prepare(SELECT_COLUMNS + " " +
  293 + findAllByFromAndTypeStmt = prepare(SELECT_COLUMNS + " " +
294 294 FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
295 295 WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + EQUAL_TO_PARAM +
296 296 AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + EQUAL_TO_PARAM +
... ... @@ -303,7 +303,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
303 303
304 304 private PreparedStatement getFindAllByToStmt() {
305 305 if (findAllByToStmt == null) {
306   - findAllByToStmt = getSession().prepare(SELECT_COLUMNS + " " +
  306 + findAllByToStmt = prepare(SELECT_COLUMNS + " " +
307 307 FROM + ModelConstants.RELATION_REVERSE_VIEW_NAME + " " +
308 308 WHERE + ModelConstants.RELATION_TO_ID_PROPERTY + EQUAL_TO_PARAM +
309 309 AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + EQUAL_TO_PARAM +
... ... @@ -314,7 +314,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
314 314
315 315 private PreparedStatement getFindAllByToAndTypeStmt() {
316 316 if (findAllByToAndTypeStmt == null) {
317   - findAllByToAndTypeStmt = getSession().prepare(SELECT_COLUMNS + " " +
  317 + findAllByToAndTypeStmt = prepare(SELECT_COLUMNS + " " +
318 318 FROM + ModelConstants.RELATION_REVERSE_VIEW_NAME + " " +
319 319 WHERE + ModelConstants.RELATION_TO_ID_PROPERTY + EQUAL_TO_PARAM +
320 320 AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + EQUAL_TO_PARAM +
... ... @@ -327,7 +327,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
327 327
328 328 private PreparedStatement getCheckRelationStmt() {
329 329 if (checkRelationStmt == null) {
330   - checkRelationStmt = getSession().prepare(SELECT_COLUMNS + " " +
  330 + checkRelationStmt = prepare(SELECT_COLUMNS + " " +
331 331 FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
332 332 WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + EQUAL_TO_PARAM +
333 333 AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + EQUAL_TO_PARAM +
... ...
... ... @@ -82,8 +82,9 @@ public class BaseRelationService implements RelationService {
82 82 }
83 83
84 84 @Caching(evict = {
85   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
  85 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"),
86 86 @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"),
  87 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
87 88 @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"),
88 89 @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}")
89 90 })
... ... @@ -95,8 +96,9 @@ public class BaseRelationService implements RelationService {
95 96 }
96 97
97 98 @Caching(evict = {
98   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
  99 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"),
99 100 @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"),
  101 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
100 102 @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"),
101 103 @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}")
102 104 })
... ... @@ -108,11 +110,11 @@ public class BaseRelationService implements RelationService {
108 110 }
109 111
110 112 @Caching(evict = {
111   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
  113 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"),
112 114 @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"),
  115 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
113 116 @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"),
114   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}"),
115   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}")
  117 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}")
116 118 })
117 119 @Override
118 120 public boolean deleteRelation(EntityRelation relation) {
... ... @@ -122,11 +124,11 @@ public class BaseRelationService implements RelationService {
122 124 }
123 125
124 126 @Caching(evict = {
125   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.from"),
126   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type}"),
127   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.to"),
128   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type}"),
129   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}")
  127 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"),
  128 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"),
  129 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
  130 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"),
  131 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}")
130 132 })
131 133 @Override
132 134 public ListenableFuture<Boolean> deleteRelationAsync(EntityRelation relation) {
... ... @@ -136,11 +138,11 @@ public class BaseRelationService implements RelationService {
136 138 }
137 139
138 140 @Caching(evict = {
139   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#from"),
140   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType}"),
141   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#to"),
142   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType}"),
143   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType}")
  141 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType, #typeGroup}"),
  142 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup}"),
  143 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}"),
  144 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup}"),
  145 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup}")
144 146 })
145 147 @Override
146 148 public boolean deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
... ... @@ -150,11 +152,11 @@ public class BaseRelationService implements RelationService {
150 152 }
151 153
152 154 @Caching(evict = {
153   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#from"),
154   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType}"),
155   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#to"),
156   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType}"),
157   - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType}")
  155 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType, #typeGroup}"),
  156 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup}"),
  157 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}"),
  158 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup}"),
  159 + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup}")
158 160 })
159 161 @Override
160 162 public ListenableFuture<Boolean> deleteRelationAsync(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
... ...
... ... @@ -73,7 +73,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
73 73
74 74 private PreparedStatement partitionInsertStmt;
75 75 private PreparedStatement partitionInsertTtlStmt;
76   - private PreparedStatement[] latestInsertStmts;
  76 + private PreparedStatement latestInsertStmt;
77 77 private PreparedStatement[] saveStmts;
78 78 private PreparedStatement[] saveTtlStmts;
79 79 private PreparedStatement[] fetchStmts;
... ... @@ -306,13 +306,15 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
306 306
307 307 @Override
308 308 public ListenableFuture<Void> saveLatest(EntityId entityId, TsKvEntry tsKvEntry) {
309   - DataType type = tsKvEntry.getDataType();
310   - BoundStatement stmt = getLatestStmt(type).bind()
  309 + BoundStatement stmt = getLatestStmt().bind()
311 310 .setString(0, entityId.getEntityType().name())
312 311 .setUUID(1, entityId.getId())
313 312 .setString(2, tsKvEntry.getKey())
314   - .setLong(3, tsKvEntry.getTs());
315   - addValue(tsKvEntry, stmt, 4);
  313 + .setLong(3, tsKvEntry.getTs())
  314 + .set(4, tsKvEntry.getBooleanValue().orElse(null), Boolean.class)
  315 + .set(5, tsKvEntry.getStrValue().orElse(null), String.class)
  316 + .set(6, tsKvEntry.getLongValue().orElse(null), Long.class)
  317 + .set(7, tsKvEntry.getDoubleValue().orElse(null), Double.class);
316 318 return getFuture(executeAsyncWrite(stmt), rs -> null);
317 319 }
318 320
... ... @@ -381,7 +383,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
381 383 if (saveStmts == null) {
382 384 saveStmts = new PreparedStatement[DataType.values().length];
383 385 for (DataType type : DataType.values()) {
384   - saveStmts[type.ordinal()] = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
  386 + saveStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
385 387 "(" + ModelConstants.ENTITY_TYPE_COLUMN +
386 388 "," + ModelConstants.ENTITY_ID_COLUMN +
387 389 "," + ModelConstants.KEY_COLUMN +
... ... @@ -398,7 +400,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
398 400 if (saveTtlStmts == null) {
399 401 saveTtlStmts = new PreparedStatement[DataType.values().length];
400 402 for (DataType type : DataType.values()) {
401   - saveTtlStmts[type.ordinal()] = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
  403 + saveTtlStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
402 404 "(" + ModelConstants.ENTITY_TYPE_COLUMN +
403 405 "," + ModelConstants.ENTITY_ID_COLUMN +
404 406 "," + ModelConstants.KEY_COLUMN +
... ... @@ -420,7 +422,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
420 422 } else if (type == Aggregation.AVG && fetchStmts[Aggregation.SUM.ordinal()] != null) {
421 423 fetchStmts[type.ordinal()] = fetchStmts[Aggregation.SUM.ordinal()];
422 424 } else {
423   - fetchStmts[type.ordinal()] = getSession().prepare(SELECT_PREFIX +
  425 + fetchStmts[type.ordinal()] = prepare(SELECT_PREFIX +
424 426 String.join(", ", ModelConstants.getFetchColumnNames(type)) + " FROM " + ModelConstants.TS_KV_CF
425 427 + " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM
426 428 + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
... ... @@ -435,26 +437,26 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
435 437 return fetchStmts[aggType.ordinal()];
436 438 }
437 439
438   - private PreparedStatement getLatestStmt(DataType dataType) {
439   - if (latestInsertStmts == null) {
440   - latestInsertStmts = new PreparedStatement[DataType.values().length];
441   - for (DataType type : DataType.values()) {
442   - latestInsertStmts[type.ordinal()] = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_LATEST_CF +
443   - "(" + ModelConstants.ENTITY_TYPE_COLUMN +
444   - "," + ModelConstants.ENTITY_ID_COLUMN +
445   - "," + ModelConstants.KEY_COLUMN +
446   - "," + ModelConstants.TS_COLUMN +
447   - "," + getColumnName(type) + ")" +
448   - " VALUES(?, ?, ?, ?, ?)");
449   - }
  440 + private PreparedStatement getLatestStmt() {
  441 + if (latestInsertStmt == null) {
  442 + latestInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_LATEST_CF +
  443 + "(" + ModelConstants.ENTITY_TYPE_COLUMN +
  444 + "," + ModelConstants.ENTITY_ID_COLUMN +
  445 + "," + ModelConstants.KEY_COLUMN +
  446 + "," + ModelConstants.TS_COLUMN +
  447 + "," + ModelConstants.BOOLEAN_VALUE_COLUMN +
  448 + "," + ModelConstants.STRING_VALUE_COLUMN +
  449 + "," + ModelConstants.LONG_VALUE_COLUMN +
  450 + "," + ModelConstants.DOUBLE_VALUE_COLUMN + ")" +
  451 + " VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
450 452 }
451   - return latestInsertStmts[dataType.ordinal()];
  453 + return latestInsertStmt;
452 454 }
453 455
454 456
455 457 private PreparedStatement getPartitionInsertStmt() {
456 458 if (partitionInsertStmt == null) {
457   - partitionInsertStmt = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
  459 + partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
458 460 "(" + ModelConstants.ENTITY_TYPE_COLUMN +
459 461 "," + ModelConstants.ENTITY_ID_COLUMN +
460 462 "," + ModelConstants.PARTITION_COLUMN +
... ... @@ -466,7 +468,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
466 468
467 469 private PreparedStatement getPartitionInsertTtlStmt() {
468 470 if (partitionInsertTtlStmt == null) {
469   - partitionInsertTtlStmt = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
  471 + partitionInsertTtlStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
470 472 "(" + ModelConstants.ENTITY_TYPE_COLUMN +
471 473 "," + ModelConstants.ENTITY_ID_COLUMN +
472 474 "," + ModelConstants.PARTITION_COLUMN +
... ... @@ -479,7 +481,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
479 481
480 482 private PreparedStatement getFindLatestStmt() {
481 483 if (findLatestStmt == null) {
482   - findLatestStmt = getSession().prepare(SELECT_PREFIX +
  484 + findLatestStmt = prepare(SELECT_PREFIX +
483 485 ModelConstants.KEY_COLUMN + "," +
484 486 ModelConstants.TS_COLUMN + "," +
485 487 ModelConstants.STRING_VALUE_COLUMN + "," +
... ... @@ -496,7 +498,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
496 498
497 499 private PreparedStatement getFindAllLatestStmt() {
498 500 if (findAllLatestStmt == null) {
499   - findAllLatestStmt = getSession().prepare(SELECT_PREFIX +
  501 + findAllLatestStmt = prepare(SELECT_PREFIX +
500 502 ModelConstants.KEY_COLUMN + "," +
501 503 ModelConstants.TS_COLUMN + "," +
502 504 ModelConstants.STRING_VALUE_COLUMN + "," +
... ...
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.util;
  17 +
  18 +import com.google.common.util.concurrent.ListenableFuture;
  19 +
  20 +public interface AsyncRateLimiter {
  21 +
  22 + ListenableFuture<Void> acquireAsync();
  23 +
  24 + void release();
  25 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.util;
  17 +
  18 +import com.google.common.util.concurrent.Futures;
  19 +import com.google.common.util.concurrent.ListenableFuture;
  20 +import com.google.common.util.concurrent.ListeningExecutorService;
  21 +import com.google.common.util.concurrent.MoreExecutors;
  22 +import lombok.extern.slf4j.Slf4j;
  23 +import org.springframework.beans.factory.annotation.Value;
  24 +import org.springframework.scheduling.annotation.Scheduled;
  25 +import org.springframework.stereotype.Component;
  26 +
  27 +import java.util.concurrent.*;
  28 +import java.util.concurrent.atomic.AtomicInteger;
  29 +
  30 +@Component
  31 +@Slf4j
  32 +@NoSqlDao
  33 +public class BufferedRateLimiter implements AsyncRateLimiter {
  34 +
  35 + private final ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
  36 +
  37 + private final int permitsLimit;
  38 + private final int maxPermitWaitTime;
  39 + private final AtomicInteger permits;
  40 + private final BlockingQueue<LockedFuture> queue;
  41 +
  42 + private final AtomicInteger maxQueueSize = new AtomicInteger();
  43 + private final AtomicInteger maxGrantedPermissions = new AtomicInteger();
  44 +
  45 + public BufferedRateLimiter(@Value("${cassandra.query.buffer_size}") int queueLimit,
  46 + @Value("${cassandra.query.concurrent_limit}") int permitsLimit,
  47 + @Value("${cassandra.query.permit_max_wait_time}") int maxPermitWaitTime) {
  48 + this.permitsLimit = permitsLimit;
  49 + this.maxPermitWaitTime = maxPermitWaitTime;
  50 + this.permits = new AtomicInteger();
  51 + this.queue = new LinkedBlockingQueue<>(queueLimit);
  52 + }
  53 +
  54 + @Override
  55 + public ListenableFuture<Void> acquireAsync() {
  56 + if (queue.isEmpty()) {
  57 + if (permits.incrementAndGet() <= permitsLimit) {
  58 + if (permits.get() > maxGrantedPermissions.get()) {
  59 + maxGrantedPermissions.set(permits.get());
  60 + }
  61 + return Futures.immediateFuture(null);
  62 + }
  63 + permits.decrementAndGet();
  64 + }
  65 +
  66 + return putInQueue();
  67 + }
  68 +
  69 + @Override
  70 + public void release() {
  71 + permits.decrementAndGet();
  72 + reprocessQueue();
  73 + }
  74 +
  75 + private void reprocessQueue() {
  76 + while (permits.get() < permitsLimit) {
  77 + if (permits.incrementAndGet() <= permitsLimit) {
  78 + if (permits.get() > maxGrantedPermissions.get()) {
  79 + maxGrantedPermissions.set(permits.get());
  80 + }
  81 + LockedFuture lockedFuture = queue.poll();
  82 + if (lockedFuture != null) {
  83 + lockedFuture.latch.countDown();
  84 + } else {
  85 + permits.decrementAndGet();
  86 + break;
  87 + }
  88 + } else {
  89 + permits.decrementAndGet();
  90 + }
  91 + }
  92 + }
  93 +
  94 + private LockedFuture createLockedFuture() {
  95 + CountDownLatch latch = new CountDownLatch(1);
  96 + ListenableFuture<Void> future = pool.submit(() -> {
  97 + latch.await();
  98 + return null;
  99 + });
  100 + return new LockedFuture(latch, future, System.currentTimeMillis());
  101 + }
  102 +
  103 + private ListenableFuture<Void> putInQueue() {
  104 +
  105 + int size = queue.size();
  106 + if (size > maxQueueSize.get()) {
  107 + maxQueueSize.set(size);
  108 + }
  109 +
  110 + if (queue.remainingCapacity() > 0) {
  111 + try {
  112 + LockedFuture lockedFuture = createLockedFuture();
  113 + if (!queue.offer(lockedFuture, 1, TimeUnit.SECONDS)) {
  114 + lockedFuture.cancelFuture();
  115 + return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Buffer is full. Reject"));
  116 + }
  117 + if(permits.get() < permitsLimit) {
  118 + reprocessQueue();
  119 + }
  120 + return lockedFuture.future;
  121 + } catch (InterruptedException e) {
  122 + return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Task interrupted. Reject"));
  123 + }
  124 + }
  125 + return Futures.immediateFailedFuture(new IllegalStateException("Rate Limit Buffer is full. Reject"));
  126 + }
  127 +
  128 + @Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}")
  129 + public void printStats() {
  130 + int expiredCount = 0;
  131 + for (LockedFuture lockedFuture : queue) {
  132 + if (lockedFuture.isExpired()) {
  133 + lockedFuture.cancelFuture();
  134 + expiredCount++;
  135 + }
  136 + }
  137 + log.info("Permits maxBuffer is [{}] max concurrent [{}] expired [{}] current granted [{}]", maxQueueSize.getAndSet(0),
  138 + maxGrantedPermissions.getAndSet(0), expiredCount, permits.get());
  139 + }
  140 +
  141 + private class LockedFuture {
  142 + final CountDownLatch latch;
  143 + final ListenableFuture<Void> future;
  144 + final long createTime;
  145 +
  146 + public LockedFuture(CountDownLatch latch, ListenableFuture<Void> future, long createTime) {
  147 + this.latch = latch;
  148 + this.future = future;
  149 + this.createTime = createTime;
  150 + }
  151 +
  152 + void cancelFuture() {
  153 + future.cancel(false);
  154 + latch.countDown();
  155 + }
  156 +
  157 + boolean isExpired() {
  158 + return (System.currentTimeMillis() - createTime) > maxPermitWaitTime;
  159 + }
  160 +
  161 + }
  162 +
  163 +
  164 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.nosql;
  17 +
  18 +import com.datastax.driver.core.*;
  19 +import com.datastax.driver.core.exceptions.UnsupportedFeatureException;
  20 +import com.google.common.util.concurrent.Futures;
  21 +import com.google.common.util.concurrent.ListenableFuture;
  22 +import org.junit.Test;
  23 +import org.junit.runner.RunWith;
  24 +import org.mockito.Mock;
  25 +import org.mockito.Mockito;
  26 +import org.mockito.runners.MockitoJUnitRunner;
  27 +import org.mockito.stubbing.Answer;
  28 +import org.thingsboard.server.dao.util.AsyncRateLimiter;
  29 +
  30 +import java.util.concurrent.ExecutionException;
  31 +import java.util.concurrent.TimeoutException;
  32 +
  33 +import static org.junit.Assert.*;
  34 +import static org.mockito.Mockito.*;
  35 +
  36 +@RunWith(MockitoJUnitRunner.class)
  37 +public class RateLimitedResultSetFutureTest {
  38 +
  39 + private RateLimitedResultSetFuture resultSetFuture;
  40 +
  41 + @Mock
  42 + private AsyncRateLimiter rateLimiter;
  43 + @Mock
  44 + private Session session;
  45 + @Mock
  46 + private Statement statement;
  47 + @Mock
  48 + private ResultSetFuture realFuture;
  49 + @Mock
  50 + private ResultSet rows;
  51 + @Mock
  52 + private Row row;
  53 +
  54 + @Test
  55 + public void doNotReleasePermissionIfRateLimitFutureFailed() throws InterruptedException {
  56 + when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFailedFuture(new IllegalArgumentException()));
  57 + resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
  58 + Thread.sleep(1000L);
  59 + verify(rateLimiter).acquireAsync();
  60 + try {
  61 + assertTrue(resultSetFuture.isDone());
  62 + fail();
  63 + } catch (Exception e) {
  64 + assertTrue(e instanceof IllegalStateException);
  65 + Throwable actualCause = e.getCause();
  66 + assertTrue(actualCause instanceof ExecutionException);
  67 + }
  68 + verifyNoMoreInteractions(session, rateLimiter, statement);
  69 +
  70 + }
  71 +
  72 + @Test
  73 + public void getUninterruptiblyDelegateToCassandra() throws InterruptedException, ExecutionException {
  74 + when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null));
  75 + when(session.executeAsync(statement)).thenReturn(realFuture);
  76 + Mockito.doAnswer((Answer<Void>) invocation -> {
  77 + Object[] args = invocation.getArguments();
  78 + Runnable task = (Runnable) args[0];
  79 + task.run();
  80 + return null;
  81 + }).when(realFuture).addListener(Mockito.any(), Mockito.any());
  82 +
  83 + when(realFuture.getUninterruptibly()).thenReturn(rows);
  84 +
  85 + resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
  86 + ResultSet actual = resultSetFuture.getUninterruptibly();
  87 + assertSame(rows, actual);
  88 + verify(rateLimiter, times(1)).acquireAsync();
  89 + verify(rateLimiter, times(1)).release();
  90 + }
  91 +
  92 + @Test
  93 + public void addListenerAllowsFutureTransformation() throws InterruptedException, ExecutionException {
  94 + when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null));
  95 + when(session.executeAsync(statement)).thenReturn(realFuture);
  96 + Mockito.doAnswer((Answer<Void>) invocation -> {
  97 + Object[] args = invocation.getArguments();
  98 + Runnable task = (Runnable) args[0];
  99 + task.run();
  100 + return null;
  101 + }).when(realFuture).addListener(Mockito.any(), Mockito.any());
  102 +
  103 + when(realFuture.get()).thenReturn(rows);
  104 + when(rows.one()).thenReturn(row);
  105 +
  106 + resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
  107 +
  108 + ListenableFuture<Row> transform = Futures.transform(resultSetFuture, ResultSet::one);
  109 + Row actualRow = transform.get();
  110 +
  111 + assertSame(row, actualRow);
  112 + verify(rateLimiter, times(1)).acquireAsync();
  113 + verify(rateLimiter, times(1)).release();
  114 + }
  115 +
  116 + @Test
  117 + public void immidiateCassandraExceptionReturnsPermit() throws InterruptedException, ExecutionException {
  118 + when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null));
  119 + when(session.executeAsync(statement)).thenThrow(new UnsupportedFeatureException(ProtocolVersion.V3, "hjg"));
  120 + resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
  121 + ListenableFuture<Row> transform = Futures.transform(resultSetFuture, ResultSet::one);
  122 + try {
  123 + transform.get();
  124 + fail();
  125 + } catch (Exception e) {
  126 + assertTrue(e instanceof ExecutionException);
  127 + }
  128 + verify(rateLimiter, times(1)).acquireAsync();
  129 + verify(rateLimiter, times(1)).release();
  130 + }
  131 +
  132 + @Test
  133 + public void queryTimeoutReturnsPermit() throws InterruptedException, ExecutionException {
  134 + when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null));
  135 + when(session.executeAsync(statement)).thenReturn(realFuture);
  136 + Mockito.doAnswer((Answer<Void>) invocation -> {
  137 + Object[] args = invocation.getArguments();
  138 + Runnable task = (Runnable) args[0];
  139 + task.run();
  140 + return null;
  141 + }).when(realFuture).addListener(Mockito.any(), Mockito.any());
  142 +
  143 + when(realFuture.get()).thenThrow(new ExecutionException("Fail", new TimeoutException("timeout")));
  144 + resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
  145 + ListenableFuture<Row> transform = Futures.transform(resultSetFuture, ResultSet::one);
  146 + try {
  147 + transform.get();
  148 + fail();
  149 + } catch (Exception e) {
  150 + assertTrue(e instanceof ExecutionException);
  151 + }
  152 + verify(rateLimiter, times(1)).acquireAsync();
  153 + verify(rateLimiter, times(1)).release();
  154 + }
  155 +
  156 +}
\ No newline at end of file
... ...
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.util;
  17 +
  18 +import com.google.common.util.concurrent.*;
  19 +import org.junit.Test;
  20 +
  21 +import javax.annotation.Nullable;
  22 +import java.util.concurrent.ExecutionException;
  23 +import java.util.concurrent.Executors;
  24 +import java.util.concurrent.TimeUnit;
  25 +import java.util.concurrent.atomic.AtomicInteger;
  26 +
  27 +import static org.junit.Assert.*;
  28 +
  29 +
  30 +public class BufferedRateLimiterTest {
  31 +
  32 + @Test
  33 + public void finishedFutureReturnedIfPermitsAreGranted() {
  34 + BufferedRateLimiter limiter = new BufferedRateLimiter(10, 10, 100);
  35 + ListenableFuture<Void> actual = limiter.acquireAsync();
  36 + assertTrue(actual.isDone());
  37 + }
  38 +
  39 + @Test
  40 + public void notFinishedFutureReturnedIfPermitsAreNotGranted() {
  41 + BufferedRateLimiter limiter = new BufferedRateLimiter(10, 1, 100);
  42 + ListenableFuture<Void> actual1 = limiter.acquireAsync();
  43 + ListenableFuture<Void> actual2 = limiter.acquireAsync();
  44 + assertTrue(actual1.isDone());
  45 + assertFalse(actual2.isDone());
  46 + }
  47 +
  48 + @Test
  49 + public void failedFutureReturnedIfQueueIsfull() {
  50 + BufferedRateLimiter limiter = new BufferedRateLimiter(1, 1, 100);
  51 + ListenableFuture<Void> actual1 = limiter.acquireAsync();
  52 + ListenableFuture<Void> actual2 = limiter.acquireAsync();
  53 + ListenableFuture<Void> actual3 = limiter.acquireAsync();
  54 +
  55 + assertTrue(actual1.isDone());
  56 + assertFalse(actual2.isDone());
  57 + assertTrue(actual3.isDone());
  58 + try {
  59 + actual3.get();
  60 + fail();
  61 + } catch (Exception e) {
  62 + assertTrue(e instanceof ExecutionException);
  63 + Throwable actualCause = e.getCause();
  64 + assertTrue(actualCause instanceof IllegalStateException);
  65 + assertEquals("Rate Limit Buffer is full. Reject", actualCause.getMessage());
  66 + }
  67 + }
  68 +
  69 + @Test
  70 + public void releasedPermitTriggerTasksFromQueue() throws InterruptedException {
  71 + BufferedRateLimiter limiter = new BufferedRateLimiter(10, 2, 100);
  72 + ListenableFuture<Void> actual1 = limiter.acquireAsync();
  73 + ListenableFuture<Void> actual2 = limiter.acquireAsync();
  74 + ListenableFuture<Void> actual3 = limiter.acquireAsync();
  75 + ListenableFuture<Void> actual4 = limiter.acquireAsync();
  76 + assertTrue(actual1.isDone());
  77 + assertTrue(actual2.isDone());
  78 + assertFalse(actual3.isDone());
  79 + assertFalse(actual4.isDone());
  80 + limiter.release();
  81 + TimeUnit.MILLISECONDS.sleep(100L);
  82 + assertTrue(actual3.isDone());
  83 + assertFalse(actual4.isDone());
  84 + limiter.release();
  85 + TimeUnit.MILLISECONDS.sleep(100L);
  86 + assertTrue(actual4.isDone());
  87 + }
  88 +
  89 + @Test
  90 + public void permitsReleasedInConcurrentMode() throws InterruptedException {
  91 + BufferedRateLimiter limiter = new BufferedRateLimiter(10, 2, 100);
  92 + AtomicInteger actualReleased = new AtomicInteger();
  93 + AtomicInteger actualRejected = new AtomicInteger();
  94 + ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(5));
  95 + for (int i = 0; i < 100; i++) {
  96 + ListenableFuture<ListenableFuture<Void>> submit = pool.submit(limiter::acquireAsync);
  97 + Futures.addCallback(submit, new FutureCallback<ListenableFuture<Void>>() {
  98 + @Override
  99 + public void onSuccess(@Nullable ListenableFuture<Void> result) {
  100 + Futures.addCallback(result, new FutureCallback<Void>() {
  101 + @Override
  102 + public void onSuccess(@Nullable Void result) {
  103 + try {
  104 + TimeUnit.MILLISECONDS.sleep(100);
  105 + } catch (InterruptedException e) {
  106 + e.printStackTrace();
  107 + }
  108 + limiter.release();
  109 + actualReleased.incrementAndGet();
  110 + }
  111 +
  112 + @Override
  113 + public void onFailure(Throwable t) {
  114 + actualRejected.incrementAndGet();
  115 + }
  116 + });
  117 + }
  118 +
  119 + @Override
  120 + public void onFailure(Throwable t) {
  121 + }
  122 + });
  123 + }
  124 +
  125 + TimeUnit.SECONDS.sleep(2);
  126 + assertTrue("Unexpected released count " + actualReleased.get(),
  127 + actualReleased.get() > 10 && actualReleased.get() < 20);
  128 + assertTrue("Unexpected rejected count " + actualRejected.get(),
  129 + actualRejected.get() > 80 && actualRejected.get() < 90);
  130 +
  131 + }
  132 +
  133 +
  134 +}
\ No newline at end of file
... ...
... ... @@ -47,3 +47,8 @@ cassandra.query.default_fetch_size=2000
47 47 cassandra.query.ts_key_value_partitioning=HOURS
48 48
49 49 cassandra.query.max_limit_per_request=1000
  50 +cassandra.query.buffer_size=100000
  51 +cassandra.query.concurrent_limit=1000
  52 +cassandra.query.permit_max_wait_time=20000
  53 +cassandra.query.rate_limit_print_interval_ms=30000
  54 +
... ...
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.api;
  17 +
  18 +public interface NodeConfiguration {
  19 +
  20 + NodeConfiguration defaultConfiguration();
  21 +
  22 +}
... ...
... ... @@ -35,15 +35,16 @@ public @interface RuleNode {
35 35
36 36 String nodeDetails();
37 37
  38 + Class<? extends NodeConfiguration> configClazz();
  39 +
38 40 boolean inEnabled() default true;
39 41
40 42 boolean outEnabled() default true;
41 43
42 44 ComponentScope scope() default ComponentScope.TENANT;
43 45
44   - String defaultConfigResource() default "EmptyNodeConfig.json";
45   -
46 46 String[] relationTypes() default {"Success", "Failure"};
47 47
48 48 boolean customRelations() default false;
  49 +
49 50 }
... ...
... ... @@ -30,6 +30,7 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
30 30 @RuleNode(
31 31 type = ComponentType.FILTER,
32 32 name = "script", relationTypes = {"True", "False", "Failure"},
  33 + configClazz = TbJsFilterNodeConfiguration.class,
33 34 nodeDescription = "Filter incoming messages using JS script",
34 35 nodeDetails = "Evaluate incoming Message with configured JS condition. " +
35 36 "If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." +
... ...
... ... @@ -16,9 +16,17 @@
16 16 package org.thingsboard.rule.engine.filter;
17 17
18 18 import lombok.Data;
  19 +import org.thingsboard.rule.engine.api.NodeConfiguration;
19 20
20 21 @Data
21   -public class TbJsFilterNodeConfiguration {
  22 +public class TbJsFilterNodeConfiguration implements NodeConfiguration {
22 23
23 24 private String jsScript;
  25 +
  26 + @Override
  27 + public TbJsFilterNodeConfiguration defaultConfiguration() {
  28 + TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration();
  29 + configuration.setJsScript("msg.passed < 15 && msg.name === 'Vit' && meta.temp == 10 && msg.bigObj.prop == 42;");
  30 + return configuration;
  31 + }
24 32 }
... ...
... ... @@ -31,6 +31,7 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
31 31 @RuleNode(
32 32 type = ComponentType.FILTER,
33 33 name = "switch", customRelations = true,
  34 + configClazz = TbJsSwitchNodeConfiguration.class,
34 35 nodeDescription = "Route incoming Message to one or multiple output chains",
35 36 nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " +
36 37 "If Array is empty - message not routed to next Node. " +
... ...
... ... @@ -15,14 +15,29 @@
15 15 */
16 16 package org.thingsboard.rule.engine.filter;
17 17
  18 +import com.google.common.collect.Sets;
18 19 import lombok.Data;
  20 +import org.thingsboard.rule.engine.api.NodeConfiguration;
19 21
20 22 import java.util.Set;
21 23
22 24 @Data
23   -public class TbJsSwitchNodeConfiguration {
  25 +public class TbJsSwitchNodeConfiguration implements NodeConfiguration {
24 26
25 27 private String jsScript;
26 28 private Set<String> allowedRelations;
27 29 private boolean routeToAllWithNoCheck;
  30 +
  31 + @Override
  32 + public TbJsSwitchNodeConfiguration defaultConfiguration() {
  33 + TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration();
  34 + configuration.setJsScript("function nextRelation(meta, msg) {\n" +
  35 + " return ['one','nine'];" +
  36 + "};\n" +
  37 + "\n" +
  38 + "nextRelation(meta, msg);");
  39 + configuration.setAllowedRelations(Sets.newHashSet("one", "two"));
  40 + configuration.setRouteToAllWithNoCheck(false);
  41 + return configuration;
  42 + }
28 43 }
... ...
... ... @@ -28,6 +28,7 @@ import org.thingsboard.server.common.msg.TbMsg;
28 28 @RuleNode(
29 29 type = ComponentType.FILTER,
30 30 name = "message type",
  31 + configClazz = TbMsgTypeFilterNodeConfiguration.class,
31 32 nodeDescription = "Filter incoming messages by Message Type",
32 33 nodeDetails = "Evaluate incoming Message with configured JS condition. " +
33 34 "If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.")
... ...
... ... @@ -16,15 +16,24 @@
16 16 package org.thingsboard.rule.engine.filter;
17 17
18 18 import lombok.Data;
  19 +import org.thingsboard.rule.engine.api.NodeConfiguration;
19 20
  21 +import java.util.Arrays;
  22 +import java.util.Collections;
20 23 import java.util.List;
21 24
22 25 /**
23 26 * Created by ashvayka on 19.01.18.
24 27 */
25 28 @Data
26   -public class TbMsgTypeFilterNodeConfiguration {
  29 +public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration {
27 30
28 31 private List<String> messageTypes;
29 32
  33 + @Override
  34 + public TbMsgTypeFilterNodeConfiguration defaultConfiguration() {
  35 + TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration();
  36 + configuration.setMessageTypes(Arrays.asList("GET_ATTRIBUTES","POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST"));
  37 + return configuration;
  38 + }
30 39 }
... ...
... ... @@ -38,6 +38,7 @@ import static org.thingsboard.server.common.data.DataConstants.*;
38 38 @Slf4j
39 39 @RuleNode(type = ComponentType.ENRICHMENT,
40 40 name = "originator attributes",
  41 + configClazz = TbGetAttributesNodeConfiguration.class,
41 42 nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata",
42 43 nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
43 44 "with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " +
... ...
... ... @@ -16,14 +16,16 @@
16 16 package org.thingsboard.rule.engine.metadata;
17 17
18 18 import lombok.Data;
  19 +import org.thingsboard.rule.engine.api.NodeConfiguration;
19 20
  21 +import java.util.Collections;
20 22 import java.util.List;
21 23
22 24 /**
23 25 * Created by ashvayka on 19.01.18.
24 26 */
25 27 @Data
26   -public class TbGetAttributesNodeConfiguration {
  28 +public class TbGetAttributesNodeConfiguration implements NodeConfiguration {
27 29
28 30 private List<String> clientAttributeNames;
29 31 private List<String> sharedAttributeNames;
... ... @@ -31,4 +33,13 @@ public class TbGetAttributesNodeConfiguration {
31 33
32 34 private List<String> latestTsKeyNames;
33 35
  36 + @Override
  37 + public TbGetAttributesNodeConfiguration defaultConfiguration() {
  38 + TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
  39 + configuration.setClientAttributeNames(Collections.emptyList());
  40 + configuration.setSharedAttributeNames(Collections.emptyList());
  41 + configuration.setServerAttributeNames(Collections.emptyList());
  42 + configuration.setLatestTsKeyNames(Collections.emptyList());
  43 + return configuration;
  44 + }
34 45 }
... ...
... ... @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
26 26 @RuleNode(
27 27 type = ComponentType.ENRICHMENT,
28 28 name="customer attributes",
  29 + configClazz = TbGetEntityAttrNodeConfiguration.class,
29 30 nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata",
30 31 nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
31 32 "To access those attributes in other nodes this template can be used " +
... ...
... ... @@ -16,13 +16,25 @@
16 16 package org.thingsboard.rule.engine.metadata;
17 17
18 18 import lombok.Data;
  19 +import org.thingsboard.rule.engine.api.NodeConfiguration;
19 20
  21 +import java.util.HashMap;
20 22 import java.util.Map;
21 23 import java.util.Optional;
22 24
23 25 @Data
24   -public class TbGetEntityAttrNodeConfiguration {
  26 +public class TbGetEntityAttrNodeConfiguration implements NodeConfiguration {
25 27
26 28 private Map<String, String> attrMapping;
27 29 private boolean isTelemetry = false;
  30 +
  31 + @Override
  32 + public TbGetEntityAttrNodeConfiguration defaultConfiguration() {
  33 + TbGetEntityAttrNodeConfiguration configuration = new TbGetEntityAttrNodeConfiguration();
  34 + Map<String, String> attrMapping = new HashMap<>();
  35 + attrMapping.putIfAbsent("temperature", "tempo");
  36 + configuration.setAttrMapping(attrMapping);
  37 + configuration.setTelemetry(true);
  38 + return configuration;
  39 + }
28 40 }
... ...
... ... @@ -16,11 +16,28 @@
16 16 package org.thingsboard.rule.engine.metadata;
17 17
18 18 import lombok.Data;
  19 +import org.thingsboard.rule.engine.api.NodeConfiguration;
  20 +import org.thingsboard.server.common.data.relation.EntityRelation;
19 21 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
20 22
  23 +import java.util.HashMap;
  24 +import java.util.Map;
  25 +
21 26 @Data
22   -public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration {
  27 +public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration {
23 28
24 29 private String relationType;
25 30 private EntitySearchDirection direction;
  31 +
  32 + @Override
  33 + public TbGetRelatedAttrNodeConfiguration defaultConfiguration() {
  34 + TbGetRelatedAttrNodeConfiguration configuration = new TbGetRelatedAttrNodeConfiguration();
  35 + Map<String, String> attrMapping = new HashMap<>();
  36 + attrMapping.putIfAbsent("temperature", "tempo");
  37 + configuration.setAttrMapping(attrMapping);
  38 + configuration.setTelemetry(true);
  39 + configuration.setRelationType(EntityRelation.CONTAINS_TYPE);
  40 + configuration.setDirection(EntitySearchDirection.FROM);
  41 + return configuration;
  42 + }
26 43 }
... ...
... ... @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
26 26 @RuleNode(
27 27 type = ComponentType.ENRICHMENT,
28 28 name="related attributes",
  29 + configClazz = TbGetRelatedAttrNodeConfiguration.class,
29 30 nodeDescription = "Add Originators Related Entity Attributes or Latest Telemetry into Message Metadata",
30 31 nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
31 32 "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " +
... ...
... ... @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
28 28 @RuleNode(
29 29 type = ComponentType.ENRICHMENT,
30 30 name="tenant attributes",
  31 + configClazz = TbGetEntityAttrNodeConfiguration.class,
31 32 nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata",
32 33 nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
33 34 "To access those attributes in other nodes this template can be used " +
... ...
... ... @@ -36,6 +36,7 @@ import java.util.HashSet;
36 36 @RuleNode(
37 37 type = ComponentType.TRANSFORMATION,
38 38 name="change originator",
  39 + configClazz = TbChangeOriginatorNodeConfiguration.class,
39 40 nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity",
40 41 nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
41 42 "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ")
... ...
... ... @@ -16,12 +16,24 @@
16 16 package org.thingsboard.rule.engine.transform;
17 17
18 18 import lombok.Data;
  19 +import org.thingsboard.rule.engine.api.NodeConfiguration;
  20 +import org.thingsboard.server.common.data.relation.EntityRelation;
19 21 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
20 22
21 23 @Data
22   -public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration{
  24 +public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
23 25
24 26 private String originatorSource;
25 27 private EntitySearchDirection direction;
26 28 private String relationType;
  29 +
  30 + @Override
  31 + public TbChangeOriginatorNodeConfiguration defaultConfiguration() {
  32 + TbChangeOriginatorNodeConfiguration configuration = new TbChangeOriginatorNodeConfiguration();
  33 + configuration.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE);
  34 + configuration.setDirection(EntitySearchDirection.FROM);
  35 + configuration.setRelationType(EntityRelation.CONTAINS_TYPE);
  36 + configuration.setStartNewChain(false);
  37 + return configuration;
  38 + }
27 39 }
... ...
... ... @@ -27,6 +27,7 @@ import javax.script.Bindings;
27 27 @RuleNode(
28 28 type = ComponentType.TRANSFORMATION,
29 29 name = "script",
  30 + configClazz = TbTransformMsgNodeConfiguration.class,
30 31 nodeDescription = "Change Message payload and Metadata using JavaScript",
31 32 nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " +
32 33 "<code>meta</code> - is a Message metadata.<br/>" +
... ...
... ... @@ -16,9 +16,18 @@
16 16 package org.thingsboard.rule.engine.transform;
17 17
18 18 import lombok.Data;
  19 +import org.thingsboard.rule.engine.api.NodeConfiguration;
19 20
20 21 @Data
21   -public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguration {
  22 +public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
22 23
23 24 private String jsScript;
  25 +
  26 + @Override
  27 + public TbTransformMsgNodeConfiguration defaultConfiguration() {
  28 + TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration();
  29 + configuration.setStartNewChain(false);
  30 + configuration.setJsScript("msg.passed = msg.passed * meta.temp; msg.bigObj.newProp = 'Ukraine' ");
  31 + return configuration;
  32 + }
24 33 }
... ...
... ... @@ -153,16 +153,21 @@ function RuleChainService($http, $q, $filter, types, componentDescriptorService)
153 153 return deferred.promise;
154 154 }
155 155
156   - function getRuleNodeSupportedLinks(nodeType) { //eslint-disable-line
157   - //TODO:
158   - var deferred = $q.defer();
159   - var linkLabels = [
160   - { name: 'Success', custom: false },
161   - { name: 'Fail', custom: false },
162   - { name: 'Custom', custom: true },
163   - ];
164   - deferred.resolve(linkLabels);
165   - return deferred.promise;
  156 + function getRuleNodeSupportedLinks(component) {
  157 + var relationTypes = component.configurationDescriptor.nodeDefinition.relationTypes;
  158 + var customRelations = component.configurationDescriptor.nodeDefinition.customRelations;
  159 + var linkLabels = [];
  160 + for (var i=0;i<relationTypes.length;i++) {
  161 + linkLabels.push({
  162 + name: relationTypes[i], custom: false
  163 + });
  164 + }
  165 + if (customRelations) {
  166 + linkLabels.push(
  167 + { name: 'Custom', custom: true }
  168 + );
  169 + }
  170 + return linkLabels;
166 171 }
167 172
168 173 function getRuleNodeComponents() {
... ...
  1 +/*
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +export default function fixAceEditor(aceEditor) {
  18 + aceEditor.$blockScrolling = Infinity;
  19 + aceEditor.on("showGutterTooltip", function (tooltip) {
  20 + if (!tooltip.isAttachedToBody) {
  21 + document.body.appendChild(tooltip.$element); //eslint-disable-line
  22 + tooltip.isAttachedToBody = true;
  23 + onElementRemoved(tooltip.$parentNode, () => {
  24 + if (tooltip.$element.parentNode != null) {
  25 + tooltip.$element.parentNode.removeChild(tooltip.$element);
  26 + }
  27 + });
  28 + }
  29 + });
  30 +}
  31 +
  32 +function onElementRemoved(element, callback) {
  33 + if (!document.body.contains(element)) { //eslint-disable-line
  34 + callback();
  35 + } else {
  36 + var observer;
  37 + observer = new MutationObserver(function(mutations) { //eslint-disable-line
  38 + if (!document.body.contains(element)) { //eslint-disable-line
  39 + callback();
  40 + observer.disconnect();
  41 + }
  42 + });
  43 + observer.observe(document.body, {childList: true}); //eslint-disable-line
  44 + }
  45 +}
... ...
... ... @@ -22,6 +22,8 @@ import thingsboardToast from '../services/toast';
22 22 import thingsboardUtils from '../common/utils.service';
23 23 import thingsboardExpandFullscreen from './expand-fullscreen.directive';
24 24
  25 +import fixAceEditor from './ace-editor-fix';
  26 +
25 27 /* eslint-disable import/no-unresolved, import/default */
26 28
27 29 import jsFuncTemplate from './js-func.tpl.html';
... ... @@ -83,6 +85,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
83 85 scope.js_editor.session.on("change", function () {
84 86 scope.cleanupJsErrors();
85 87 });
  88 + fixAceEditor(_ace);
86 89 }
87 90 };
88 91
... ...
  1 +/*
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +import './json-object-edit.scss';
  17 +
  18 +import 'brace/ext/language_tools';
  19 +import 'brace/mode/json';
  20 +import 'ace-builds/src-min-noconflict/snippets/json';
  21 +
  22 +import fixAceEditor from './ace-editor-fix';
  23 +
  24 +/* eslint-disable import/no-unresolved, import/default */
  25 +
  26 +import jsonObjectEditTemplate from './json-object-edit.tpl.html';
  27 +
  28 +/* eslint-enable import/no-unresolved, import/default */
  29 +
  30 +export default angular.module('thingsboard.directives.jsonObjectEdit', [])
  31 + .directive('tbJsonObjectEdit', JsonObjectEdit)
  32 + .name;
  33 +
  34 +/*@ngInject*/
  35 +function JsonObjectEdit($compile, $templateCache, $document, toast, utils) {
  36 +
  37 + var linker = function (scope, element, attrs, ngModelCtrl) {
  38 + var template = $templateCache.get(jsonObjectEditTemplate);
  39 + element.html(template);
  40 +
  41 + scope.label = attrs.label;
  42 +
  43 + scope.objectValid = true;
  44 + scope.validationError = '';
  45 +
  46 + scope.json_editor;
  47 +
  48 + scope.onFullscreenChanged = function () {
  49 + updateEditorSize();
  50 + };
  51 +
  52 + function updateEditorSize() {
  53 + if (scope.json_editor) {
  54 + scope.json_editor.resize();
  55 + scope.json_editor.renderer.updateFull();
  56 + }
  57 + }
  58 +
  59 + scope.jsonEditorOptions = {
  60 + useWrapMode: true,
  61 + mode: 'json',
  62 + advanced: {
  63 + enableSnippets: true,
  64 + enableBasicAutocompletion: true,
  65 + enableLiveAutocompletion: true
  66 + },
  67 + onLoad: function (_ace) {
  68 + scope.json_editor = _ace;
  69 + scope.json_editor.session.on("change", function () {
  70 + scope.cleanupJsonErrors();
  71 + });
  72 + fixAceEditor(_ace);
  73 + }
  74 + };
  75 +
  76 + scope.cleanupJsonErrors = function () {
  77 + toast.hide();
  78 + };
  79 +
  80 + scope.updateValidity = function () {
  81 + ngModelCtrl.$setValidity('objectValid', scope.objectValid);
  82 + };
  83 +
  84 + scope.$watch('contentBody', function (newVal, prevVal) {
  85 + if (!angular.equals(newVal, prevVal)) {
  86 + var object = scope.validate();
  87 + ngModelCtrl.$setViewValue(object);
  88 + scope.updateValidity();
  89 + }
  90 + });
  91 +
  92 + ngModelCtrl.$render = function () {
  93 + var object = ngModelCtrl.$viewValue;
  94 + var content = '';
  95 + try {
  96 + if (object) {
  97 + content = angular.toJson(object, true);
  98 + }
  99 + } catch (e) {
  100 + //
  101 + }
  102 + scope.contentBody = content;
  103 + };
  104 +
  105 + scope.showError = function (error) {
  106 + var toastParent = angular.element('#tb-json-panel', element);
  107 + toast.showError(error, toastParent, 'bottom left');
  108 + };
  109 +
  110 + scope.validate = function () {
  111 + if (!scope.contentBody || !scope.contentBody.length) {
  112 + if (scope.required) {
  113 + scope.validationError = 'Json object is required.';
  114 + scope.objectValid = false;
  115 + } else {
  116 + scope.validationError = '';
  117 + scope.objectValid = true;
  118 + }
  119 + return null;
  120 + } else {
  121 + try {
  122 + var object = angular.fromJson(scope.contentBody);
  123 + scope.validationError = '';
  124 + scope.objectValid = true;
  125 + return object;
  126 + } catch (e) {
  127 + var details = utils.parseException(e);
  128 + var errorInfo = 'Error:';
  129 + if (details.name) {
  130 + errorInfo += ' ' + details.name + ':';
  131 + }
  132 + if (details.message) {
  133 + errorInfo += ' ' + details.message;
  134 + }
  135 + scope.validationError = errorInfo;
  136 + scope.objectValid = false;
  137 + return null;
  138 + }
  139 + }
  140 + };
  141 +
  142 + scope.$on('form-submit', function () {
  143 + if (!scope.readonly) {
  144 + scope.cleanupJsonErrors();
  145 + if (!scope.objectValid) {
  146 + scope.showError(scope.validationError);
  147 + }
  148 + }
  149 + });
  150 +
  151 + scope.$on('update-ace-editor-size', function () {
  152 + updateEditorSize();
  153 + });
  154 +
  155 + $compile(element.contents())(scope);
  156 + }
  157 +
  158 + return {
  159 + restrict: "E",
  160 + require: "^ngModel",
  161 + scope: {
  162 + required:'=ngRequired',
  163 + readonly:'=ngReadonly',
  164 + fillHeight:'=?'
  165 + },
  166 + link: linker
  167 + };
  168 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +tb-json-object-edit {
  17 + position: relative;
  18 + .fill-height {
  19 + height: 100%;
  20 + }
  21 +}
  22 +
  23 +.tb-json-object-panel {
  24 + margin-left: 15px;
  25 + border: 1px solid #C0C0C0;
  26 + height: 100%;
  27 + #tb-json-input {
  28 + min-width: 200px;
  29 + width: 100%;
  30 + height: 100%;
  31 + &:not(.fill-height) {
  32 + min-height: 200px;
  33 + }
  34 + }
  35 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2018 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<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">
  19 + <div layout="row" layout-align="start center">
  20 + <label class="tb-title no-padding"
  21 + ng-class="{'tb-required': required,
  22 + 'tb-readonly': readonly,
  23 + 'tb-error': !objectValid}">{{ label }}</label>
  24 + <span flex></span>
  25 + <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
  26 + </div>
  27 + <div flex id="tb-json-panel" class="tb-json-object-panel" layout="column">
  28 + <div flex id="tb-json-input" ng-class="{'fill-height': fillHeight}"
  29 + ng-readonly="readonly"
  30 + ui-ace="jsonEditorOptions"
  31 + ng-model="contentBody">
  32 + </div>
  33 + </div>
  34 +</div>
... ...
... ... @@ -23,6 +23,8 @@ import FlatButton from 'material-ui/FlatButton';
23 23 import 'brace/ext/language_tools';
24 24 import 'brace/theme/github';
25 25
  26 +import fixAceEditor from './../ace-editor-fix';
  27 +
26 28 class ThingsboardAceEditor extends React.Component {
27 29
28 30 constructor(props) {
... ... @@ -31,6 +33,7 @@ class ThingsboardAceEditor extends React.Component {
31 33 this.onBlur = this.onBlur.bind(this);
32 34 this.onFocus = this.onFocus.bind(this);
33 35 this.onTidy = this.onTidy.bind(this);
  36 + this.onLoad = this.onLoad.bind(this);
34 37 var value = props.value ? props.value + '' : '';
35 38 this.state = {
36 39 value: value,
... ... @@ -72,6 +75,10 @@ class ThingsboardAceEditor extends React.Component {
72 75 }
73 76 }
74 77
  78 + onLoad(editor) {
  79 + fixAceEditor(editor);
  80 + }
  81 +
75 82 render() {
76 83
77 84 const styles = reactCSS({
... ... @@ -117,6 +124,7 @@ class ThingsboardAceEditor extends React.Component {
117 124 onChange={this.onValueChanged}
118 125 onFocus={this.onFocus}
119 126 onBlur={this.onBlur}
  127 + onLoad={this.onLoad}
120 128 name={this.props.form.title}
121 129 value={this.state.value}
122 130 readOnly={this.props.form.readonly}
... ...
... ... @@ -23,6 +23,8 @@ import thingsboardJsonForm from '../json-form.directive';
23 23 import thingsboardManageWidgetActions from './action/manage-widget-actions.directive';
24 24 import 'angular-ui-ace';
25 25
  26 +import fixAceEditor from './../ace-editor-fix';
  27 +
26 28 import './widget-config.scss';
27 29
28 30 /* eslint-disable import/no-unresolved, import/default */
... ... @@ -72,6 +74,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
72 74 enableSnippets: true,
73 75 enableBasicAutocompletion: true,
74 76 enableLiveAutocompletion: true
  77 + },
  78 + onLoad: function (_ace) {
  79 + fixAceEditor(_ace);
75 80 }
76 81 };
77 82
... ...
... ... @@ -128,8 +128,8 @@ export default function ExtensionFormOpcDirective($compile, $templateCache, $tra
128 128 let addedFile = event.target.result;
129 129
130 130 if (addedFile && addedFile.length > 0) {
131   - model[options.fileName] = $file.name;
132   - model[options.file] = addedFile.replace(/^data.*base64,/, "");
  131 + model[options.location] = $file.name;
  132 + model[options.fileContent] = addedFile.replace(/^data.*base64,/, "");
133 133
134 134 }
135 135 }
... ... @@ -142,8 +142,8 @@ export default function ExtensionFormOpcDirective($compile, $templateCache, $tra
142 142 scope.clearFile = function(model, options) {
143 143 scope.theForm.$setDirty();
144 144
145   - model[options.fileName] = null;
146   - model[options.file] = null;
  145 + model[options.location] = null;
  146 + model[options.fileContent] = null;
147 147
148 148 };
149 149
... ...
... ... @@ -212,8 +212,8 @@
212 212 </md-input-container>
213 213
214 214 <section class="dropdown-section">
215   - <div class="tb-container" ng-class="{'ng-invalid':!server.keystore.file}">
216   - <span ng-init='fieldsToFill = {"fileName":"fileName", "file":"file"}'></span>
  215 + <div class="tb-container" ng-class="{'ng-invalid':!server.keystore.fileContent}">
  216 + <span ng-init='fieldsToFill = {"location":"location", "fileContent":"fileContent"}'></span>
217 217 <label class="tb-label" translate>extension.opc-keystore-location</label>
218 218 <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, server.keystore, fieldsToFill)' class="tb-file-select-container">
219 219 <div class="tb-file-clear-container">
... ... @@ -231,14 +231,14 @@
231 231 class="file-input"
232 232 flow-btn id="dropFileKeystore_{{serverIndex}}"
233 233 name="keystoreFile"
234   - ng-model="server.keystore.file"
  234 + ng-model="server.keystore.fileContent"
235 235 >
236 236 </div>
237 237 </div>
238 238 </div>
239 239 <div class="dropdown-messages">
240   - <div ng-if="!server.keystore[fieldsToFill.fileName]" class="tb-error-message" translate>extension.no-file</div>
241   - <div ng-if="server.keystore[fieldsToFill.fileName]">{{server.keystore[fieldsToFill.fileName]}}</div>
  240 + <div ng-if="!server.keystore[fieldsToFill.location]" class="tb-error-message" translate>extension.no-file</div>
  241 + <div ng-if="server.keystore[fieldsToFill.location]">{{server.keystore[fieldsToFill.location]}}</div>
242 242 </div>
243 243 </section>
244 244
... ...
... ... @@ -29,6 +29,7 @@ import thingsboardNoAnimate from '../components/no-animate.directive';
29 29 import thingsboardOnFinishRender from '../components/finish-render.directive';
30 30 import thingsboardSideMenu from '../components/side-menu.directive';
31 31 import thingsboardDashboardAutocomplete from '../components/dashboard-autocomplete.directive';
  32 +import thingsboardJsonObjectEdit from '../components/json-object-edit.directive';
32 33
33 34 import thingsboardUserMenu from './user-menu.directive';
34 35
... ... @@ -90,7 +91,8 @@ export default angular.module('thingsboard.home', [
90 91 thingsboardNoAnimate,
91 92 thingsboardOnFinishRender,
92 93 thingsboardSideMenu,
93   - thingsboardDashboardAutocomplete
  94 + thingsboardDashboardAutocomplete,
  95 + thingsboardJsonObjectEdit
94 96 ])
95 97 .config(HomeRoutes)
96 98 .controller('HomeController', HomeController)
... ...
... ... @@ -1179,6 +1179,7 @@ export default angular.module('thingsboard.locale', [])
1179 1179 "delete": "Delete rule node",
1180 1180 "rulenode-details": "Rule node details",
1181 1181 "debug-mode": "Debug mode",
  1182 + "configuration": "Configuration",
1182 1183 "link-details": "Rule node link details",
1183 1184 "add-link": "Add link",
1184 1185 "link-label": "Link label",
... ...
... ... @@ -151,6 +151,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
151 151 },
152 152 'mouseLeave': function () {
153 153 destroyTooltips();
  154 + },
  155 + 'mouseDown': function () {
  156 + destroyTooltips();
154 157 }
155 158 }
156 159 };
... ... @@ -226,16 +229,12 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
226 229 edgeDoubleClick: function (event, edge) {
227 230 var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
228 231 if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
229   - ruleChainService.getRuleNodeSupportedLinks(sourceNode.component.clazz).then(
230   - (labels) => {
231   - vm.isEditingRuleNode = false;
232   - vm.editingRuleNode = null;
233   - vm.editingRuleNodeLinkLabels = labels;
234   - vm.isEditingRuleNodeLink = true;
235   - vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
236   - vm.editingRuleNodeLink = angular.copy(edge);
237   - }
238   - );
  232 + vm.isEditingRuleNode = false;
  233 + vm.editingRuleNode = null;
  234 + vm.editingRuleNodeLinkLabels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
  235 + vm.isEditingRuleNodeLink = true;
  236 + vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
  237 + vm.editingRuleNodeLink = angular.copy(edge);
239 238 }
240 239 },
241 240 nodeCallbacks: {
... ... @@ -267,16 +266,10 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
267 266 deferred.resolve(edge);
268 267 }
269 268 } else {
270   - ruleChainService.getRuleNodeSupportedLinks(sourceNode.component.clazz).then(
271   - (labels) => {
272   - addRuleNodeLink(event, edge, labels).then(
273   - (link) => {
274   - deferred.resolve(link);
275   - },
276   - () => {
277   - deferred.reject();
278   - }
279   - );
  269 + var labels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
  270 + addRuleNodeLink(event, edge, labels).then(
  271 + (link) => {
  272 + deferred.resolve(link);
280 273 },
281 274 () => {
282 275 deferred.reject();
... ... @@ -309,24 +302,19 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
309 302 y: 10+50*model.nodes.length,
310 303 connectors: []
311 304 };
312   - if (componentType == types.ruleNodeType.RULE_CHAIN.value) {
313   - node.connectors.push(
314   - {
315   - type: flowchartConstants.leftConnectorType,
316   - id: model.nodes.length
317   - }
318   - );
319   - } else {
  305 + if (ruleNodeComponent.configurationDescriptor.nodeDefinition.inEnabled) {
320 306 node.connectors.push(
321 307 {
322 308 type: flowchartConstants.leftConnectorType,
323   - id: model.nodes.length*2
  309 + id: model.nodes.length * 2
324 310 }
325 311 );
  312 + }
  313 + if (ruleNodeComponent.configurationDescriptor.nodeDefinition.outEnabled) {
326 314 node.connectors.push(
327 315 {
328 316 type: flowchartConstants.rightConnectorType,
329   - id: model.nodes.length*2+1
  317 + id: model.nodes.length * 2 + 1
330 318 }
331 319 );
332 320 }
... ... @@ -398,17 +386,24 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
398 386 name: ruleNode.name,
399 387 nodeClass: vm.types.ruleNodeType[component.type].nodeClass,
400 388 icon: vm.types.ruleNodeType[component.type].icon,
401   - connectors: [
  389 + connectors: []
  390 + };
  391 + if (component.configurationDescriptor.nodeDefinition.inEnabled) {
  392 + node.connectors.push(
402 393 {
403 394 type: flowchartConstants.leftConnectorType,
404 395 id: vm.nextConnectorID++
405   - },
  396 + }
  397 + );
  398 + }
  399 + if (component.configurationDescriptor.nodeDefinition.outEnabled) {
  400 + node.connectors.push(
406 401 {
407 402 type: flowchartConstants.rightConnectorType,
408 403 id: vm.nextConnectorID++
409 404 }
410   - ]
411   - };
  405 + );
  406 + }
412 407 nodes.push(node);
413 408 vm.ruleChainModel.nodes.push(node);
414 409 }
... ... @@ -590,6 +585,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
590 585 }
591 586
592 587 function addRuleNode($event, ruleNode) {
  588 +
  589 + ruleNode.configuration = angular.copy(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration);
  590 +
593 591 $mdDialog.show({
594 592 controller: 'AddRuleNodeController',
595 593 controllerAs: 'vm',
... ... @@ -601,13 +599,15 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
601 599 }).then(function (ruleNode) {
602 600 ruleNode.id = vm.nextNodeID++;
603 601 ruleNode.connectors = [];
604   - ruleNode.connectors.push(
605   - {
606   - id: vm.nextConnectorID++,
607   - type: flowchartConstants.leftConnectorType
608   - }
609   - );
610   - if (ruleNode.component.type != types.ruleNodeType.RULE_CHAIN.value) {
  602 + if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) {
  603 + ruleNode.connectors.push(
  604 + {
  605 + id: vm.nextConnectorID++,
  606 + type: flowchartConstants.leftConnectorType
  607 + }
  608 + );
  609 + }
  610 + if (ruleNode.component.configurationDescriptor.nodeDefinition.outEnabled) {
611 611 ruleNode.connectors.push(
612 612 {
613 613 id: vm.nextConnectorID++,
... ...
... ... @@ -38,6 +38,11 @@
38 38 ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }}
39 39 </md-checkbox>
40 40 </md-input-container>
  41 + <tb-json-object-edit class="tb-rule-node-configuration-json" ng-model="ruleNode.configuration"
  42 + label="{{ 'rulenode.configuration' | translate }}"
  43 + ng-required="true"
  44 + fill-height="true">
  45 + </tb-json-object-edit>
41 46 <md-input-container class="md-block">
42 47 <label translate>rulenode.description</label>
43 48 <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
... ...
... ... @@ -14,6 +14,8 @@
14 14 * limitations under the License.
15 15 */
16 16
  17 +import './rulenode.scss';
  18 +
17 19 /* eslint-disable import/no-unresolved, import/default */
18 20
19 21 import ruleNodeFieldsetTemplate from './rulenode-fieldset.tpl.html';
... ...
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +.tb-rulenode {
  18 + tb-json-object-edit.tb-rule-node-configuration-json {
  19 + height: 300px;
  20 + display: block;
  21 + }
  22 +}
\ No newline at end of file
... ...
... ... @@ -19,7 +19,7 @@
19 19 id="{{node.id}}"
20 20 ng-attr-style="position: absolute; top: {{ node.y }}px; left: {{ node.x }}px;"
21 21 ng-dblclick="callbacks.doubleClick($event, node)"
22   - ng-mouseover="callbacks.mouseOver($event, node)"
  22 + ng-mousedown="callbacks.mouseDown($event, node)"
23 23 ng-mouseenter="callbacks.mouseEnter($event, node)"
24 24 ng-mouseleave="callbacks.mouseLeave($event, node)">
25 25 <div class="tb-rule-node {{node.nodeClass}}">
... ...
... ... @@ -203,6 +203,12 @@ md-sidenav {
203 203 * THINGSBOARD SPECIFIC
204 204 ***********************/
205 205
  206 +$swift-ease-out-duration: 0.4s !default;
  207 +$swift-ease-out-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
  208 +
  209 +$input-label-float-offset: 6px !default;
  210 +$input-label-float-scale: 0.75 !default;
  211 +
206 212 label {
207 213 &.tb-title {
208 214 pointer-events: none;
... ... @@ -213,6 +219,18 @@ label {
213 219 &.no-padding {
214 220 padding-bottom: 0px;
215 221 }
  222 + &.tb-required:after {
  223 + content: ' *';
  224 + font-size: 13px;
  225 + vertical-align: top;
  226 + color: rgba(0,0,0,0.54);
  227 + }
  228 + &.tb-error {
  229 + color: rgb(221,44,0);
  230 + &.tb-required:after {
  231 + color: rgb(221,44,0);
  232 + }
  233 + }
216 234 }
217 235 }
218 236
... ...