You can use several types of extensions to add dynamic content to response headers and response body.
For example, if you create a response with body { "Number": "{{randomInteger}}", "Date": "{{httpNow}}" } it will be rendered as { "Number": "165007562", "Date": "Sat, 02 Apr 2017 18:41:35 GMT" }
Below you will find all available extensions.
For a basic introduction to dynamic responses with examples have a look at Dynamic responses tutorial.
Save your data in trafficparrot-x.y.z/data/data.csv in CSV format.
To load the data in the response use {{csvDataFile 'row,col'}} in the response definition.
For example, {{csvDataFile '2,4'}} will read file trafficparrot-x.y.z/data/data.csv then load the 2nd row in the file and then lookup the 4th column value.
For more advanced CSV data loading, you can put your CSV file with any name in the trafficparrot-x.y.z/data directory.
To select data from a CSV use {{select 'a from x.csv where b equals' value}} in the response definition.
For example, {{select 'title from jobs.csv where name equals' 'example' }} will read the file trafficparrot-x.y.z/data/jobs.csv then load the value of the column title where the value of the column name is equal to the value example.
The value parameter passed to the {{select}} can be a dynamic value that is taken from the request e.g. xPath or jsonPath. For example:
{{select 'Age from UserData.csv where Username equals' (xPath request.body '//User/Username/text()')}}
You can enable the "select" helper CSV file indexing and caching for performance improvement by setting the property:
trafficparrot.virtualservice.handlebars.select.indexAndCacheCsvFiles=true
After loading a value from a CSV file, you can include it in an arithmetic operation to compute a value. For example:
{ {{#with (jsonPath request.body '$.id') as |id|}} "id": {{ id }}, "total": {{ math (select 'quantity from sales.csv where id equals' id) '*' (select 'price from sales.csv where id equals' id) }} {{/with}} }
To generate a random value in the response use one of the following:
The following HTTP response regions can be templated:
The following JMS response regions can be templated:
The following Native IBM® MQ response regions can be templated:
The following file response regions can be templated:
The following gRPC response regions can be templated:
To use HTTP request attributes in the response:
To use HTTP request/response attributes in webhooks:
To use JMS request message attributes in the JMS response message:
To use Native IBM® MQ request message attributes in the IBM® MQ response message:
To use request file attributes in the response file:
To use gRPC request attributes in the response:
All of the default functionality provided by the jknack Java Handlebars implementation is available. You can find more information on the Handlebars.java blog or Handlebars.js guide.
We have support for:You can place .hbs files in the installation root directory and reference them in your primary response template.
This can be useful to either reuse the same template many times, or to organize response template in separate files, with proper .hbs syntax highlighting in your editor or IDE.
The syntax
{{>responses/example-response}}
will load the content from the plain text template file responses/example-response.hbs.
The syntax
{{>(stringFormat 'responses/example-response-%s' 'some-value' )}}
allows you to construct the name of the template file dynamically using a string format for the name of the template file.
You can also pass in parameters to templates
{{>responses/example-response parameter=1234 }}
or reference parameters that exist already in the surrounding context.
More advanced usage is possible with template inheritance which you can use to parameterize blocks in a template:
The following WireMock Handlebars helpers are available for use.
The following WireMock Handlebars helpers have alternatives in Traffic Parrot.
All of jknack Handlebars string helpers are available for use in both HTTP and JMS response headers and body.
The math helper can be used to perform arithmetic operations:
The {{ modifyResponse }} helper can be used to modify the response HTTP status code.
Examples:{{ modifyResponse 'statusCode' 404 }}
{{#contains request.body 'A'}} {{ modifyResponse 'statusCode' 400 }} {{else contains request.body 'B'}} {{ modifyResponse 'statusCode' 500 }} {{else}} {{ modifyResponse 'statusCode' 200 }} {{/contains}}
The {{ cast }} helper can be used to convert between data types.
Examples:The {{ dateOffset }} helper can be used to calculate an offset from a given date. This can be combined with extracting data from the request to dynamically offset a date in the response based on a date in the request.
Examples:The {{ dataSource }} helper can be used to query or update an existing data source using a SQL style syntax. This can be combined with extracting data from the request to dynamically select a response field or update state for later use.
Currently supported data source features:Source | CREATE | SELECT | INSERT | UPDATE | DELETE | Caching | Multiple Results | Multiple Conditions | Default Values |
---|---|---|---|---|---|---|---|---|---|
CSV | |||||||||
XLS | |||||||||
JDBC |
Please contact us if you require any data source operation compatibility that is not currently available.
Examples:{{ dataSource '.csv' 'SELECT isoCountryCode FROM isoCountryCodes.csv WHERE internalCode = :1' (jsonPath request.body '$.internalCode') single=true default='GBR' }}
[ {{#each (dataSource 'example.db' 'SELECT * FROM table') }} { "A": {{ A }}, "B": {{ B }} }{{#unless @last}},{{/unless}} {{/each}} ]
{{ dataSource 'example.db' 'INSERT INTO PERSON(ID, NAME) VALUES(:id, :name)' id=(jsonPath request '$.id') name=(jsonPath request '$.name') }}
{{ dataSource 'example.db' 'INSERT INTO PERSON(ID, NAME) VALUES(:ids, :names)' ids=(jsonPath request '$.data[*].id') names=(jsonPath request '$.data[*].name') }}
{{#with (dataSource 'example.xlsx' 'SELECT * FROM ExampleSheet' singleRow=true ) }} { "A": {{ A }}, "B": {{ B }} } {{/with}}
{{dataSource 'example.xlsx' 'SELECT name FROM ExampleSheet WHERE id = :1' (jsonPath request.body '$.id') single=true }}
{{dataSource 'example.xlsx' 'SELECT name FROM ExampleSheet WHERE id = :id' id=(jsonPath request.body '$.id') single=true }}
Further details on the specifics of each data source can be found below.
[ { "connectionId": "example.db", "type": "JDBC_CONNECTION", "driverClass": "org.h2.Driver", "jdbcUrl": "jdbc:h2:mem:example", "properties": { "user": "sa", "password": "sa" } } ]
The {{ objectStore }} helper can be used to query or update a JSON object. This can be combined with extracting data from the request to manage simple persistent object state across requests.
Examples:{{ objectStore 'person.json' operation='create' id=(jsonPath request.body '$.id') object=request.body }}
{{#trim}} {{#with (randomValue type='UUID') }} {{ objectStore 'person.json' operation='create' id=. object=request.body }} {{ objectStore 'person.json' operation='put' id=. path='$' key='id' object=. }} {{ objectStore 'person.json' operation='get' id=. path='$' }} {{/with}} {{/trim}}
{{ objectStore 'person.json' operation='get' id=(jsonPath request.body '$.id') path='$' }}
{{ objectStore 'person.json' operation='get' id=(jsonPath request.body '$.id') path='$.field' }}
{{ objectStore 'person.json' operation='set' id=(jsonPath request.body '$.id') path='$.field' object=(jsonPath request.body '$.field') }}
{{ objectStore 'person.json' operation='delete' id=(jsonPath request.body '$.id') path='$.field' }}
{{ objectStore 'person.json' operation='delete' id=(jsonPath request.body '$.id') path='$' }}
{{ objectStore 'person.json' operation='put' id=(jsonPath request.body '$.id') path='$' key='field' object=(jsonPath request.body '$.field') }}
{{ objectStore 'person.json' operation='put' id=(jsonPath request.body '$.id') path='$.child' key='field' object=(jsonPath request.body '$.child.field') }}
{{ objectStore 'person.json' operation='add' id=(jsonPath request.body '$.id') path='$.names' object=(jsonPath request.body '$.name') }}
The following additional helpers are available in both HTTP, JMS and File response templates.
You can use it to extract a value from a field using an XPath.
Syntax:{{xPath sourceAttribute 'TheXPathValue'}}For example, if you use the following handlebar in a HTTP response body:
Request foobar value was: {{xPath request.body '/foo/bar/text()'}}and then send a HTTP request:
<foo><bar>Long live mocking!</bar></foo>you will see a HTTP response:
Request foobar value was: Long live mocking!
You can use it to extract a list of nodes from a field using an XPath. Can be combined with loop iterators to transform a request body.
Syntax:{{xPathList sourceAttribute 'TheXPathValue'}}For example, if you use the following handlebar in a HTTP response body:
<items> {{#each (xPathList request.body '/request/items') }} <item id="{{ xPath this '/item/@id' }}" status="OK">{{ xPath this '/item/text()' }}</item> {{/each}} </items>and send a HTTP request:
<request> <items> <item id="one" ignored="any">body1</item> <item id="two" ignored="any">body2</item> </items> </request>you will see a HTTP response:
<items> <item id="one" status="OK">body1</item> <item id="two" status="OK">body2</item> </items>
You can use it to extract a value from a field using an JsonPath.
Syntax:{{jsonPath sourceAttribute 'TheJsonPathValue'}}For example, if you use the following handlebar in a HTTP response body:
Request foobar value was: {{jsonPath request.body '$.foo.bar'}}and then send a HTTP request:
{"foo":{"bar":"Long live mocking!"}}you will see a HTTP response:
Request foobar value was: Long live mocking!
You can use it to extract a list of values from a field using an JsonPath. Can be combined with loop iterators to transform a request body.
Syntax:{{jsonPathList sourceAttribute 'TheJsonPathValue'}}For example, if you use the following handlebar in a HTTP response body:
[ {{#each (jsonPathList request.body '$.items') }} { id: {{ jsonPath this '$.id' }}", status: "OK" }{{#unless @last}},{{/unless}} {{/each}} ]and send a HTTP request:
{ items: [ { id: 1234, ignored: "any" }, { id: 1235, ignored: "any" } ] }you will see a HTTP response:
[ { id: 1234, status: "OK" }, { id: 1235, status: "OK" } ]
You can use it to extract a value from a field using a single regular expression capturing group.
Syntax:{{regex sourceAttribute 'TheRegex(.*)Value'}}For example, if you use the following handlebar in a HTTP response body:
Request value was: {{regex request.body '.+? ([0-9A-Za-z]+Camel-[0-9A-Za-z]+).*'}}and then send a HTTP request:
before 001116059c5549a0tOACamel-116059c554a20tOH afteryou will see a HTTP response:
Request value was: 001116059c5549a0tOACamel-116059c554a20tOH
You can call helpers inside other helpers. You just have to wrap them in parenthesis.
For example, if you use the following handlebar in a HTTP response body:Bar was equal to xxx: {{#equal (jsonPath request.body '$.foo.bar') 'xxx'}}true{{else}}false{{/equal}}'and then send a HTTP request:
{"foo":{"bar":"xxx"}}you will see a HTTP response:
Bar was equal to xxx: true
<requestHeaders> {{#each request.headers}} <name>{{@key}}</name> <value>{{this}}</value> {{/each}} </requestHeaders>
{{#with (jsonPath request.body '$.internalCode') as |internalCode|}} "internalCode": {{ internalCode }}, "isoCountryCode": "{{select 'isoCountryCode from isoCountryCodes.csv where internalCode equals' internalCode }}" {{/with}}
{{#if request.query.password}} Password was present in the request. {{else}} Please provide a password! {{/if}}
You can use it to compare two values.
Syntax:{{#equal attributeValue1 attributeValue2}} result if true {{else}} result if false {{/equal}}For example, if you use the following handlebar:
Is user Bob? {{#equal request.query.user 'Bob'}} Yes! {{else}} No! {{/equal}}
You can use it to see if a number is even or odd.
Syntax:{{#ifEven attributeValue}} result if true {{else}} result if false {{/equal}}For example, use the following handlebar to see if the number of order items in the request body (which is json) is odd or even:
{{#ifEven (jsonPath request.body '$.orderItem')}}number is even{{else}}number is odd{{/ifEven}}
In order to create your own HTTP extensions basic Java programming language knowledge is required. We use Java to write extensions because it is a simple to use and yet very powerful language. It is also the most popular programming language in the world. If this is not something you can do, we can create the extensions for you instead.
Traffic Parrot allows for Handlebars notation in the responses. They are typically used to add dynamic content to the responses. They can be used for both HTTP and JMS.
For example, all of these existing extensions are handlebar helpers:package com.trafficparrot.sdk.example.http.template; import com.github.jknack.handlebars.Handlebars; import com.github.jknack.handlebars.Options; import com.trafficparrot.sdk.handlebars.TrafficParrotHelper; import java.io.IOException; import java.util.Random; public class RandomInteger extends TrafficParrotHelper<Object> { @Override public Handlebars.SafeString doApply(Object context, Options options) throws IOException { return new Handlebars.SafeString(String.valueOf(new Random().nextInt())); } }
Traffic Parrot allows for creating custom HTTP response transformers. They allow for altering any part of the HTTP response in any way you like, and have access to the request data.
If you would like to add a new HTTP response transformer, have a look at the following example:package com.trafficparrot.sdk.example.http.responsetransformer; import com.github.tomakehurst.wiremock.common.FileSource; import com.github.tomakehurst.wiremock.extension.Parameters; import com.github.tomakehurst.wiremock.http.Request; import com.github.tomakehurst.wiremock.http.Response; import com.trafficparrot.sdk.http.HttpResponseTransformer; import java.util.Random; import static com.github.tomakehurst.wiremock.http.HttpHeaders.noHeaders; public class RandomServerFailure extends HttpResponseTransformer { @Override protected Response doTransform(Request request, Response response, FileSource fileSource, Parameters parameters) { if (new Random().nextBoolean()) { return Response.Builder .like(response) .but() .status(500) .headers(noHeaders()) .body("Server error!") .build(); } else { return response; } } @Override public boolean applyGlobally() { return true; } @Override public String getName() { return getClass().getSimpleName(); } }
trafficparrot.http.responsetransformers=com.trafficparrot.sdk.example.http.RandomServerFailure
@Override public boolean applyGlobally() { return true; }it will be applied globally for all mappings.
(optional) step if you do not want the transformer to apply globally
Note, if you do not set applyGlobally to true then you need to assign the transformer to specific mappings.
The "transformers" field is not exposed in the Traffic Parrot user interface yet, so you need to define the transformers JSON mapping file directly.
To do that, you can edit that mapping in a text editor and put "transformers": ["RandomServerFailure"] anywhere in the response section, for example:"response" : { "status" : 200, "body" : "Hello world!!!", "transformers": ["RandomServerFailure"] }
Traffic Parrot allows for creating custom JMS response transformers. They allow for altering any part of the JMS response message in any way you like, and have access to the request data.
If you would like to add a new JMS response transformer, have a look at the following example:package com.trafficparrot.sdk.example.jms; import com.github.tomakehurst.wiremock.extension.Parameters; import com.github.tomakehurst.wiremock.common.Json; import com.trafficparrot.sdk.jms.Destination; import com.trafficparrot.sdk.jms.JmsResponse; import com.trafficparrot.sdk.jms.JmsResponseBuilder; import com.trafficparrot.sdk.jms.TextJmsResponseTransformer; import javax.jms.JMSException; import javax.jms.TextMessage; public class ChooseResponseQueueBasedOnRequestMessageBody extends TextJmsResponseTransformer { @Override protected JmsResponse doTransform(Destination requestDestination, TextMessage requestMessage, JmsResponse response, Parameters parameters) throws JMSException { Req payment = Json.read(requestMessage.getText(), Req.class); String newDestinationName; if ("foo".equals(payment.requestField)) { newDestinationName = "response_queue_1"; } else if ("bar".equals(payment.requestField)) { newDestinationName = "response_queue_2"; } else { logger.error("Destination not configured for req.requestField '" + payment.requestField + "'"); throw new UnsupportedOperationException(payment.requestField); } logger.info("Redirecting message to " + newDestinationName); return new JmsResponseBuilder() .like(response) .withDestination(new Destination(newDestinationName, response.destination.type)) .build(); } } class Req { public String requestField; }
Traffic Parrot allows for creating custom Native IBM MQ response transformers. They allow for altering any part of the Native IBM MQ response message in any way you like, and have access to the request data.
If you would like to add a new Native IBM MQ response transformer, have a look at the following example:package com.trafficparrot.sdk.example.nativeibmmq; import com.github.tomakehurst.wiremock.extension.Parameters; import com.ibm.mq.MQMessage; import com.trafficparrot.sdk.ibmmq.IbmMqResponse; import com.trafficparrot.sdk.ibmmq.IbmMqResponseTransformer; import com.trafficparrot.sdk.ibmmq.SdkIbmMqUtils; import com.trafficparrot.sdk.jms.Destination; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import java.io.ByteArrayInputStream; import java.io.IOException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Optional; import java.util.Properties; import static com.trafficparrot.sdk.PropertiesHelper.readPropertiesFile; import static com.trafficparrot.sdk.ibmmq.DestinationLookup.USE_MAPPING_RESPONSE_DESTINATION; import static com.trafficparrot.sdk.ibmmq.SdkIbmMqUtils.readMessageBodySkipHeaders; import static com.trafficparrot.sdk.ibmmq.SdkIbmMqUtils.setMessageBody; public class TransformAndProxyRequestMessage extends IbmMqResponseTransformer { private static final Logger LOGGER = LoggerFactory.getLogger(TransformAndProxyRequestMessage.class); private static final long FOUR_YEARS_IN_MILLIS = 4L * 365 * 24 * 60 * 60 * 1000; public static final String DEFAULT_REAL_REQUEST_QUEUE_KEY = "default.real.request.queue"; public static final String SIT1_REAL_REQUEST_QUEUE_KEY = "sit1.real.request.queue"; public static final String SIT1_ENVIRONMENT_IDENTIFIER_KEY = "sit1.environment.identifier"; public static final String TRANSFORM_AND_PROXY_PROPERTIES_FILENAME = "transform-and-proxy.properties"; // You can put this file in the main Traffic Parrot directory where all the other properties files are private final Properties properties = readPropertiesFile(TRANSFORM_AND_PROXY_PROPERTIES_FILENAME); @Override @SuppressWarnings("UnnecessaryLocalVariable") protected IbmMqResponse doTransform(Destination requestDestination, MQMessage requestMessage, IbmMqResponse mappingResponse, Parameters parameters) throws Exception { MQMessage proxyResponseMessage = requestMessage; setReplyToQ(proxyResponseMessage); resetMqPropertiesThatAreSetByMq(proxyResponseMessage); moveDatesInBody(requestMessage, proxyResponseMessage); return new IbmMqResponse(mappingResponse.destination, proxyResponseMessage, mappingResponse.fixedDelayMilliseconds, Optional.of(USE_MAPPING_RESPONSE_DESTINATION)); } private void setReplyToQ(MQMessage proxiedMessage) { proxiedMessage.replyToQueueName = getReplyToQueue(proxiedMessage); } private String getReplyToQueue(MQMessage requestMessage) { String messageAsString = SdkIbmMqUtils.mqMessageToString(requestMessage); LOGGER.info("Transformer message converted to string: " + messageAsString); String sitEnvironmentId = properties.getProperty(SIT1_ENVIRONMENT_IDENTIFIER_KEY); if (messageAsString.contains(sitEnvironmentId)) { String sit1RealRequestQueue = properties.getProperty(SIT1_REAL_REQUEST_QUEUE_KEY); LOGGER.info("Transformer using " + sit1RealRequestQueue); return sit1RealRequestQueue; } else { String defaultRealRequestQueue = properties.getProperty(DEFAULT_REAL_REQUEST_QUEUE_KEY); LOGGER.info("Transformer using " + defaultRealRequestQueue); return defaultRealRequestQueue; } } private void moveDatesInBody(MQMessage requestMessage, MQMessage proxyResponseMessage) throws IOException, ParserConfigurationException, XPathExpressionException, SAXException, ParseException { // parse the XML request body String originalMessageBody = readMessageBodySkipHeaders(requestMessage); DocumentBuilderFactory abstractFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder factory = abstractFactory.newDocumentBuilder(); Document doc = factory.parse(new ByteArrayInputStream(originalMessageBody.getBytes())); String requestDateString = XPathFactory.newInstance().newXPath().compile("/transactions/get/startDate/text()").evaluate(doc); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); Date requestDate = format.parse(requestDateString); // calculate the new date (in the past) Date movedDate = new Date(requestDate.getTime() - FOUR_YEARS_IN_MILLIS); String movedDateString = format.format(movedDate); // set new date in the response String newMessageBody = originalMessageBody.replace(requestDateString, movedDateString); setMessageBody(proxyResponseMessage, newMessageBody); } private void resetMqPropertiesThatAreSetByMq(MQMessage proxyResponseMessage) { proxyResponseMessage.putDateTime = null; proxyResponseMessage.putApplicationName = "Traffic Parrot transformer proxy " + getClass().getSimpleName(); } }
{ "mappingId" : "1001", "request" : { "destination" : { "name" : "PROXY.REQUEST.QUEUE", "type" : "QUEUE" }, "bodyMatcher" : { "anything" : "anything" } }, "response" : { "destination" : { "name" : "REAL.REQUEST.QUEUE", "type" : "QUEUE" }, "ibmMqResponseTransformerClassName" : "com.trafficparrot.sdk.example.nativeibmmq.TransformAndProxyRequestMessage" }, "receiveThreads" : 1, "sendThreads" : 1 }
Extensions may access a standard set of runtime properties:
To access the properties in response transformers:
To access the properties in handlebars helpers:
Available property names:
Property name | Description | Example access |
---|---|---|
dataDirectory | The location of the data directory, which can be e.g. used to store CSV and XLS files | Path dataDirectory = Paths.get(properties.getProperty("dataDirectory")); |
databaseConnectionsFile | The location of the database-connections.json file containing database configuration JSON | Path databaseConnectionsFile = Paths.get(properties.getProperty("databaseConnectionsFile")); |
scenarioDirectory | The working directory, which is either the virtual service directory scenarios/ServiceName when using "properties" and the installation root when using "sharedProperties" | Path scenarioDirectory = Paths.get(properties.getProperty("scenarioDirectory")); |
This documentation is for an old version of Traffic Parrot. There is a more recent Traffic Parrot version available for download at trafficparrot.com