diff --git a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/JSONTool.java b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/JSONTool.java index bc5e1dabe4be..976ae1d5601d 100644 --- a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/JSONTool.java +++ b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/JSONTool.java @@ -1,11 +1,15 @@ package com.dotcms.rendering.velocity.viewtools; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; +import com.dotcms.http.CircuitBreakerUrlBuilder; import com.dotcms.rest.api.v1.DotObjectMapperProvider; +import com.google.common.annotations.VisibleForTesting; import org.apache.velocity.tools.view.ImportSupport; import org.apache.velocity.tools.view.tools.ViewTool; @@ -18,6 +22,19 @@ public class JSONTool extends ImportSupport implements ViewTool { + private final Supplier circuitBreakerUrlSupplier; + + public JSONTool() { + super(); + this.circuitBreakerUrlSupplier = CircuitBreakerUrl::builder; + } + + @VisibleForTesting + JSONTool(final Supplier circuitBreakerUrlSupplier) { + super(); + this.circuitBreakerUrlSupplier = circuitBreakerUrlSupplier; + } + public void init(Object obj) { } @@ -102,8 +119,7 @@ public Object get(final String url, final int timeout) { public Object get(String url, int timeout, Map headers) { try { - String x = CircuitBreakerUrl - .builder() + String x = this.circuitBreakerUrlSupplier.get() .setHeaders(headers) .setUrl(url) .setTimeout(timeout) @@ -117,11 +133,18 @@ public Object get(String url, int timeout, Map headers) { return null; } + /** + * Will put data to the remote URL returning the JSON Object or JSON Array response from the server + * @param url The URL to put to + * @param timeout The timeout in milliseconds + * @param headers The headers to send in the HTTP request + * @param rawData The raw data to send in the request + * @return The JSON Object or JSON Array returned from the remote URL + */ public Object put(final String url, final int timeout, final Map headers, final String rawData) { try { - final String response = CircuitBreakerUrl - .builder() + final String response = this.circuitBreakerUrlSupplier.get() .setMethod(Method.PUT) .setHeaders(headers) .setUrl(url) @@ -137,20 +160,33 @@ public Object put(final String url, final int timeout, final Map return null; } + /** + * Will put data to the remote URL returning the JSON Object or JSON Array response from the server + * @param url The URL to put to + * @param headers The headers to send in the HTTP request + * @param rawData The raw data to send in the request + * @return The JSON Object or JSON Array returned from the remote URL + */ public Object put(final String url, final Map headers, final String rawData) { return put(url, Config.getIntProperty("URL_CONNECTION_TIMEOUT", 2000), headers, rawData); } - + /** + * Will put data to the remote URL returning the JSON Object or JSON Array response from the server + * @param url The URL to put to + * @param timeout The timeout in milliseconds + * @param headers The headers to send in the HTTP request + * @param params The parameters to send in the request + * @return The JSON Object or JSON Array returned from the remote URL + */ public Object put(final String url, final int timeout, final Map headers, - final Map params) { + final Map params) { try { - final String response = CircuitBreakerUrl - .builder() + final String response = this.circuitBreakerUrlSupplier.get() .setMethod(Method.PUT) .setHeaders(headers) .setUrl(url) - .setParams(params) + .setParams(convertObjToStringParameters(params)) .setTimeout(timeout) .build() .doString(); @@ -162,16 +198,64 @@ public Object put(final String url, final int timeout, final Map return null; } + /** + * Will put data to the remote URL returning the JSON Object or JSON Array response from the server + * @param url The URL to put to + * @param headers The headers to send in the HTTP request + * @param params The parameters to send in the request + * @return The JSON Object or JSON Array returned from the remote URL + */ + public Object put(final String url, final Map headers, + final Map params) { + return put(url, Config.getIntProperty("URL_CONNECTION_TIMEOUT", 2000), headers, params); + } + + /** + * Will put data to the remote URL returning the JSON Object or JSON Array response from the server + * @param url The URL to put to + * @param timeout The timeout in milliseconds + * @param headers The headers to send in the HTTP request + * @param params The parameters to send in the request + * @param useParamsAsJsonPayload If true, the params will be sent as a JSON payload, + * otherwise they will be sent as request parameters + * @return The JSON Object or JSON Array returned from the remote URL + */ + public Object put(final String url, final int timeout, final Map headers, + final Map params, final boolean useParamsAsJsonPayload) { + if (useParamsAsJsonPayload) { + return put(url, timeout, headers, generate(params).toString()); + } else { + return put(url, timeout, headers, params); + } + } + + /** + * Will put data to the remote URL returning the JSON Object or JSON Array response from the server + * @param url The URL to put to + * @param headers The headers to send in the HTTP request + * @param params The parameters to send in the request + * @param useParamsAsJsonPayload If true, the params will be sent as a JSON payload, + * otherwise they will be sent as request parameters + * @return The JSON Object or JSON Array returned from the remote URL + */ public Object put(final String url, final Map headers, - final Map params) { - return post(url, Config.getIntProperty("URL_CONNECTION_TIMEOUT", 2000), headers, params); + final Map params, final boolean useParamsAsJsonPayload) { + return put(url, Config.getIntProperty("URL_CONNECTION_TIMEOUT", 2000), + headers, params, useParamsAsJsonPayload); } + /** + * Will post data to the remote URL returning the JSON Object or JSON Array response from the server + * @param url The URL to post to + * @param timeout The timeout in milliseconds + * @param headers The headers to send in the HTTP request + * @param rawData The raw data to send in the request + * @return The JSON Object or JSON Array returned from the remote URL + */ public Object post(final String url, final int timeout, final Map headers, final String rawData) { try { - final String response = CircuitBreakerUrl - .builder() + final String response = this.circuitBreakerUrlSupplier.get() .setMethod(Method.POST) .setHeaders(headers) .setUrl(url) @@ -187,19 +271,33 @@ public Object post(final String url, final int timeout, final Map headers, final String rawData) { return post(url, Config.getIntProperty("URL_CONNECTION_TIMEOUT", 5000), headers, rawData); } + /** + * Will post data to the remote URL returning the JSON Object or JSON Array response from the server + * @param url The URL to post to + * @param timeout The timeout in milliseconds + * @param headers The headers to send in the HTTP request + * @param params The parameters to send in the request + * @return The JSON Object or JSON Array returned from the remote URL + */ public Object post(final String url, final int timeout, final Map headers, - final Map params) { + final Map params) { try { - final String response = CircuitBreakerUrl - .builder() + final String response = this.circuitBreakerUrlSupplier.get() .setMethod(Method.POST) .setHeaders(headers) .setUrl(url) - .setParams(params) + .setParams(convertObjToStringParameters(params)) .setTimeout(timeout) .build() .doString(); @@ -211,11 +309,75 @@ public Object post(final String url, final int timeout, final Map headers, - final Map params) { + final Map params) { return post(url, Config.getIntProperty("URL_CONNECTION_TIMEOUT", 2000), headers, params); } + /** + * Will post data to the remote URL returning the JSON Object or JSON Array response from the server + * @param url The URL to post to + * @param timeout The timeout in milliseconds + * @param headers The headers to send in the HTTP request + * @param params The parameters to send in the request + * @param useParamsAsJsonPayload If true, the params will be sent as a JSON payload, + * otherwise they will be sent as request parameters + * @return The JSON Object or JSON Array returned from the remote URL + */ + public Object post(final String url, final int timeout, final Map headers, + final Map params, final boolean useParamsAsJsonPayload) { + if (useParamsAsJsonPayload) { + return post(url, timeout, headers, generate(params).toString()); + } else { + return post(url, timeout, headers, params); + } + } + + /** + * Will post data to the remote URL returning the JSON Object or JSON Array response from the server + * @param url The URL to post to + * @param headers The headers to send in the HTTP request + * @param params The parameters to send in the request + * @param useParamsAsJsonPayload If true, the params will be sent as a JSON payload, + * otherwise they will be sent as request parameters + * @return The JSON Object or JSON Array returned from the remote URL + */ + public Object post(final String url, final Map headers, + final Map params, final boolean useParamsAsJsonPayload) { + return post(url, Config.getIntProperty("URL_CONNECTION_TIMEOUT", 2000), + headers, params, useParamsAsJsonPayload); + } + + /** + * Converts the given map of objects to a map of strings to be used as parameters in a request + * @param objParams The map of objects to convert + * @return The map of strings to be used as parameters in a request + */ + private Map convertObjToStringParameters(Map objParams) { + Map params = new HashMap<>(); + for (Map.Entry entry : objParams.entrySet()) { + String value; + if (entry.getValue() == null) { + value = ""; + } else if (entry.getValue() instanceof Map || entry.getValue() instanceof List) { + value = generate(entry.getValue()).toString(); + } else if (entry.getValue() instanceof String) { + value = (String) entry.getValue(); + } else { + value = entry.getValue().toString(); + } + params.put(entry.getKey(), value); + } + return params; + } + /** * Returns a JSONObject from a passed in Map * diff --git a/dotCMS/src/test/java/com/dotcms/rendering/velocity/viewtools/JSONToolTest.java b/dotCMS/src/test/java/com/dotcms/rendering/velocity/viewtools/JSONToolTest.java new file mode 100644 index 000000000000..520c0288771e --- /dev/null +++ b/dotCMS/src/test/java/com/dotcms/rendering/velocity/viewtools/JSONToolTest.java @@ -0,0 +1,223 @@ +package com.dotcms.rendering.velocity.viewtools; + +import com.dotcms.http.CircuitBreakerUrl; +import com.dotcms.http.CircuitBreakerUrlBuilder; +import com.dotmarketing.util.json.JSONObject; +import org.junit.Test; +import org.mockito.ArgumentMatcher; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Verifies that the {@link JSONTool} ViewTool works as expected. + */ +public class JSONToolTest { + + /** + * Tests that the {@link JSONTool#post(String, Map, Object, boolean)} method works as expected + * when the parameters map {@link Map} is sent as a json payload. + * @throws IOException error executing the request + */ + @Test + public void test_PostJsonPayloadFromMap() throws IOException { + + final Map testPayloadMap = getTestPayloadMap(); + testPayloadMap.put("workflowAction", null); + + final CircuitBreakerUrlBuilder circuitBreakerUrlBuilder = mockCircuitBreakerUrlBuilder(); + when(circuitBreakerUrlBuilder.setRawData(anyString())) + .thenThrow(new RuntimeException("Payload map does not match")); + when(circuitBreakerUrlBuilder.setRawData(argThat( + new PayloadMapMatcher(testPayloadMap)))).thenReturn(circuitBreakerUrlBuilder); + + final JSONTool jsonTool = new JSONTool(() -> circuitBreakerUrlBuilder); + final Object result = jsonTool.post("http://localhost:8080/api/v1/content/publish/1", + Map.of("Content-Type", "application/json"), + testPayloadMap, + true); + + assertNotNull(result); + + } + + /** + * Tests that the {@link JSONTool#post(String, Map, Object, boolean)} method works as expected + * when the parameters map {@link Map} is sent as the request parameters. + * @throws IOException error executing the request + */ + @Test + public void test_PostParamMap() throws IOException { + + final Map testParamMap = Map.of( + "param-1", "value-1", + "param-2", 2, + "param-3", true + ); + + final CircuitBreakerUrlBuilder circuitBreakerUrlBuilder = mockCircuitBreakerUrlBuilder(); + when(circuitBreakerUrlBuilder.setParams(anyMap())) + .thenThrow(new RuntimeException("Param map does not match")); + + final Map expectedParamMap = Map.of( + "param-1", "value-1", + "param-2", "2", + "param-3", "true" + ); + when(circuitBreakerUrlBuilder.setParams(eq(expectedParamMap))).thenReturn(circuitBreakerUrlBuilder); + + final JSONTool jsonTool = new JSONTool(() -> circuitBreakerUrlBuilder); + final Object result = jsonTool.post("http://localhost:8080/api/v1/content/publish/1", + Map.of("Accept", "application/json"), + testParamMap, + false); + + assertNotNull(result); + + } + + /** + * Tests that the {@link JSONTool#put(String, Map, Object, boolean)} method works as expected + * when the parameters map {@link Map} is sent as a json payload. + * @throws IOException error executing the request + */ + @Test + public void test_PutJsonPayloadFromMap() throws IOException { + + final Map testPayloadMap = getTestPayloadMap(); + + final CircuitBreakerUrlBuilder circuitBreakerUrlBuilder = mockCircuitBreakerUrlBuilder(); + when(circuitBreakerUrlBuilder.setRawData(anyString())) + .thenThrow(new RuntimeException("Payload map does not match")); + when(circuitBreakerUrlBuilder.setRawData(argThat( + new PayloadMapMatcher(testPayloadMap)))).thenReturn(circuitBreakerUrlBuilder); + + final JSONTool jsonTool = new JSONTool(() -> circuitBreakerUrlBuilder); + final Object result = jsonTool.put("http://localhost:8080/api/v1/content/publish/1", + Map.of("Content-Type", "application/json"), + testPayloadMap, + true); + + assertNotNull(result); + + } + + /** + * Tests that the {@link JSONTool#put(String, Map, Object, boolean)} method works as expected + * when the parameters map {@link Map} is sent as the request parameters. + * @throws IOException error executing the request + */ + @Test + public void test_PutParamMap() throws IOException { + + final Map testParamMap = Map.of( + "param-1", "value-1", + "param-2", 3, + "param-3", true + ); + + final CircuitBreakerUrlBuilder circuitBreakerUrlBuilder = mockCircuitBreakerUrlBuilder(); + when(circuitBreakerUrlBuilder.setParams(anyMap())) + .thenThrow(new RuntimeException("Param map does not match")); + + final Map expectedParamMap = Map.of( + "param-1", "value-1", + "param-2", "3", + "param-3", "true" + ); + when(circuitBreakerUrlBuilder.setParams(eq(expectedParamMap))).thenReturn(circuitBreakerUrlBuilder); + + final JSONTool jsonTool = new JSONTool(() -> circuitBreakerUrlBuilder); + final Object result = jsonTool.put("http://localhost:8080/api/v1/content/publish/1", + Map.of("Accept", "application/json"), + testParamMap, + false); + + assertNotNull(result); + + } + + private static Map getTestPayloadMap() { + final Map payloadMap = Map.of("languageId", 1, + "hostId", "host-1", + "system", true, + "nested-obj", Map.of( + "first-nested-obj-key", "first-nested-obj-value", + "second-nested-obj-key", 2.25), + "nested-list", List.of( + "nested-list-value-1", + "nested-list-value-2", + "nested-list-value-3"), + "workflowAction", "action-1"); + return new HashMap<>(payloadMap); + } + + private static CircuitBreakerUrlBuilder mockCircuitBreakerUrlBuilder() throws IOException { + final CircuitBreakerUrlBuilder circuitBreakerUrlBuilder = mock(CircuitBreakerUrlBuilder.class); + final CircuitBreakerUrl circuitBreakerUrl = mock(CircuitBreakerUrl.class); + + when(circuitBreakerUrlBuilder.setMethod(any())).thenReturn(circuitBreakerUrlBuilder); + when(circuitBreakerUrlBuilder.setHeaders(anyMap())).thenReturn(circuitBreakerUrlBuilder); + when(circuitBreakerUrlBuilder.setUrl(anyString())).thenReturn(circuitBreakerUrlBuilder); + when(circuitBreakerUrlBuilder.setTimeout(anyLong())).thenReturn(circuitBreakerUrlBuilder); + when(circuitBreakerUrlBuilder.build()).thenReturn(circuitBreakerUrl); + when(circuitBreakerUrl.doString()).thenReturn("{\"success\": true}"); + return circuitBreakerUrlBuilder; + } + + /** + * Verifies that a given {@link Map} matches a given json string. + */ + public static class PayloadMapMatcher implements ArgumentMatcher { + private final Map expectedMap; + + public PayloadMapMatcher(Map expectedMap) { + this.expectedMap = expectedMap; + } + + @Override + public boolean matches(String mapAsString) { + final Map actualMap = new JSONObject(mapAsString).getAsMap(); + if (actualMap.size() != expectedMap.size()) { + return false; + } + for (Map.Entry entry : expectedMap.entrySet()) { + if (!actualMap.containsKey(entry.getKey())) { + return false; + } + Object actualValue = actualMap.get(entry.getKey()); + if (JSONObject.NULL.equals(actualValue)) { + actualValue = null; + } + final Object expectedValue = entry.getValue(); + if (actualValue == null && expectedValue == null) { + continue; + } + if (actualValue == null || expectedValue == null) { + return false; + } else if (actualValue instanceof Number && !(expectedValue instanceof Number)) { + return false; + } else if (actualValue instanceof Map && !(expectedValue instanceof Map)) { + return false; + } else if (actualValue instanceof Collection && !(expectedValue instanceof Collection)) { + return false; + } else if (actualValue instanceof Boolean && !(expectedValue instanceof Boolean)) { + return false; + } else if (actualValue instanceof String && !(expectedValue instanceof String)) { + return false; + } + } + return true; + } + } + +}