From 905f004da91ff4e6acb9b22ad6116a4e8d096359 Mon Sep 17 00:00:00 2001 From: James D Bloom <733179+jamesdbloom@users.noreply.github.com> Date: Wed, 16 Mar 2022 20:57:44 +0000 Subject: [PATCH] #1052 added support for numerous velocity tools for example for JSON and XML parsing to velocity response templates --- changelog.md | 1 + .../mock_server/response_templates.html | 166 ++++++++++++++---- mockserver-core/pom.xml | 5 + .../javascript/JavaScriptTemplateEngine.java | 4 +- .../mustache/MustacheTemplateEngine.java | 4 +- .../velocity/VelocityTemplateEngine.java | 96 +++++++--- .../mustache/MustacheTemplateEngineTest.java | 3 - .../velocity/VelocityTemplateEngineTest.java | 104 ++++++++++- pom.xml | 17 ++ 9 files changed, 328 insertions(+), 72 deletions(-) diff --git a/changelog.md b/changelog.md index 21bf481e8..7a3b20e4d 100644 --- a/changelog.md +++ b/changelog.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - added response template variables for date, uuid and random for javascript - added path parameters, remote address and client certificate chain to response template model - added support for EMCAScript 6 in JavaScript response templates for Java versions between 9 and 15 +- added support for numerous velocity tools for example for JSON and XML parsing to velocity response templates ### Changed - included Bouncy Castle now used by default to resolve issues with modules in Java 16+ and backwards compatibility for Java 8 diff --git a/jekyll-www.mock-server.com/mock_server/response_templates.html b/jekyll-www.mock-server.com/mock_server/response_templates.html index 9d36cce9a..e19328d65 100644 --- a/jekyll-www.mock-server.com/mock_server/response_templates.html +++ b/jekyll-www.mock-server.com/mock_server/response_templates.html @@ -156,19 +156,19 @@

Request Model Variables

- +
mustache:{{ request.pathParameters.<key>.<index> }}{{ request.pathParameters.<key&ft;.<index&ft; }}
- +
velocity:$!request.pathParameters[<key>][<index>]$!request.pathParameters[<key&ft;][<index&ft;]
- +
javascript:request.pathParameters[<key>][<index>]request.pathParameters[<key&ft;][<index&ft;]
@@ -176,19 +176,19 @@

Request Model Variables

- +
mustache:{{ request.queryStringParameters.<key>.<index> }}{{ request.queryStringParameters.<key&ft;.<index&ft; }}
- +
velocity:$!request.queryStringParameters[<key>][<index>]$!request.queryStringParameters[<key&ft;][<index&ft;]
- +
javascript:request.queryStringParameters[<key>][<index>]request.queryStringParameters[<key&ft;][<index&ft;]
@@ -196,19 +196,19 @@

Request Model Variables

- +
mustache:{{ request.headers.<key>.<index> }}{{ request.headers.<key&ft;.<index&ft; }}
- +
velocity:$!request.headers[<key>][<index>]$!request.headers[<key&ft;][<index&ft;]
- +
javascript:request.headers[<key>][<index>]request.headers[<key&ft;][<index&ft;]
@@ -216,19 +216,19 @@

Request Model Variables

- +
mustache:{{ request.cookies.<key> }}{{ request.cookies.<key&ft; }}
- +
velocity:$!request.cookies[<key>]$!request.cookies[<key&ft;]
- +
javascript:request.cookies[<key>]request.cookies[<key&ft;]
@@ -365,19 +365,19 @@

Request Multi-Value And Single Value Maps

