Merge pull request #7243 from YuriyLytvynchuk/feature/node_json_path
[3.4.2]Feature: New ruleNode 'json path'
This commit is contained in:
		
						commit
						204266496e
					
				@ -286,7 +286,6 @@
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.jayway.jsonpath</groupId>
 | 
			
		||||
            <artifactId>json-path</artifactId>
 | 
			
		||||
            <scope>test</scope>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.jayway.jsonpath</groupId>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								pom.xml
									
									
									
									
									
								
							@ -1213,7 +1213,6 @@
 | 
			
		||||
                <groupId>com.jayway.jsonpath</groupId>
 | 
			
		||||
                <artifactId>json-path</artifactId>
 | 
			
		||||
                <version>${json-path.version}</version>
 | 
			
		||||
                <scope>test</scope>
 | 
			
		||||
            </dependency>
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>com.jayway.jsonpath</groupId>
 | 
			
		||||
 | 
			
		||||
@ -160,6 +160,10 @@
 | 
			
		||||
            </exclusions>
 | 
			
		||||
            <scope>test</scope>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.jayway.jsonpath</groupId>
 | 
			
		||||
            <artifactId>json-path</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
    </dependencies>
 | 
			
		||||
 | 
			
		||||
    <build>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,82 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2022 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.rule.engine.transform;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.databind.JsonNode;
 | 
			
		||||
import com.jayway.jsonpath.Configuration;
 | 
			
		||||
import com.jayway.jsonpath.JsonPath;
 | 
			
		||||
import com.jayway.jsonpath.PathNotFoundException;
 | 
			
		||||
import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.thingsboard.common.util.JacksonUtil;
 | 
			
		||||
import org.thingsboard.rule.engine.api.RuleNode;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbContext;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbNode;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbNodeException;
 | 
			
		||||
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
 | 
			
		||||
import org.thingsboard.server.common.data.plugin.ComponentType;
 | 
			
		||||
import org.thingsboard.server.common.msg.TbMsg;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.ExecutionException;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
@RuleNode(
 | 
			
		||||
        type = ComponentType.TRANSFORMATION,
 | 
			
		||||
        name = "json path",
 | 
			
		||||
        configClazz = TbJsonPathNodeConfiguration.class,
 | 
			
		||||
        nodeDescription = "Transforms incoming message body using JSONPath expression.",
 | 
			
		||||
        nodeDetails = "JSONPath expression specifies a path to an element or a set of elements in a JSON structure. <br/>"
 | 
			
		||||
                + "<b>'$'</b> represents the root object or array. <br/>"
 | 
			
		||||
                + "If JSONPath expression evaluation failed, incoming message routes via <code>Failure</code> chain, "
 | 
			
		||||
                + "otherwise <code>Success</code> chain is used.",
 | 
			
		||||
        uiResources = {"static/rulenode/rulenode-core-config.js"},
 | 
			
		||||
        icon = "functions",
 | 
			
		||||
        configDirective = "tbTransformationNodeJsonPathConfig"
 | 
			
		||||
)
 | 
			
		||||
public class TbJsonPathNode implements TbNode {
 | 
			
		||||
 | 
			
		||||
    private TbJsonPathNodeConfiguration config;
 | 
			
		||||
    private Configuration configurationJsonPath;
 | 
			
		||||
    private JsonPath jsonPath;
 | 
			
		||||
    private String jsonPathValue;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
 | 
			
		||||
        this.config = TbNodeUtils.convert(configuration, TbJsonPathNodeConfiguration.class);
 | 
			
		||||
        this.jsonPathValue = config.getJsonPath();
 | 
			
