thingsboard-memoizeit

Rule Node Configuration

3/23/2018 5:41:29 AM

Changes

rule-engine/rule-engine-api/src/main/resources/EmptyNodeConfig.json 2(+0 -2)

rule-engine/rule-engine-api/src/main/resources/EmptyNodeDescriptor.json 2(+0 -2)

Details

diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
index 52d7d7d..479f424 100644
--- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
@@ -180,7 +180,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
         return scannedComponent;
     }
 
-    private NodeDefinition prepareNodeDefinition(RuleNode nodeAnnotation) throws IOException {
+    private NodeDefinition prepareNodeDefinition(RuleNode nodeAnnotation) throws Exception {
         NodeDefinition nodeDefinition = new NodeDefinition();
         nodeDefinition.setDetails(nodeAnnotation.nodeDetails());
         nodeDefinition.setDescription(nodeAnnotation.nodeDescription());
@@ -188,9 +188,10 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
         nodeDefinition.setOutEnabled(nodeAnnotation.outEnabled());
         nodeDefinition.setRelationTypes(nodeAnnotation.relationTypes());
         nodeDefinition.setCustomRelations(nodeAnnotation.customRelations());
-        String defaultConfigResourceName = nodeAnnotation.defaultConfigResource();
-        nodeDefinition.setDefaultConfiguration(mapper.readTree(
-                Resources.toString(Resources.getResource(defaultConfigResourceName), Charsets.UTF_8)));
+        Class<? extends NodeConfiguration> configClazz = nodeAnnotation.configClazz();
+        NodeConfiguration config = configClazz.newInstance();
+        NodeConfiguration defaultConfiguration = config.defaultConfiguration();
+        nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration));
         return nodeDefinition;
     }
 
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 2758521..1df2f1c 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -234,7 +234,7 @@ caffeine:
   specs:
     relations:
       timeToLiveInMinutes: 1440