- +
mustache:{{ request.headers.<key>.<index> }}
{{ request.queryStringParameters.<key>.<index> }}
{{ request.headers.<key>.<index> }}
{{ request.headers.<key&ft;.<index&ft; }}
{{ request.queryStringParameters.<key&ft;.<index&ft; }}
{{ request.headers.<key&ft;.<index&ft; }}
- +
velocity:$!request.pathParameters[<key>][<index>]
$!request.queryStringParameters[<key>][<index>]
$!request.headers[<key>][<index>]
$!request.pathParameters[<key&ft;][<index&ft;]
$!request.queryStringParameters[<key&ft;][<index&ft;]
$!request.headers[<key&ft;][<index&ft;]
- +
javascript:request.pathParameters[<key>][<index>]
request.queryStringParameters[<key>][<index>]
request.headers[<key>][<index>]
request.pathParameters[<key&ft;][<index&ft;]
request.queryStringParameters[<key&ft;][<index&ft;]
request.headers[<key&ft;][<index&ft;]
@@ -879,27 +879,27 @@

XPath

Given a request with the following xml body:

-
<?xml version="1.0" encoding="UTF-8" ?>
-<store>
-  <book>
-    <category>reference</category>
-    <author>Nigel Rees</author>
-    <title>Sayings of the Century</title>
-    <price>18.95</price>
-  </book>
-  <book>
-    <category>fiction</category>
-    <author>Herman Melville</author>
-    <title>Moby Dick</title>
-    <isbn>0-553-21311-3</isbn>
-    <price>8.99</price>
-  </book>
-  <bicycle>
-    <color>red</color>
-    <price>19.95</price>
-  </bicycle>
-  <expensive>10</expensive>
-</store>
+
<?xml version="1.0" encoding="UTF-8" ?&ft;
+<store&ft;
+  <book&ft;
+    <category&ft;reference</category&ft;
+    <author&ft;Nigel Rees</author&ft;
+    <title&ft;Sayings of the Century</title&ft;
+    <price&ft;18.95</price&ft;
+  </book&ft;
+  <book&ft;
+    <category&ft;fiction</category&ft;
+    <author&ft;Herman Melville</author&ft;
+    <title&ft;Moby Dick</title&ft;
+    <isbn&ft;0-553-21311-3</isbn&ft;
+    <price&ft;8.99</price&ft;
+  </book&ft;
+  <bicycle&ft;
+    <color&ft;red</color&ft;
+    <price&ft;19.95</price&ft;
+  </bicycle&ft;
+  <expensive&ft;10</expensive&ft;
+</store&ft;

The example produces:

@@ -1003,6 +1003,100 @@

Mathematical

$item #end +

For additional mathematical functionality it is also possible to use the MathTool or NumberTool in velocity response templates.

+ +
#set($power = $math.pow($number, 2))
+#set($max = $math.max($number, 10))
+ +

Json Bodies

+ +

The JsonTool can be used to help parse JSON bodies, as follows:

+ +
#set($jsonBody = $json.parse($!request.body))
+
+{
+    'statusCode': 200,
+    'body': "{'titles': [#foreach( $book in $jsonBody.store.book )'$book.title'#if( $foreach.hasNext ), #end#end], 'bikeColor': '$jsonBody.store.bicycle.color'}"
+}
+ +

Given the following request:

+ +
{
+  "path" : "/somePath",
+  "body" : {
+    "type" : "JSON",
+    "json" : {
+      "store" : {
+        "book" : [ {
+          "category" : "reference",
+          "author" : "Nigel Rees",
+          "title" : "Sayings of the Century",
+          "price" : 18.95
+        }, {
+          "category" : "fiction",
+          "author" : "Herman Melville",
+          "title" : "Moby Dick",
+          "isbn" : "0-553-21311-3",
+          "price" : 8.99
+        } ],
+        "bicycle" : {
+          "color" : "red",
+          "price" : 19.95
+        }
+      },
+      "expensive" : 10
+    }
+  }
+}
+ +

The example produces:

+ +
{
+  "statusCode" : 200,
+  "body" : "{'titles': ['Sayings of the Century', 'Moby Dick'], 'bikeColor': 'red'}"
+}
+ +

XML Bodies

+ +

The XmlTool can be used to help parse XML bodies, execute XPath and XML traversal.

+ +

The following example shows how to use XmlTool for XPath:

+ +
#set($xmlBody = $xml.parse($!request.body))
+
+{
+    'statusCode': 200,
+    'body': "{'key': '$xml.find('/element/key/text()')', 'value': '$xml.find('/element/value/text()')'}"
+}
+ +