		||||
        if (!TbJsonPathNodeConfiguration.DEFAULT_JSON_PATH.equals(this.jsonPathValue)) {
 | 
			
		||||
            this.configurationJsonPath = Configuration.builder()
 | 
			
		||||
                    .jsonProvider(new JacksonJsonNodeJsonProvider())
 | 
			
		||||
                    .build();
 | 
			
		||||
            this.jsonPath = JsonPath.compile(config.getJsonPath());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
 | 
			
		||||
        if (!TbJsonPathNodeConfiguration.DEFAULT_JSON_PATH.equals(this.jsonPathValue)) {
 | 
			
		||||
            try {
 | 
			
		||||
                JsonNode jsonPathData = jsonPath.read(msg.getData(), this.configurationJsonPath);
 | 
			
		||||
                ctx.tellSuccess(TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), JacksonUtil.toString(jsonPathData)));
 | 
			
		||||
            } catch (PathNotFoundException e) {
 | 
			
		||||
                ctx.tellFailure(msg, e);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            ctx.tellSuccess(msg);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,34 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2022 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.rule.engine.transform;
 | 
			
		||||
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import org.thingsboard.rule.engine.api.NodeConfiguration;
 | 
			
		||||
 | 
			
		||||
@Data
 | 
			
		||||
public class TbJsonPathNodeConfiguration implements NodeConfiguration<TbJsonPathNodeConfiguration> {
 | 
			
		||||
 | 
			
		||||
    static final String DEFAULT_JSON_PATH = "$";
 | 
			
		||||
    private String jsonPath;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public TbJsonPathNodeConfiguration defaultConfiguration() {
 | 
			
		||||
        TbJsonPathNodeConfiguration configuration = new TbJsonPathNodeConfiguration();
 | 
			
		||||
        configuration.setJsonPath(DEFAULT_JSON_PATH);
 | 
			
		||||
        return configuration;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,167 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2022 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.rule.engine.transform;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.databind.JsonNode;
 | 
			
		||||
import com.fasterxml.jackson.databind.ObjectMapper;
 | 
			
		||||
import com.jayway.jsonpath.PathNotFoundException;
 | 
			
		||||
import org.junit.jupiter.api.AfterEach;
 | 
			
		||||
import org.junit.jupiter.api.BeforeEach;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
import org.mockito.ArgumentCaptor;
 | 
			
		||||
import org.thingsboard.common.util.JacksonUtil;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbContext;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbNodeException;
 | 
			
		||||
import org.thingsboard.server.common.data.id.DeviceId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.EntityId;
 | 
			
		||||
import org.thingsboard.server.common.msg.TbMsg;
 | 
			
		||||
import org.thingsboard.server.common.msg.TbMsgMetaData;
 | 
			
		||||
import org.thingsboard.server.common.msg.queue.TbMsgCallback;
 | 
			
		||||
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.any;
 | 
			
		||||
import static org.mockito.Mockito.mock;
 | 
			
		||||
import static org.mockito.Mockito.never;
 | 
			
		||||
import static org.mockito.Mockito.spy;
 | 
			
		||||
import static org.mockito.Mockito.times;
 | 
			
		||||
import static org.mockito.Mockito.verify;
 | 
			
		||||
 | 
			
		||||
public class TbJsonPathNodeTest {
 | 
			
		||||
    final ObjectMapper mapper = new ObjectMapper();
 | 
			
		||||
 | 
			
		||||
    DeviceId deviceId;
 | 
			
		||||
    TbJsonPathNode node;
 | 
			
		||||
    TbJsonPathNodeConfiguration config;
 | 
			
		||||
    TbNodeConfiguration nodeConfiguration;
 | 
			
		||||
    TbContext ctx;
 | 
			
		||||
    TbMsgCallback callback;
 | 
			
		||||
 | 
			
		||||
    @BeforeEach
 | 
			
		||||
    void setUp() throws TbNodeException {
 | 
			
		||||
        deviceId = new DeviceId(UUID.randomUUID());
 | 
			
		||||
        callback = mock(TbMsgCallback.class);
 | 
			
		||||
        ctx = mock(TbContext.class);
 | 
			
		||||
        config = new TbJsonPathNodeConfiguration();
 | 
			
		||||
        config.setJsonPath("$.Attribute_2");
 | 
			
		||||
        nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
 | 
			
		||||
        node = spy(new TbJsonPathNode());
 | 
			
		||||
        node.init(ctx, nodeConfiguration);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @AfterEach
 | 
			
		||||
    void tearDown() {
 | 
			
		||||
        node.destroy();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void givenDefaultConfig_whenInit_thenFail() {
 | 
			
		||||
        config.setJsonPath("");
 | 
			
		||||
        nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
 | 
			
		||||
        assertThatThrownBy(() -> node.init(ctx, nodeConfiguration)).isInstanceOf(IllegalArgumentException.class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void givenDefaultConfig_whenVerify_thenOK() {
 | 
			
		||||
        TbJsonPathNodeConfiguration defaultConfig = new TbJsonPathNodeConfiguration().defaultConfiguration();
 | 
			
		||||
        assertThat(defaultConfig.getJsonPath()).isEqualTo(TbJsonPathNodeConfiguration.DEFAULT_JSON_PATH);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void givenPrimitiveMsg_whenOnMsg_thenVerifyOutput() throws Exception {
 | 
			
		||||
        String data = "{\"Attribute_1\":22.5,\"Attribute_2\":100}";
 | 
			
		||||
        VerifyOutputMsg(data, 1, 100);
 | 
			
		||||
 | 
			
		||||
        data = "{\"Attribute_1\":22.5,\"Attribute_2\":\"StringValue\"}";
 | 
			
		||||
        VerifyOutputMsg(data, 2, "StringValue");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void givenJsonArray_whenOnMsg_thenVerifyOutput() throws Exception {
 | 
			
		||||
        String data = "{\"Attribute_1\":22.5,\"Attribute_2\":[{\"Attribute_3\":22.5,\"Attribute_4\":10.3}, {\"Attribute_5\":22.5,\"Attribute_6\":10.3}]}";
 | 
			
		||||
        VerifyOutputMsg(data, 1, JacksonUtil.toJsonNode(data).get("Attribute_2"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void givenJsonNode_whenOnMsg_thenVerifyOutput() throws Exception {
 | 
			
		||||
        String data = "{\"Attribute_1\":22.5,\"Attribute_2\":{\"Attribute_3\":22.5,\"Attribute_4\":10.3}}";
 | 
			
		||||
        VerifyOutputMsg(data, 1, JacksonUtil.toJsonNode(data).get("Attribute_2"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void givenJsonArrayWithFilter_whenOnMsg_thenVerifyOutput() throws Exception {
 | 
			
		||||
        config.setJsonPath("$.Attribute_2[?(@.voltage > 200)]");
 | 
			
		||||
        nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
 | 
			
		||||
        node.init(ctx, nodeConfiguration);
 | 
			
		||||
 | 
			
		||||
        String data = "{\"Attribute_1\":22.5,\"Attribute_2\":[{\"voltage\":220}, {\"voltage\":250}, {\"voltage\":110}]}";
 | 
			
		||||
        VerifyOutputMsg(data, 1, JacksonUtil.toJsonNode("[{\"voltage\":220}, {\"voltage\":250}]"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void givenNoArrayMsg_whenOnMsg_thenTellFailure() throws Exception {
 | 
			
		||||
        String data = "{\"Attribute_1\":22.5,\"Attribute_5\":10.3}";
 | 
			
		||||
        JsonNode dataNode = JacksonUtil.toJsonNode(data);
 | 
			
		||||
        TbMsg msg = getTbMsg(deviceId, dataNode.toString());
 | 
			
		||||
        node.onMsg(ctx, msg);
 | 
			
		||||
 | 
			
		||||
        ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
 | 
			
		||||
        ArgumentCaptor<Exception> exceptionCaptor = ArgumentCaptor.forClass(Exception.class);
 | 
			
		||||
        verify(ctx, never()).tellSuccess(any());
 | 
			
		||||
        verify(ctx, times(1)).tellFailure(newMsgCaptor.capture(), exceptionCaptor.capture());
 | 
			
		||||
 | 
			
		||||
        assertThat(newMsgCaptor.getValue()).isSameAs(msg);
 | 
			
		||||
        assertThat(exceptionCaptor.getValue()).isInstanceOf(RuntimeException.class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void givenNoResultsForPath_whenOnMsg_thenTellFailure() throws Exception {
 | 
			
		||||
        String data = "{\"Attribute_1\":22.5,\"Attribute_5\":10.3}";
 | 
			
		||||
        JsonNode dataNode = JacksonUtil.toJsonNode(data);
 | 
			
		||||
        TbMsg msg = getTbMsg(deviceId, dataNode.toString());
 | 
			
		||||
        node.onMsg(ctx, msg);
 | 
			
		||||
 | 
			
		||||
        ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
 | 
			
		||||
        ArgumentCaptor<Exception> exceptionCaptor = ArgumentCaptor.forClass(Exception.class);
 | 
			
		||||
        verify(ctx, never()).tellSuccess(any());
 | 
			
		||||
        verify(ctx, times(1)).tellFailure(newMsgCaptor.capture(), exceptionCaptor.capture());
 | 
			
		||||
 | 
			
		||||
        assertThat(newMsgCaptor.getValue()).isSameAs(msg);
 | 
			
		||||
        assertThat(exceptionCaptor.getValue()).isInstanceOf(PathNotFoundException.class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void VerifyOutputMsg(String data, int countTellSuccess, Object value) throws Exception {
 | 
			
		||||
        JsonNode dataNode = JacksonUtil.toJsonNode(data);
 | 
			
		||||
        node.onMsg(ctx, getTbMsg(deviceId, dataNode.toString()));
 | 
			
		||||
 | 
			
		||||
        ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
 | 
			
		||||
        verify(ctx, times(countTellSuccess)).tellSuccess(newMsgCaptor.capture());
 | 
			
		||||
        verify(ctx, never()).tellFailure(any(), any());
 | 
			
		||||
 | 
			
		||||
        assertThat(newMsgCaptor.getValue().getData()).isEqualTo(JacksonUtil.toString(value));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private TbMsg getTbMsg(EntityId entityId, String data) {
 | 
			
		||||
        Map<String, String> mdMap = Map.of("country", "US",
 | 
			
		||||
                "city", "NY"
 | 
			
		||||
        );
 | 
			
		||||
        return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, new TbMsgMetaData(mdMap), data, callback);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user