diff --git a/pom.xml b/pom.xml
index 895127e0c3..9fb7ba43e4 100755
--- a/pom.xml
+++ b/pom.xml
@@ -128,6 +128,7 @@
1.5.2
5.8.2
2.6.0
+ 5.13.1
1.3.0
1.2.7
@@ -1877,6 +1878,18 @@
${zeroturnaround.version}
test
+
+ org.mock-server
+ mockserver-netty
+ ${mock-server.version}
+ test
+
+
+ org.mock-server
+ mockserver-client-java
+ ${mock-server.version}
+ test
+
org.opensmpp
opensmpp-core
diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml
index 2606fc32d4..ebbfffc1b3 100644
--- a/rule-engine/rule-engine-components/pom.xml
+++ b/rule-engine/rule-engine-components/pom.xml
@@ -136,6 +136,15 @@
awaitility
test
+
+ org.mock-server
+ mockserver-netty
+
+
+ org.mock-server
+ mockserver-client-java
+
+
org.cassandraunit
cassandra-unit
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
index c2a6867f94..e035d415ed 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
@@ -40,6 +40,7 @@ import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
import org.springframework.web.client.AsyncRestTemplate;
import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.TbRelationTypes;
@@ -54,6 +55,7 @@ import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
+import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.Deque;
@@ -189,8 +191,9 @@ public class TbHttpClient {
entity = new HttpEntity<>(msg.getData(), headers);
}
+ URI uri = buildEncodedUri(endpointUrl);
ListenableFuture> future = httpClient.exchange(
- endpointUrl, method, entity, String.class);
+ uri, method, entity, String.class);
future.addCallback(new ListenableFutureCallback>() {
@Override
public void onFailure(Throwable throwable) {
@@ -214,6 +217,28 @@ public class TbHttpClient {
}
}
+ public URI buildEncodedUri(String endpointUrl) {
+ if (endpointUrl == null) {
+ throw new RuntimeException("Url string cannot be null!");
+ }
+ if (endpointUrl.isEmpty()) {
+ throw new RuntimeException("Url string cannot be empty!");
+ }
+
+ URI uri = UriComponentsBuilder.fromUriString(endpointUrl).build().encode().toUri();
+ if (uri.getScheme() == null || uri.getScheme().isEmpty()) {
+ throw new RuntimeException("Transport scheme(protocol) must be provided!");
+ }
+
+ boolean authorityNotValid = uri.getAuthority() == null || uri.getAuthority().isEmpty();
+ boolean hostNotValid = uri.getHost() == null || uri.getHost().isEmpty();
+ if (authorityNotValid || hostNotValid) {
+ throw new RuntimeException("Url string is invalid!");
+ }
+
+ return uri;
+ }
+
private TbMsg processResponse(TbContext ctx, TbMsg origMsg, ResponseEntity response) {
TbMsgMetaData metaData = origMsg.getMetaData();
metaData.putValue(STATUS, response.getStatusCode().name());
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java
index 9834ef631d..f5c6977dd5 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java
@@ -18,16 +18,38 @@ package org.thingsboard.rule.engine.rest;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
+import org.awaitility.Awaitility;
import org.junit.After;
+import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.mockserver.integration.ClientAndServer;
+import org.springframework.web.client.AsyncRestTemplate;
+import org.thingsboard.rule.engine.api.TbContext;
+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 java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.willCallRealMethod;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockserver.integration.ClientAndServer.startClientAndServer;
+import static org.mockserver.model.HttpRequest.request;
+import static org.mockserver.model.HttpResponse.response;
public class TbHttpClientTest {
@@ -58,4 +80,128 @@ public class TbHttpClientTest {
eventLoop = client.getSharedOrCreateEventLoopGroup(null);
assertThat(eventLoop, instanceOf(NioEventLoopGroup.class));
}
+
+ @Test
+ public void testBuildSimpleUri() {
+ Mockito.when(client.buildEncodedUri(any())).thenCallRealMethod();
+ String url = "http://localhost:8080/";
+ URI uri = client.buildEncodedUri(url);
+ Assert.assertEquals(url, uri.toString());
+ }
+
+ @Test
+ public void testBuildUriWithoutProtocol() {
+ Mockito.when(client.buildEncodedUri(any())).thenCallRealMethod();
+ String url = "localhost:8080/";
+ assertThatThrownBy(() -> client.buildEncodedUri(url));
+ }
+
+ @Test
+ public void testBuildInvalidUri() {
+ Mockito.when(client.buildEncodedUri(any())).thenCallRealMethod();
+ String url = "aaa";
+ assertThatThrownBy(() -> client.buildEncodedUri(url));
+ }
+
+ @Test
+ public void testBuildUriWithSpecialSymbols() {
+ Mockito.when(client.buildEncodedUri(any())).thenCallRealMethod();
+ String url = "http://192.168.1.1/data?d={\"a\": 12}";
+ String expected = "http://192.168.1.1/data?d=%7B%22a%22:%2012%7D";
+ URI uri = client.buildEncodedUri(url);
+ Assert.assertEquals(expected, uri.toString());
+ }
+
+ @Test
+ public void testProcessMessageWithJsonInUrlVariable() throws Exception {
+ String host = "localhost";
+ String path = "/api";
+ String paramKey = "data";
+ String paramVal = "[{\"test\":\"test\"}]";
+ String successResponseBody = "SUCCESS";
+
+ var server = setUpDummyServer(host, path, paramKey, paramVal, successResponseBody);
+
+ String endpointUrl = String.format(
+ "http://%s:%d%s?%s=%s",
+ host, server.getPort(), path, paramKey, paramVal
+ );
+ String method = "GET";
+
+
+ var config = new TbRestApiCallNodeConfiguration()
+ .defaultConfiguration();
+ config.setRequestMethod(method);
+ config.setRestEndpointUrlPattern(endpointUrl);
+ config.setUseSimpleClientHttpFactory(true);
+
+ var asyncRestTemplate = new AsyncRestTemplate();
+
+ var httpClient = new TbHttpClient(config, eventLoop);
+ httpClient.setHttpClient(asyncRestTemplate);
+
+ var msg = TbMsg.newMsg(
+ "Main", "GET", new DeviceId(EntityId.NULL_UUID),
+ TbMsgMetaData.EMPTY, "{}"
+ );
+ var successMsg = TbMsg.newMsg(
+ "SUCCESS", msg.getOriginator(),
+ msg.getMetaData(), msg.getData()
+ );
+
+ var ctx = mock(TbContext.class);
+ when(ctx.transformMsg(
+ eq(msg), eq(msg.getType()),
+ eq(msg.getOriginator()),
+ eq(msg.getMetaData()),
+ eq(msg.getData())
+ )).thenReturn(successMsg);
+
+ var capturedData = ArgumentCaptor.forClass(String.class);
+
+ when(ctx.transformMsg(
+ eq(msg), eq(msg.getType()),
+ eq(msg.getOriginator()),
+ any(),
+ capturedData.capture()
+ )).thenReturn(successMsg);
+
+ httpClient.processMessage(ctx, msg);
+
+ Awaitility.await()
+ .atMost(30, TimeUnit.SECONDS)
+ .until(() -> {
+ try {
+ verify(ctx, times(1)).tellSuccess(any());
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ });
+
+ verify(ctx, times(1)).tellSuccess(any());
+ verify(ctx, times(0)).tellFailure(any(), any());
+ Assert.assertEquals(successResponseBody, capturedData.getValue());
+ }
+
+ private ClientAndServer setUpDummyServer(String host, String path, String paramKey, String paramVal, String successResponseBody) {
+ var server = startClientAndServer(host, 1080);
+ createGetMethodExpectations(server, path, paramKey, paramVal, successResponseBody);
+ return server;
+ }
+
+ private void createGetMethodExpectations(ClientAndServer server, String path, String paramKey, String paramVal, String successResponseBody) {
+ server.when(
+ request()
+ .withMethod("GET")
+ .withPath(path)
+ .withQueryStringParameter(paramKey, paramVal)
+ ).respond(
+ response()
+ .withStatusCode(200)
+ .withBody(successResponseBody)
+ );
+ }
+
+
}
\ No newline at end of file