-      maxSize: 100000
+      maxSize: 0
     deviceCredentials:
       timeToLiveInMinutes: 1440
       maxSize: 100000
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeConfiguration.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeConfiguration.java
new file mode 100644
index 0000000..5e4c4b5
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeConfiguration.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.api;
+
+public interface NodeConfiguration {
+
+    NodeConfiguration defaultConfiguration();
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java
index f8e0fa2..1617034 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java
@@ -35,15 +35,16 @@ public @interface RuleNode {
 
     String nodeDetails();
 
+    Class<? extends NodeConfiguration> configClazz();
+
     boolean inEnabled() default true;
 
     boolean outEnabled() default true;
 
     ComponentScope scope() default ComponentScope.TENANT;
 
-    String defaultConfigResource() default "EmptyNodeConfig.json";
-
     String[] relationTypes() default {"Success", "Failure"};
 
     boolean customRelations() default false;
+
 }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
index c85d480..07b166d 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
@@ -30,6 +30,7 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
 @RuleNode(
         type = ComponentType.FILTER,
         name = "script", relationTypes = {"True", "False", "Failure"},
+        configClazz = TbJsFilterNodeConfiguration.class,
         nodeDescription = "Filter incoming messages using JS script",
         nodeDetails = "Evaluate incoming Message with configured JS condition. " +
                 "If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." +
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java
index bf543e3..3b19c7c 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java
@@ -16,9 +16,17 @@
 package org.thingsboard.rule.engine.filter;
 
 import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
 
 @Data
-public class TbJsFilterNodeConfiguration {
+public class TbJsFilterNodeConfiguration implements NodeConfiguration {
 
     private String jsScript;
+
+    @Override
+    public TbJsFilterNodeConfiguration defaultConfiguration() {
+        TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration();
+        configuration.setJsScript("msg.passed < 15 && msg.name === 'Vit' && meta.temp == 10 && msg.bigObj.prop == 42;");
+        return configuration;
+    }
 }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
index faf97b4..c1236a4 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
@@ -31,6 +31,7 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
 @RuleNode(
         type = ComponentType.FILTER,
         name = "switch", customRelations = true,
+        configClazz = TbJsSwitchNodeConfiguration.class,
         nodeDescription = "Route incoming Message to one or multiple output chains",
         nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " +
                 "If Array is empty - message not routed to next Node. " +
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java
index 331302d..b354c71 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java
@@ -15,14 +15,29 @@
  */
 package org.thingsboard.rule.engine.filter;
 
+import com.google.common.collect.Sets;
 import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
 
 import java.util.Set;
 
 @Data
-public class TbJsSwitchNodeConfiguration {
+public class TbJsSwitchNodeConfiguration implements NodeConfiguration {
 
     private String jsScript;
     private Set<String> allowedRelations;
     private boolean routeToAllWithNoCheck;
+
+    @Override
+    public TbJsSwitchNodeConfiguration defaultConfiguration() {
+        TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration();
+        configuration.setJsScript("function nextRelation(meta, msg) {\n" +
+                "    return ['one','nine'];" +
+                "};\n" +
+                "\n" +
+                "nextRelation(meta, msg);");
+        configuration.setAllowedRelations(Sets.newHashSet("one", "two"));
+        configuration.setRouteToAllWithNoCheck(false);
+        return configuration;
+    }
 }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java
index 7a6f9fd..3a86c25 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java
@@ -28,6 +28,7 @@ import org.thingsboard.server.common.msg.TbMsg;
 @RuleNode(
         type = ComponentType.FILTER,
         name = "message type",
+        configClazz = TbMsgTypeFilterNodeConfiguration.class,
         nodeDescription = "Filter incoming messages by Message Type",
         nodeDetails = "Evaluate incoming Message with configured JS condition. " +
                 "If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.")
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java
index 3b7ba90..a2e1b17 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java
@@ -16,15 +16,24 @@
 package org.thingsboard.rule.engine.filter;
 
 import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
 
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 /**
  * Created by ashvayka on 19.01.18.
  */
 @Data
-public class TbMsgTypeFilterNodeConfiguration {
+public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration {
 
     private List<String> messageTypes;
 
+    @Override
+    public TbMsgTypeFilterNodeConfiguration defaultConfiguration() {
+        TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration();
+        configuration.setMessageTypes(Arrays.asList("GET_ATTRIBUTES","POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST"));
+        return configuration;
+    }
 }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
index 6228206..69ee9d7 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
@@ -38,6 +38,7 @@ import static org.thingsboard.server.common.data.DataConstants.*;
 @Slf4j
 @RuleNode(type = ComponentType.ENRICHMENT,
           name = "originator attributes",
+          configClazz = TbGetAttributesNodeConfiguration.class,
           nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata",
           nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
                 "with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " +
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
index ad92314..103b4de 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
@@ -16,14 +16,16 @@
 package org.thingsboard.rule.engine.metadata;
 
 import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
 
+import java.util.Collections;
 import java.util.List;
 
 /**
  * Created by ashvayka on 19.01.18.
  */
 @Data
-public class TbGetAttributesNodeConfiguration {
+public class TbGetAttributesNodeConfiguration implements NodeConfiguration {
 
     private List<String> clientAttributeNames;
     private List<String> sharedAttributeNames;
@@ -31,4 +33,13 @@ public class TbGetAttributesNodeConfiguration {
 
     private List<String> latestTsKeyNames;
 
+    @Override
+    public TbGetAttributesNodeConfiguration defaultConfiguration() {
+        TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
+        configuration.setClientAttributeNames(Collections.emptyList());
+        configuration.setSharedAttributeNames(Collections.emptyList());
+        configuration.setServerAttributeNames(Collections.emptyList());
+        configuration.setLatestTsKeyNames(Collections.emptyList());
+        return configuration;
+    }
 }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
index d85fb56..cc6d6a1 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
@@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
 @RuleNode(
         type = ComponentType.ENRICHMENT,
         name="customer attributes",
+        configClazz = TbGetEntityAttrNodeConfiguration.class,
         nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata",
         nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
                 "To access those attributes in other nodes this template can be used " +
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java
index a5e85c5..5195115 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java
@@ -16,13 +16,25 @@
 package org.thingsboard.rule.engine.metadata;
 
 import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
 
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
 
 @Data
-public class TbGetEntityAttrNodeConfiguration {
+public class TbGetEntityAttrNodeConfiguration implements NodeConfiguration {
 
     private Map<String, String> attrMapping;
     private boolean isTelemetry = false;
+
+    @Override
+    public TbGetEntityAttrNodeConfiguration defaultConfiguration() {
+        TbGetEntityAttrNodeConfiguration configuration = new TbGetEntityAttrNodeConfiguration();
+        Map<String, String> attrMapping = new HashMap<>();
+        attrMapping.putIfAbsent("temperature", "tempo");
+        configuration.setAttrMapping(attrMapping);
+        configuration.setTelemetry(true);
+        return configuration;
+    }
 }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
index 26b7561..22c0b9f 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
@@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
 @RuleNode(
         type = ComponentType.ENRICHMENT,
         name="related attributes",
+        configClazz = TbGetRelatedAttrNodeConfiguration.class,
         nodeDescription = "Add Originators Related Entity Attributes or Latest Telemetry into Message Metadata",
         nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
                 "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " +
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java
index ae0b662..8211992 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java
@@ -16,11 +16,28 @@
 package org.thingsboard.rule.engine.metadata;
 
 import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
 
+import java.util.HashMap;
+import java.util.Map;
+
 @Data
-public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration  {
+public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration {
 
     private String relationType;
     private EntitySearchDirection direction;
+
+    @Override
+    public TbGetRelatedAttrNodeConfiguration defaultConfiguration() {
+        TbGetRelatedAttrNodeConfiguration configuration = new TbGetRelatedAttrNodeConfiguration();
+        Map<String, String> attrMapping = new HashMap<>();
+        attrMapping.putIfAbsent("temperature", "tempo");
+        configuration.setAttrMapping(attrMapping);
+        configuration.setTelemetry(true);
+        configuration.setRelationType(EntityRelation.CONTAINS_TYPE);
+        configuration.setDirection(EntitySearchDirection.FROM);
+        return configuration;
+    }
 }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java
index 7d9c50b..b5f5e02 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java
@@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
 @RuleNode(
         type = ComponentType.ENRICHMENT,
         name="tenant attributes",
+        configClazz = TbGetEntityAttrNodeConfiguration.class,
         nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata",
         nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
                 "To access those attributes in other nodes this template can be used " +
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
index d237df8..40a647a 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
@@ -36,6 +36,7 @@ import java.util.HashSet;
 @RuleNode(
         type = ComponentType.TRANSFORMATION,
         name="change originator",
+        configClazz = TbChangeOriginatorNodeConfiguration.class,
         nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity",
         nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
                 "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ")
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java
index cf03681..3370408 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java
@@ -16,12 +16,24 @@
 package org.thingsboard.rule.engine.transform;
 
 import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
 
 @Data
-public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration{
+public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
 
     private String originatorSource;
     private EntitySearchDirection direction;
     private String relationType;
+
+    @Override
+    public TbChangeOriginatorNodeConfiguration defaultConfiguration() {
+        TbChangeOriginatorNodeConfiguration configuration = new TbChangeOriginatorNodeConfiguration();
+        configuration.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE);
+        configuration.setDirection(EntitySearchDirection.FROM);
+        configuration.setRelationType(EntityRelation.CONTAINS_TYPE);
+        configuration.setStartNewChain(false);
+        return configuration;
+    }
 }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
index babdbc3..626790f 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
@@ -27,6 +27,7 @@ import javax.script.Bindings;
 @RuleNode(
         type = ComponentType.TRANSFORMATION,
         name = "script",
+        configClazz = TbTransformMsgNodeConfiguration.class,
         nodeDescription = "Change Message payload and Metadata using JavaScript",
         nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " +
                 "<code>meta</code> - is a Message metadata.<br/>" +
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java
index 9cc926b..4f9e9eb 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java
@@ -16,9 +16,18 @@
 package org.thingsboard.rule.engine.transform;
 
 import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
 
 @Data
-public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguration {
+public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
 
     private String jsScript;
+
+    @Override
+    public TbTransformMsgNodeConfiguration defaultConfiguration() {
+        TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration();
+        configuration.setStartNewChain(false);
+        configuration.setJsScript("msg.passed = msg.passed * meta.temp; msg.bigObj.newProp = 'Ukraine' ");
+        return configuration;
+    }
 }
diff --git a/ui/src/app/api/rule-chain.service.js b/ui/src/app/api/rule-chain.service.js
index f175535..ebc48fa 100644
--- a/ui/src/app/api/rule-chain.service.js
+++ b/ui/src/app/api/rule-chain.service.js
@@ -153,16 +153,21 @@ function RuleChainService($http, $q, $filter, types, componentDescriptorService)
         return deferred.promise;
     }
 
-    function getRuleNodeSupportedLinks(nodeType) { //eslint-disable-line
-        //TODO:
-        var deferred = $q.defer();
-        var linkLabels = [
-            { name: 'Success', custom: false },
-            { name: 'Fail', custom: false },
-            { name: 'Custom', custom: true },
-        ];
-        deferred.resolve(linkLabels);
-        return deferred.promise;
+    function getRuleNodeSupportedLinks(component) {
+        var relationTypes = component.configurationDescriptor.nodeDefinition.relationTypes;
+        var customRelations = component.configurationDescriptor.nodeDefinition.customRelations;
+        var linkLabels = [];
+        for (var i=0;i<relationTypes.length;i++) {
+            linkLabels.push({
+                name: relationTypes[i], custom: false
+            });
+        }
+        if (customRelations) {
+            linkLabels.push(
+                { name: 'Custom', custom: true }
+            );
+        }
+        return linkLabels;
     }
 
     function getRuleNodeComponents() {
diff --git a/ui/src/app/components/json-object-edit.directive.js b/ui/src/app/components/json-object-edit.directive.js
new file mode 100644
index 0000000..03ddc07
--- /dev/null
+++ b/ui/src/app/components/json-object-edit.directive.js
@@ -0,0 +1,165 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './json-object-edit.scss';
+
+import 'brace/ext/language_tools';
+import 'brace/mode/json';
+import 'ace-builds/src-min-noconflict/snippets/json';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import jsonObjectEditTemplate from './json-object-edit.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.jsonObjectEdit', [])
+    .directive('tbJsonObjectEdit', JsonObjectEdit)
+    .name;
+
+/*@ngInject*/
+function JsonObjectEdit($compile, $templateCache, toast, utils) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(jsonObjectEditTemplate);
+        element.html(template);
+
+        scope.label = attrs.label;
+
+        scope.objectValid = true;
+        scope.validationError = '';
+
+        scope.json_editor;
+
+        scope.onFullscreenChanged = function () {
+            updateEditorSize();
+        };
+
+        function updateEditorSize() {
+            if (scope.json_editor) {
+                scope.json_editor.resize();
+                scope.json_editor.renderer.updateFull();
+            }
+        }
+
+        scope.jsonEditorOptions = {
+            useWrapMode: true,
+            mode: 'json',
+            advanced: {
+                enableSnippets: true,
+                enableBasicAutocompletion: true,
+                enableLiveAutocompletion: true
+            },
+            onLoad: function (_ace) {
+                scope.json_editor = _ace;
+                scope.json_editor.session.on("change", function () {
+                    scope.cleanupJsonErrors();
+                });
+            }
+        };
+
+        scope.cleanupJsonErrors = function () {
+            toast.hide();
+        };
+
+        scope.updateValidity = function () {
+            ngModelCtrl.$setValidity('objectValid', scope.objectValid);
+        };
+
+        scope.$watch('contentBody', function (newVal, prevVal) {
+            if (!angular.equals(newVal, prevVal)) {
+                var object = scope.validate();
+                ngModelCtrl.$setViewValue(object);
+                scope.updateValidity();
+            }
+        });
+
+        ngModelCtrl.$render = function () {
+            var object = ngModelCtrl.$viewValue;
+            var content = '';
+            try {
+                if (object) {
+                    content = angular.toJson(object, true);
+                }
+            } catch (e) {
+                //
+            }
+            scope.contentBody = content;
+        };
+
+        scope.showError = function (error) {
+            var toastParent = angular.element('#tb-json-panel', element);
+            toast.showError(error, toastParent, 'bottom left');
+        };
+
+        scope.validate = function () {
+            if (!scope.contentBody || !scope.contentBody.length) {
+                if (scope.required) {
+                    scope.validationError = 'Json object is required.';
+                    scope.objectValid = false;
+                } else {
+                    scope.validationError = '';
+                    scope.objectValid = true;
+                }
+                return null;
+            } else {
+                try {
+                    var object = angular.fromJson(scope.contentBody);
+                    scope.validationError = '';
+                    scope.objectValid = true;
+                    return object;
+                } catch (e) {
+                    var details = utils.parseException(e);
+                    var errorInfo = 'Error:';
+                    if (details.name) {
+                        errorInfo += ' ' + details.name + ':';
+                    }
+                    if (details.message) {
+                        errorInfo += ' ' + details.message;
+                    }
+                    scope.validationError = errorInfo;
+                    scope.objectValid = false;
+                    return null;
+                }
+            }
+        };
+
+        scope.$on('form-submit', function () {
+            if (!scope.readonly) {
+                scope.cleanupJsonErrors();
+                if (!scope.objectValid) {
+                    scope.showError(scope.validationError);
+                }
+            }
+        });
+
+        scope.$on('update-ace-editor-size', function () {
+            updateEditorSize();
+        });
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        scope: {
+            required:'=ngRequired',
+            readonly:'=ngReadonly',
+            fillHeight:'=?'
+        },
+        link: linker
+    };
+}
diff --git a/ui/src/app/components/json-object-edit.scss b/ui/src/app/components/json-object-edit.scss
new file mode 100644
index 0000000..232d69a
--- /dev/null
+++ b/ui/src/app/components/json-object-edit.scss
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+tb-json-object-edit {
+  position: relative;
+  .fill-height {
+    height: 100%;
+  }
+}
+
+.tb-json-object-panel {
+  margin-left: 15px;
+  border: 1px solid #C0C0C0;
+  height: 100%;
+  #tb-json-input {
+    min-width: 200px;
+    width: 100%;
+    height: 100%;
+    &:not(.fill-height) {
+      min-height: 200px;
+    }
+  }
+}
diff --git a/ui/src/app/components/json-object-edit.tpl.html b/ui/src/app/components/json-object-edit.tpl.html
new file mode 100644
index 0000000..ebab3c7
--- /dev/null
+++ b/ui/src/app/components/json-object-edit.tpl.html
@@ -0,0 +1,34 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
+    <div layout="row" layout-align="start center">
+        <label class="tb-title no-padding"
+               ng-class="{'tb-required': required,
+                          'tb-readonly': readonly,
+                          'tb-error': !objectValid}">{{ label }}</label>
+        <span flex></span>
+        <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
+    </div>
+    <div flex id="tb-json-panel" class="tb-json-object-panel" layout="column">
+        <div flex id="tb-json-input" ng-class="{'fill-height': fillHeight}"
+             ng-readonly="readonly"
+             ui-ace="jsonEditorOptions"
+             ng-model="contentBody">
+        </div>
+    </div>
+</div>
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
index e90334b..d397d14 100644
--- a/ui/src/app/layout/index.js
+++ b/ui/src/app/layout/index.js
@@ -29,6 +29,7 @@ import thingsboardNoAnimate from '../components/no-animate.directive';
 import thingsboardOnFinishRender from '../components/finish-render.directive';
 import thingsboardSideMenu from '../components/side-menu.directive';
 import thingsboardDashboardAutocomplete from '../components/dashboard-autocomplete.directive';
+import thingsboardJsonObjectEdit from '../components/json-object-edit.directive';
 
 import thingsboardUserMenu from './user-menu.directive';
 
@@ -90,7 +91,8 @@ export default angular.module('thingsboard.home', [
     thingsboardNoAnimate,
     thingsboardOnFinishRender,
     thingsboardSideMenu,
-    thingsboardDashboardAutocomplete
+    thingsboardDashboardAutocomplete,
+    thingsboardJsonObjectEdit
 ])
     .config(HomeRoutes)
     .controller('HomeController', HomeController)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 360c828..cca2a11 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -1179,6 +1179,7 @@ export default angular.module('thingsboard.locale', [])
                     "delete": "Delete rule node",
                     "rulenode-details": "Rule node details",
                     "debug-mode": "Debug mode",
+                    "configuration": "Configuration",
                     "link-details": "Rule node link details",
                     "add-link": "Add link",
                     "link-label": "Link label",
diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js
index d9bbf2f..b792f13 100644
--- a/ui/src/app/rulechain/rulechain.controller.js
+++ b/ui/src/app/rulechain/rulechain.controller.js
@@ -151,6 +151,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
             },
             'mouseLeave': function () {
                 destroyTooltips();
+            },
+            'mouseDown': function () {
+                destroyTooltips();
             }
         }
     };
@@ -226,16 +229,12 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
         edgeDoubleClick: function (event, edge) {
             var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
             if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
-                ruleChainService.getRuleNodeSupportedLinks(sourceNode.component.clazz).then(
-                    (labels) => {
-                        vm.isEditingRuleNode = false;
-                        vm.editingRuleNode = null;
-                        vm.editingRuleNodeLinkLabels = labels;
-                        vm.isEditingRuleNodeLink = true;
-                        vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
-                        vm.editingRuleNodeLink = angular.copy(edge);
-                    }
-                );
+                vm.isEditingRuleNode = false;
+                vm.editingRuleNode = null;
+                vm.editingRuleNodeLinkLabels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
+                vm.isEditingRuleNodeLink = true;
+                vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
+                vm.editingRuleNodeLink = angular.copy(edge);
             }
         },
         nodeCallbacks: {
@@ -267,16 +266,10 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
                     deferred.resolve(edge);
                 }
             } else {
-                ruleChainService.getRuleNodeSupportedLinks(sourceNode.component.clazz).then(
-                    (labels) => {
-                        addRuleNodeLink(event, edge, labels).then(
-                            (link) => {
-                                deferred.resolve(link);
-                            },
-                            () => {
-                                deferred.reject();
-                            }
-                        );
+                var labels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
+                addRuleNodeLink(event, edge, labels).then(
+                    (link) => {
+                        deferred.resolve(link);
                     },
                     () => {
                         deferred.reject();
@@ -309,24 +302,19 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
                         y: 10+50*model.nodes.length,
                         connectors: []
                     };
-                    if (componentType == types.ruleNodeType.RULE_CHAIN.value) {
-                        node.connectors.push(
-                            {
-                                type: flowchartConstants.leftConnectorType,
-                                id: model.nodes.length
-                            }
-                        );
-                    } else {
+                    if (ruleNodeComponent.configurationDescriptor.nodeDefinition.inEnabled) {
                         node.connectors.push(
                             {
                                 type: flowchartConstants.leftConnectorType,
-                                id: model.nodes.length*2
+                                id: model.nodes.length * 2
                             }
                         );
+                    }
+                    if (ruleNodeComponent.configurationDescriptor.nodeDefinition.outEnabled) {
                         node.connectors.push(
                             {
                                 type: flowchartConstants.rightConnectorType,
-                                id: model.nodes.length*2+1
+                                id: model.nodes.length * 2 + 1
                             }
                         );
                     }
@@ -398,17 +386,24 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
                     name: ruleNode.name,
                     nodeClass: vm.types.ruleNodeType[component.type].nodeClass,
                     icon: vm.types.ruleNodeType[component.type].icon,
-                    connectors: [
+                    connectors: []
+                };
+                if (component.configurationDescriptor.nodeDefinition.inEnabled) {
+                    node.connectors.push(
                         {
                             type: flowchartConstants.leftConnectorType,
                             id: vm.nextConnectorID++
-                        },
+                        }
+                    );
+                }
+                if (component.configurationDescriptor.nodeDefinition.outEnabled) {
+                    node.connectors.push(
                         {
                             type: flowchartConstants.rightConnectorType,
                             id: vm.nextConnectorID++
                         }
-                    ]
-                };
+                    );
+                }
                 nodes.push(node);
                 vm.ruleChainModel.nodes.push(node);
             }
@@ -590,6 +585,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
     }
 
     function addRuleNode($event, ruleNode) {
+
+        ruleNode.configuration = angular.copy(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration);
+
         $mdDialog.show({
             controller: 'AddRuleNodeController',
             controllerAs: 'vm',
@@ -601,13 +599,15 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
         }).then(function (ruleNode) {
             ruleNode.id = vm.nextNodeID++;
             ruleNode.connectors = [];
-            ruleNode.connectors.push(
-                {
-                    id: vm.nextConnectorID++,
-                    type: flowchartConstants.leftConnectorType
-                }
-            );
-            if (ruleNode.component.type != types.ruleNodeType.RULE_CHAIN.value) {
+            if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) {
+                ruleNode.connectors.push(
+                    {
+                        id: vm.nextConnectorID++,
+                        type: flowchartConstants.leftConnectorType
+                    }
+                );
+            }
+            if (ruleNode.component.configurationDescriptor.nodeDefinition.outEnabled) {
                 ruleNode.connectors.push(
                     {
                         id: vm.nextConnectorID++,
diff --git a/ui/src/app/rulechain/rulenode.directive.js b/ui/src/app/rulechain/rulenode.directive.js
index 998e998..be3e9c3 100644
--- a/ui/src/app/rulechain/rulenode.directive.js
+++ b/ui/src/app/rulechain/rulenode.directive.js
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import './rulenode.scss';
+
 /* eslint-disable import/no-unresolved, import/default */
 
 import ruleNodeFieldsetTemplate from './rulenode-fieldset.tpl.html';
diff --git a/ui/src/app/rulechain/rulenode.scss b/ui/src/app/rulechain/rulenode.scss
new file mode 100644
index 0000000..febc637
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode.scss
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.tb-rulenode {
+  tb-json-object-edit.tb-rule-node-configuration-json {
+    height: 300px;
+    display: block;
+  }
+}
\ No newline at end of file
diff --git a/ui/src/app/rulechain/rulenode.tpl.html b/ui/src/app/rulechain/rulenode.tpl.html
index 5a521a8..ffc8a0f 100644
--- a/ui/src/app/rulechain/rulenode.tpl.html
+++ b/ui/src/app/rulechain/rulenode.tpl.html
@@ -19,7 +19,7 @@
         id="{{node.id}}"
         ng-attr-style="position: absolute; top: {{ node.y }}px; left: {{ node.x }}px;"
         ng-dblclick="callbacks.doubleClick($event, node)"
-        ng-mouseover="callbacks.mouseOver($event, node)"
+        ng-mousedown="callbacks.mouseDown($event, node)"
         ng-mouseenter="callbacks.mouseEnter($event, node)"
         ng-mouseleave="callbacks.mouseLeave($event, node)">
     <div class="tb-rule-node {{node.nodeClass}}">
diff --git a/ui/src/app/rulechain/rulenode-fieldset.tpl.html b/ui/src/app/rulechain/rulenode-fieldset.tpl.html
index 0d16e45..30cf075 100644
--- a/ui/src/app/rulechain/rulenode-fieldset.tpl.html
+++ b/ui/src/app/rulechain/rulenode-fieldset.tpl.html
@@ -38,6 +38,11 @@
                              ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }}
                 </md-checkbox>
             </md-input-container>
+            <tb-json-object-edit class="tb-rule-node-configuration-json" ng-model="ruleNode.configuration"
+                                 label="{{ 'rulenode.configuration' | translate }}"
+                                 ng-required="true"
+                                 fill-height="true">
+            </tb-json-object-edit>
             <md-input-container class="md-block">
                 <label translate>rulenode.description</label>
                 <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 93ff320..6aa662c 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -203,6 +203,12 @@ md-sidenav {
  * THINGSBOARD SPECIFIC
  ***********************/
 
+$swift-ease-out-duration: 0.4s !default;
+$swift-ease-out-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
+
+$input-label-float-offset: 6px !default;
+$input-label-float-scale: 0.75 !default;
+
 label {
   &.tb-title {
     pointer-events: none;
@@ -213,6 +219,18 @@ label {
     &.no-padding {
       padding-bottom: 0px;
     }
+    &.tb-required:after {
+      content: ' *';
+      font-size: 13px;
+      vertical-align: top;
+      color: rgba(0,0,0,0.54);
+    }
+    &.tb-error {
+      color: rgb(221,44,0);
+      &.tb-required:after {
+        color: rgb(221,44,0);
+      }
+    }
   }
 }