Given the following request:

+ +
{
+  "path" : "/somePath",
+  "body" : "<element><key>some_key</key><value>some_value</value></element>"
+}
+ +

The example produces:

+ +
{
+  "statusCode" : 200,
+  "body" : "{'key': 'some_key', 'value': 'some_value'}"
+}
+ +

Velocity Tools

+ +

The following velocity tools are available for velocity response templates:

+ +  

JavaScript Response Templates

diff --git a/mockserver-core/pom.xml b/mockserver-core/pom.xml index 277768c2b..681b5a2c8 100644 --- a/mockserver-core/pom.xml +++ b/mockserver-core/pom.xml @@ -111,6 +111,11 @@ + + org.apache.velocity.tools + velocity-tools-generic + 3.1 + com.samskivert jmustache diff --git a/mockserver-core/src/main/java/org/mockserver/templates/engine/javascript/JavaScriptTemplateEngine.java b/mockserver-core/src/main/java/org/mockserver/templates/engine/javascript/JavaScriptTemplateEngine.java index 348f88c48..5bd45cf91 100644 --- a/mockserver-core/src/main/java/org/mockserver/templates/engine/javascript/JavaScriptTemplateEngine.java +++ b/mockserver-core/src/main/java/org/mockserver/templates/engine/javascript/JavaScriptTemplateEngine.java @@ -73,10 +73,10 @@ public T executeTemplate(String template, HttpRequest request, Class T executeTemplate(String template, HttpRequest request, Class T executeTemplate(String template, HttpRequest request, Class context.setAttribute(key, value, ScriptContext.ENGINE_SCOPE)); - engine.eval(template, context); + VelocityContext context = new VelocityContext(toolContext); + context.put("request", new HttpRequestTemplateObject(request)); + TemplateFunctions.BUILT_IN_FUNCTIONS.forEach(context::put); + velocityEngine.evaluate(context, writer, "VelocityResponseTemplate", template); JsonNode generatedObject = null; try { generatedObject = objectMapper.readTree(writer.toString()); } catch (Throwable throwable) { - if (MockServerLogger.isEnabled(Level.TRACE)) { + if (MockServerLogger.isEnabled(Level.INFO)) { mockServerLogger.logEvent( new LogEntry() - .setLogLevel(Level.TRACE) + .setLogLevel(Level.INFO) .setHttpRequest(request) .setMessageFormat("exception deserialising generated content:{}into json node for request:{}") .setArguments(writer.toString(), request) diff --git a/mockserver-core/src/test/java/org/mockserver/templates/engine/mustache/MustacheTemplateEngineTest.java b/mockserver-core/src/test/java/org/mockserver/templates/engine/mustache/MustacheTemplateEngineTest.java index 873af93c3..4a4ef055c 100644 --- a/mockserver-core/src/test/java/org/mockserver/templates/engine/mustache/MustacheTemplateEngineTest.java +++ b/mockserver-core/src/test/java/org/mockserver/templates/engine/mustache/MustacheTemplateEngineTest.java @@ -591,7 +591,6 @@ public void shouldHandleHttpRequestsWithMustacheResponseTemplateWithXPathWithStr @Test public void shouldHandleHttpRequestsWithMustacheResponseTemplateWithJsonPath() throws JsonProcessingException { // given - ConfigurationProperties.logLevel("TRACE"); String template = "{" + NEW_LINE + " 'statusCode': 200," + NEW_LINE + " 'body': \"{'titles': {{#jsonPath}}$.store.book{{/jsonPath}}[{{#jsonPathResult}}{{^-first}}, {{/-first}}'{{title}}'{{/jsonPathResult}}], 'bikeColor': '{{#jsonPath}}$.store.bicycle.color{{/jsonPath}}{{jsonPathResult}}'}\"" + NEW_LINE + @@ -652,7 +651,6 @@ public void shouldHandleHttpRequestsWithMustacheResponseTemplateWithJsonPath() t @Test public void shouldHandleHttpRequestsWithMustacheResponseTemplateWithJsonPathWithXmlBody() throws JsonProcessingException { // given - ConfigurationProperties.logLevel("TRACE"); String template = "{" + NEW_LINE + " 'statusCode': 200," + NEW_LINE + " 'body': \"{'key': '{{#jsonPath}}$.store.book[0].title{{/jsonPath}}', 'value': '{{#jsonPath}}$.store.bicycle.color{{/jsonPath}}'}\"" + NEW_LINE + @@ -712,7 +710,6 @@ public void shouldHandleHttpRequestsWithMustacheResponseTemplateWithJsonPathWith @Test public void shouldHandleHttpRequestsWithMustacheResponseTemplateWithJsonPathWithStringBody() throws JsonProcessingException { // given - ConfigurationProperties.logLevel("TRACE"); String template = "{" + NEW_LINE + " 'statusCode': 200," + NEW_LINE + " 'body': \"{'key': '{{#jsonPath}}$.store.book[0].title{{/jsonPath}}', 'value': '{{#jsonPath}}$.store.bicycle.color{{/jsonPath}}'}\"" + NEW_LINE + diff --git a/mockserver-core/src/test/java/org/mockserver/templates/engine/velocity/VelocityTemplateEngineTest.java b/mockserver-core/src/test/java/org/mockserver/templates/engine/velocity/VelocityTemplateEngineTest.java index 146779925..ead040e0e 100644 --- a/mockserver-core/src/test/java/org/mockserver/templates/engine/velocity/VelocityTemplateEngineTest.java +++ b/mockserver-core/src/test/java/org/mockserver/templates/engine/velocity/VelocityTemplateEngineTest.java @@ -41,6 +41,7 @@ import static org.mockserver.log.model.LogEntry.LogMessageType.TEMPLATE_GENERATED; import static org.mockserver.model.HttpRequest.request; import static org.mockserver.model.HttpResponse.response; +import static org.mockserver.model.JsonBody.json; import static org.slf4j.event.Level.INFO; /** @@ -250,7 +251,7 @@ private void shouldPopulateRandomValue(String function, Matcher matcher } @Test - public void shouldHandleHttpRequestsWithVelocityResponseTemplateWithLoopOverValuesUsingThis() throws JsonProcessingException { + public void shouldHandleHttpRequestsWithVelocityResponseTemplateWithLoopOverValues() throws JsonProcessingException { // given String template = "{" + NEW_LINE + " 'statusCode': 200," + NEW_LINE + @@ -336,6 +337,105 @@ public void shouldHandleHttpRequestsWithVelocityResponseTemplateWithIfElse() thr ); } + @Test + public void shouldHandleHttpRequestsWithVelocityResponseTemplateWithXPath() throws JsonProcessingException { + // given + String template = "#set($xmlBody = $xml.parse($!request.body))" + NEW_LINE + + "{" + NEW_LINE + + " 'statusCode': 200," + NEW_LINE + + " 'body': \"{'key': '$xml.find('/element/key/text()')', 'value': '$xml.find('/element/value/text()')'}\"" + NEW_LINE + + "}"; + HttpRequest request = request() + .withPath("/somePath") + .withBody("some_keysome_value"); + + // when + HttpResponse actualHttpResponse = new VelocityTemplateEngine(mockServerLogger).executeTemplate(template, request, HttpResponseDTO.class); + + // then + assertThat(actualHttpResponse, is( + response() + .withStatusCode(200) + .withBody("{'key': 'some_key', 'value': 'some_value'}") + )); + verify(mockServerLogger).logEvent( + new LogEntry() + .setType(TEMPLATE_GENERATED) + .setLogLevel(INFO) + .setHttpRequest(request) + .setMessageFormat("generated output:{}from template:{}for request:{}") + .setArguments(OBJECT_MAPPER.readTree("" + + "{" + NEW_LINE + + " 'statusCode': 200," + NEW_LINE + + " 'body': \"{'key': 'some_key', 'value': 'some_value'}\"" + NEW_LINE + + "}" + NEW_LINE), + template, + request + ) + ); + } + + @Test + public void shouldHandleHttpRequestsWithVelocityResponseTemplateWithJsonParsing() throws JsonProcessingException { + // given + String template = "#set($jsonBody = $json.parse($!request.body))" + NEW_LINE + + "{" + NEW_LINE + + " 'statusCode': 200," + NEW_LINE + + " 'body': \"{'titles': [#foreach( $book in $jsonBody.store.book )'$book.title'#if( $foreach.hasNext ), #end#end], 'bikeColor': '$jsonBody.store.bicycle.color'}\"" + NEW_LINE + + "}"; + HttpRequest request = request() + .withPath("/somePath") + .withBody(json("{" + NEW_LINE + + " \"store\": {" + NEW_LINE + + " \"book\": [" + NEW_LINE + + " {" + NEW_LINE + + " \"category\": \"reference\"," + NEW_LINE + + " \"author\": \"Nigel Rees\"," + NEW_LINE + + " \"title\": \"Sayings of the Century\"," + NEW_LINE + + " \"price\": 18.95" + NEW_LINE + + " }," + NEW_LINE + + " {" + NEW_LINE + + " \"category\": \"fiction\"," + NEW_LINE + + " \"author\": \"Herman Melville\"," + NEW_LINE + + " \"title\": \"Moby Dick\"," + NEW_LINE + + " \"isbn\": \"0-553-21311-3\"," + NEW_LINE + + " \"price\": 8.99" + NEW_LINE + + " }" + NEW_LINE + + " ]," + NEW_LINE + + " \"bicycle\": {" + NEW_LINE + + " \"color\": \"red\"," + NEW_LINE + + " \"price\": 19.95" + NEW_LINE + + " }" + NEW_LINE + + " }," + NEW_LINE + + " \"expensive\": 10" + NEW_LINE + + "}")); + + // when + HttpResponse actualHttpResponse = new VelocityTemplateEngine(mockServerLogger).executeTemplate(template, request, HttpResponseDTO.class); + + // then + assertThat(actualHttpResponse, is( + response() + .withStatusCode(200) + .withBody("{'titles': ['Sayings of the Century', 'Moby Dick'], 'bikeColor': 'red'}") + )); + verify(mockServerLogger).logEvent( + new LogEntry() + .setType(TEMPLATE_GENERATED) + .setLogLevel(INFO) + .setHttpRequest(request) + .setMessageFormat("generated output:{}from template:{}for request:{}") + .setArguments(OBJECT_MAPPER.readTree("" + + "{" + NEW_LINE + + " 'statusCode': 200," + NEW_LINE + + " 'body': \"{'titles': ['Sayings of the Century', 'Moby Dick'], 'bikeColor': 'red'}\"" + NEW_LINE + + "}" + NEW_LINE), + template, + request + ) + ); + } + @Test public void shouldHandleHttpRequestsWithVelocityForwardTemplateWithPathBodyParametersAndCookies() throws JsonProcessingException { // given @@ -448,7 +548,7 @@ public void shouldHandleInvalidVelocityTemplate() { // then assertThat(runtimeException.getMessage(), is("Exception:" + NEW_LINE + "" + NEW_LINE + - " org.apache.velocity.exception.ParseErrorException: Encountered \"{\" at [line 1, column 5]" + NEW_LINE + + " Encountered \"{\" at VelocityResponseTemplate[line 1, column 5]" + NEW_LINE + " Was expecting one of:" + NEW_LINE + " \"(\" ..." + NEW_LINE + " ..." + NEW_LINE + diff --git a/pom.xml b/pom.xml index 5fd79d439..24b1b492e 100644 --- a/pom.xml +++ b/pom.xml @@ -289,6 +289,11 @@ velocity-engine-core ${velocity.version} + + org.apache.velocity.tools + velocity-tools-generic + 3.1 + com.samskivert jmustache @@ -502,6 +507,18 @@ jackson-dataformat-yaml ${jackson.version} + + + commons-beanutils + commons-beanutils + 1.9.4 + + + + commons-logging + commons-logging + 1.2 +