Dynamic Responses

« Back to documentation home

Generate responses

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.

Tutorial: Dynamic responses

For a basic introduction to dynamic responses with examples have a look at Dynamic responses tutorial.

Load CSV data file

Look up by row and column

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.

Select data using condition

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

Computed values with arithmetic operations

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}}
}

Generate random values

To generate a random value in the response use one of the following:

  1. {{randomInteger}} - A random integer, e.g. 1543243211
  2. {{randomInteger min max}} - A random integer between the given min and max, e.g. 10
  3. {{randomDouble}} - A random double, e.g. -0.3476573
  4. {{randomString}} - A random String, e.g. lW7H2gRfFqALsev0zDoJ4o3qGMtNYYgs
  5. {{randomUUID}} - A random UUID, e.g. 5d0d4989-b5c1-412e-bbe6-457fd5d58493

Available to template

HTTP*

The following HTTP response regions can be templated:

  1. Response body
  2. Response header values
  3. Response proxy URL
  4. Response webhook URL
  5. Response webhook HTTP method
  6. Response webhook body
  7. Response webhook header values
  8. Response webhook enabled expression

JMS

The following JMS response regions can be templated:

  1. Response body

Native IBM® MQ

The following Native IBM® MQ response regions can be templated:

  1. Response body

Files

The following file response regions can be templated:

  1. Response file content
  2. Response file name

gRPC

The following gRPC response regions can be templated:

  1. Response body
  2. Response headers

Thrift

The following Thrift response regions can be templated:

  1. Response body

Use request data in response

HTTP

To use HTTP request attributes in the response:

  1. {{request.url}} - URL
  2. {{request.path}} - Path
  3. {{request.path.[n]}} - N-th path element
  4. {{request.query.parameterName}} - Parameter value
  5. {{request.query.parameterName.[n]}} - N-th parameter value
  6. {{request.headers.headerName}} - First value of a header
  7. {{request.headers.[headerName]}} - First value of a header
  8. {{request.headers.headerName.[n]}}- N-th value of header
  9. {{request.cookies.cookieName}} - Cookie value
  10. {{request.body}} - Request body

To use HTTP request/response attributes in webhooks:

  1. {{originalRequest}} - Original request object, can be used as per {{request}} above
  2. {{originalResponse.body}} - Original response object (body only)

JMS

To use JMS request message attributes in the JMS response message:

  1. {{request.body}} - JMS request message body

Native IBM® MQ

To use Native IBM® MQ request message attributes in the IBM® MQ response message:

  1. {{request.body}} - IBM® MQ request message body

Files

To use request file attributes in the response file:

  1. {{request.body}} - Request message body
  2. {{request.fileName}} - Request file name

gRPC

To use gRPC request attributes in the response:

  1. {{request.body}} - Request body as a JSON object

Thrift

To use Thrift request attributes in the response:

  1. {{request.body}} - Request body as a JSON object

Handlebars helpers

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:
  1. The default string helpers
  2. The default conditional helpers
  3. The default number helpers
  4. Separate template files using partials including template reuse and inheritance

Template files

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.

The default file extension is .hbs, you can use a different file extension as follows:

{{>example.txt}}

More advanced usage is possible with template inheritance which you can use to parameterize blocks in a template:

base.hbs
Something before
{{#block "content"}}
Some default content
{{/block}}
Something after
override.hbs:
{{#partial "content" }}
Some override content
{{/partial}}
{{> base}}

WireMock helpers

The following WireMock Handlebars helpers are available for use.

  1. {{xPath request.body '/a/b/text()'}} - Extract XML values or sub documents via XPath
  2. {{soapXPath request.body '/a/b/text()'}} - Extract SOAP XML values or sub documents via XPath
  3. {{jsonPath request.body '$.outer.inner'}} - Extract JSON values or sub documents via JSONPath
  4. {{randomValue length=32 type='ALPHANUMERIC' uppercase=true }} - Generate random strings (also supports mixedcase=true)
  5. {{pickRandom 'A' 'B' 'C'}} - Pick a random value from a list of alternatives
  6. {{randomInt lower=1 upper=10}} - Pick a random value in an integer range (lower inclusive, upper exclusive)
  7. {{randomDecimal lower=1.0 upper=1.5}} - Pick a random value in a decimal range (lower inclusive, upper exclusive)
  8. {{range -2 2}} - Generate a list of integers list of integers
  9. {{array 1 'string' true}} - Put given values in a list literal
  10. {{hostname}} - Print the local machine hostname
  11. {{date (parseDate request.headers.SomeDate format='dd-MM-yyyy')}} - Date parsing
  12. {{date (truncateDate (parseDate request.headers.SomeDate) 'first day of month')}} - Date truncation
  13. {{#trim}} text with whitespace {{/trim}} - Remove whitespace
  14. {{#base64 padding=false decode=false}}content{{/base64}} - Base64 encoding and decoding
  15. {{#urlEncode decode=false}}content{{/urlEncode}} - URL encoding and decoding
  16. {{formData request.body 'form' urlDecode=true}}{{form.formField3}} - HTTP form parsing
  17. {{regexExtract request.body '([a-z]+)-([A-Z]+)-([0-9]+)' 'parts'}}{{parts.0}},{{parts.1}},{{parts.2}} - Regular expression extraction
  18. {{#if (matches '1234' '[0-9]+')}}OK{{/if}} - Regular expression conditional
  19. {{#if (contains 'abcde123' 'bcd')}}OK{{/if}} - String or array contains conditional
  20. {{parseJson request.body 'bodyJson'}}{{bodyJson.name}} - Parse string into a named JSON object
  21. {{size request.query.things}} - Number of elements in a string/list/map
  22. {{systemValue type='ENVIRONMENT' key='wiremock.VARIABLE'}} - Use system properties or environment variables
  23. {{#assign 'name'}}value{{/assign}} - Set a string variable with assign block
  24. {{lookup object 'field'}} - Object field lookup
  25. {{#trim}} content with whitespace {{/trim}} - Whitespace trim

The following WireMock Handlebars helpers have alternatives in Traffic Parrot.

  1. {{math}} - See Arithmetic operations
  2. {{now}} - See Transform strings and Date offset
    • The default implementation of {{now}} can be toggled using trafficparrot.properties
      • To use the Handlebars.java {{now}} helper:
        trafficparrot.virtualservice.handlebars.now.provider=HANDLEBARS
      • To use the WireMock {{now}} helper:
        trafficparrot.virtualservice.handlebars.now.provider=WIREMOCK
    • Alternatively, you can add the provider parameter per call to dynamically switch between them, otherwise the default implementation specified in trafficparrot.properties will be used
      • To enable this set in trafficparrot.properties:
        trafficparrot.virtualservice.handlebars.now.dynamic=true
      • To use the Handlebars.java implementation:
        {{now format='short' provider='HANDLEBARS'}}
      • To use the WireMock implementation:
        {{now offset='2 years' format='epoch' provider='WIREMOCK'}}

Transform strings

All of jknack Handlebars string helpers are available for use in both HTTP and JMS response headers and body.

  1. {{defaultIfEmpty value ["default value"]}} - Render a default value if the specified value is not available, for example {{defaultIfEmpty request.query.username "Username parameter was not present in the request"}}
  2. {{substring value start end}} - Render the beginning of the string value, for example {{defaultIfEmpty request.query.username 0 3}}
  3. {{substring value start}} - Render the end of the string value, for example {{defaultIfEmpty request.query.username 5}}
  4. {{replace value "aaa" "bbb"}} - Replace string aaa with bbb
  5. {{abbreviate value 5}} - Render a truncated version of a string. Minimum value is 4.
  6. {{yesno value [yes="yes"] [no="no"] maybe=["maybe"]}} - Convert a boolean "true", "false" to a string representation
  7. {{capitalize value [fully=false]}} - Capitalize a string
  8. {{stripTags value}} - Remove all HTML or XML tags from the string
  9. {{stringFormat string param0 param1 ... paramN}} - Format the string
  10. {{slugify value}} - Create a slug, for example "a b c" will be rendered as "a-b-c"
  11. {{numberFormat number ["format"] [locale=default]}} - Format a number for a given locale or format
  12. {{now ["format"] [tz=timeZone|timeZoneId]}} - Date is a specified format and timezone
  13. {{upper value}} - Uppercase string
  14. {{lower value}} - Lowercase string
  15. {{rjust value 20 [pad=" "]}} - Right adjust string
  16. {{ljust value 20 [pad=" "]}} - Left adjust string
  17. {{cut value [" "]}} - Cut out all the occurrences in the string
  18. {{center value size=19 [pad="char"]}} - Center string value padded with the specified character
  19. {{capitalizeFirst value}} - Capitalize first word
  20. {{join value "," [prefix="aPrefix"] [suffix="aSuffix"]}} - Join an array of values

Arithmetic operations

The math helper can be used to perform arithmetic operations:

  1. {{math a "+" b}} - Addition
  2. {{math a "-" b}} - Subtraction
  3. {{math a "*" b}} - Multiplication
  4. {{math a "/" b}} - Division
  5. {{math a "%" b}} - Modulus
  6. {{math "-" x}} - Negate
  7. {{math "round" x}} - Round
Supported options:
  • scale=N - the result will have N digits to the right of the decimal point

Modify response

The {{ modifyResponse }} helper can be used to modify the:

  • response HTTP status code
  • response header values
Examples:
  1. Modify the response status code:
    {{ modifyResponse 'statusCode' 404 }}
  2. Modify the response status code conditionally:
    {{#contains request.body 'A'}}
        {{ modifyResponse 'statusCode' 400 }}
    {{else contains request.body 'B'}}
        {{ modifyResponse 'statusCode' 500 }}
    {{else}}
        {{ modifyResponse 'statusCode' 200 }}
    {{/contains}}
  3. Modify a header value:
    {{ modifyResponse 'headerValue' 'header-name' 'header-value' }}

Convert data type

The {{ cast }} helper can be used to convert between data types.

Examples:
  1. {{ cast value type='int' }} - convert value to an int
  2. {{ cast list type='string' }} - convert list of elements to list of strings
  3. {{ cast list type='string' single=true }} - extract only element from a list and convert to a string type
  4. {{ randomInteger 0 (cast (xPath request.body '/Node/Max/text()') type='int') }} - example nested usage
  5. {{#if (cast value type='boolean') }}value cast to true{{/if}} - example if conditional usage
  6. {{#if (not (cast value type='boolean') ) }}value cast to false{{/if}} - example if not conditional usage
  7. {{#unless (cast value type='boolean') }}value cast to false{{/unless}} - example unless conditional usage
Supported options:
  • single=true - extract only element from a single element list
  • type='string' - convert to string representation
  • type='long' - convert to long
  • type='int' - convert to int
  • type='short' - convert to short
  • type='double' - convert to float
  • type='float' - convert to double
  • type='boolean' - convert to boolean
When casting to boolean, the semantics are:
  • The string 'true' casts to boolean true
  • The string 'false' casts to boolean false
  • Blank strings like '' or ' ' cast to boolean false
  • Empty collections cast to boolean false
  • Non-empty collections cast to boolean true
  • The number zero casts to boolean false
  • Non-zero numbers cast to boolean true

Date offset

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:
  • {{ dateOffset "2018-07-09" add=true format="yyyy-MM-dd" days=1 }} - add 1 day to the given date
  • {{ dateOffset "2017-10-31T16:16:10" inFormat="yyyy-MM-dd'T'HH:mm:ss" outFormat="yyyy-MM-dd'T'HH:mm:ssZ" zone="America/Los_Angeles" days=1 hours=4 }} - subtract 1 day and 4 hours from given local date and reformat as a timestamp in time zone
Supported options:
  • add=true - add to the input date rather than the default of subtracting
  • format="yyyy-MM-dd" - specify a date format for both parsing and formatting using standard date and time formatting options
  • inFormat="yyyy-MM-dd" - specify a date format for parsing using standard date and time formatting options
  • outFormat="yyyy-MM-dd" - specify a date format for formatting using standard date and time formatting options
  • zone="UTC" - specify a time zone ID for both parsing and formatting e.g. America/Los_Angeles or UTC or -08:30
  • inZone="UTC" - specify a time zone ID for parsing e.g. America/Los_Angeles or UTC or -08:30
  • outZone="UTC" - specify a time zone ID for formatting e.g. America/Los_Angeles or UTC or -08:30
  • days=1 - number of days to offset by
  • minutes=1 - number of minutes to offset by
  • seconds=1 - number of seconds to offset by
  • millis=1 - number of milliseconds to offset by
  • nanos=1 - number of nanoseconds to offset by

Data source

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 Batch Update Multiple Results Multiple Conditions Default Values
CSV
XLS
JDBC
Couchbase1

1 Couchbase uses N1SQL aka SQL++ statements which also includes other operations such as UPSERT and MERGE

Please contact us if you require any data source operation compatibility that is not currently available.

Examples:
  1. Select a single value from data/isoCountryCodes.csv or default to GBR:
    {{ dataSource '.csv'
    'SELECT isoCountryCode
    FROM isoCountryCodes.csv
    WHERE internalCode = :1'
    (jsonPath request.body '$.internalCode')
    single=true
    default='GBR' }}
  2. Select multiple rows and columns from the JDBC database with id example.db:
    [
    {{#each (dataSource 'example.db' 'SELECT * FROM table') }}
        {
            "A": {{ A }},
            "B": {{ B }}
        }{{#unless @last}},{{/unless}}
    {{/each}}
    ]
  3. Insert single record into the JDBC database with id example.db:
    {{ dataSource 'example.db'
    'INSERT INTO PERSON(ID, NAME) VALUES(:id, :name)'
    id=(jsonPath request '$.id')
    name=(jsonPath request '$.name') }}
  4. Insert multiple records into the JDBC database with id example.db:
    {{ dataSource 'example.db'
    'INSERT INTO PERSON(ID, NAME) VALUES(:ids, :names)'
    ids=(jsonPath request '$.data[*].id')
    names=(jsonPath request '$.data[*].name') }}
  5. Select multiple columns and single row from XLS file data/example.xlsx and sheet ExampeSheet:
    {{#with (dataSource 'example.xlsx' 'SELECT * FROM ExampleSheet' singleRow=true ) }}
    {
        "A": {{ A }},
        "B": {{ B }}
    }
    {{/with}}
  6. Select single value using positional argument :1
    {{dataSource 'example.xlsx'
    'SELECT name
    FROM ExampleSheet
    WHERE id = :1'
    (jsonPath request.body '$.id')
    single=true }}
  7. Select single value using named argument :id
    {{dataSource 'example.xlsx'
    'SELECT name
    FROM ExampleSheet
    WHERE id = :id'
    id=(jsonPath request.body '$.id')
    single=true }}
Supported options:
  • single=true - extract single row single column result
  • singleRow=true - extract single row result
  • maxRows=N - limit number of rows returned
  • default=value - provide default value to be used on error or no result
  • shared=true - use configuration from installation root when true or configuration from scenarios/ScenarioName/* when false

Further details on the specifics of each data source can be found below.

CSV
  • The .csv files should be placed in the data directory
  • To configure caching of CSV files for improved performance use the propertytrafficparrot.virtualservice.handlebars.select.indexAndCacheCsvFiles=true
  • Column names must be defined in the first row
XLS
  • The .xls or .xlsx files should be placed in the data directory
  • Sheet names are used as the "table" to select from
  • Column names must be defined in the first row
JDBC
  • Supported databases include:
    • MySQL
    • MariaDB
    • PostgreSQL
    • Oracle
    • Microsoft SQL Server MSSQL
    • and any other database that has a JDBC driver
  • The database driver JAR must be placed in the lib/external/*.jar directory
  • The database schema and tables must already exist before attempting to use as a data source
  • The database connection should be defined in the database-connections.json file, for example:
    [
      {
        "connectionId": "example.db",
        "type": "JDBC_CONNECTION",
        "driverClass": "org.h2.Driver",
        "jdbcUrl": "jdbc:h2:mem:example",
        "properties": {
          "user": "sa",
          "password": "sa"
        }
      },
      {
        "connectionId": "postgres.db",
        "type": "JDBC_CONNECTION",
        "driverClass": "org.postgresql.Driver",
        "jdbcUrl": "jdbc:postgresql://host:port/database",
        "properties": {
          "user": "user",
          "password": "password"
        }
      }
    ]
Couchbase
  • The following Couchbase SDK JARs must be placed in the lib/external/*.jar directory:
  • The bucket must already exist before attempting to use as a data source
  • The database connection should be defined in the database-connections.json file, for example:
    [
      {
        "connectionId": "couchbase.db",
        "type": "COUCHBASE_CONNECTION",
        "connectionString": "couchbase://localhost:32784",
        "username": "Administrator",
        "password": "password",
        "warmupQuery": "SELECT COUNT(*) FROM bucket_a UNION SELECT COUNT(*) FROM bucket_b",
        "enableDnsSrv": true,
        "networkResolution": "auto"
      }
    ]
  • Couchbase uses N1SQL aka SQL++ statements in the following syntax:
    {{ dataSource 'couchbase.db' 'INSERT INTO PERSON(KEY, VALUE) VALUES ("$id", {"id" : $id,"name" : $name})' id=1000 name='some-name' syntax='N1QL' }}
    {{ dataSource 'couchbase.db' 'SELECT name FROM PERSON USE KEYS "$id"' id=1000 single=true syntax='N1QL' }}
    {{ dataSource 'couchbase.db' 'INSERT INTO PERSON(KEY, VALUE) VALUES ("$id", $object)' id=1000 object=example syntax='N1QL' }}

Object store

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:
  1. Create a new object in data/person-uuid.json using the incoming JSON request body and unique id:
    {{ objectStore 'person.json' operation='create' id=(jsonPath request.body '$.id') object=request.body }}
  2. Create a new object in data/person-uuid.json using the incoming JSON request body and generated unique id:
    {{#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}}
  3. Get an existing object stored in data/person-uuid.json:
    {{ objectStore 'person.json' operation='get' id=(jsonPath request.body '$.id') path='$' }}
  4. Get a particular field from an existing object stored in data/person-1.json:
    {{ objectStore 'person.json' operation='get' id=(jsonPath request.body '$.id') path='$.field' }}
  5. Set an existing field to the value of a field in the request:
    {{ objectStore 'person.json' operation='set' id=(jsonPath request.body '$.id') path='$.field' object=(jsonPath request.body '$.field') }}
  6. Delete an existing field:
    {{ objectStore 'person.json' operation='delete' id=(jsonPath request.body '$.id') path='$.field' }}
  7. Delete entire object:
    {{ objectStore 'person.json' operation='delete' id=(jsonPath request.body '$.id') path='$' }}
  8. Add or update a field at the root of the object:
    {{ objectStore 'person.json' operation='put' id=(jsonPath request.body '$.id') path='$' key='field' object=(jsonPath request.body '$.field') }}
  9. Add or update a field at a child of the object:
    {{ objectStore 'person.json' operation='put' id=(jsonPath request.body '$.id') path='$.child' key='field' object=(jsonPath request.body '$.child.field') }}
  10. Add an element to an existing array:
    {{ objectStore 'person.json' operation='add' id=(jsonPath request.body '$.id') path='$.names' object=(jsonPath request.body '$.name') }}
Supported options:
  • operation='name' - supported operations are: create, get, set, delete, add, put
  • overwrite=true - allows the create operation to overwrite an existing object when true
  • id='' - a unique id must be provided to identify the object
  • default=null - if specified, the provided default value will be returned if an object is not found with the given id
  • path='$' - a JSONPath expression used by the operation
  • object='' - a JSON object used by the operation
  • key='' - the name of the new field to be added by the put operation
  • shared=true - use configuration from installation root when true or configuration from scenarios/ScenarioName/* when false

Evaluate

The {{ evaluate }} helper can be used to evaluate an expression in a scripting language such as JavaScript.

NOTE: this requires a scripting engine such as Nashhorn or GraalVM to be installed, either as part of the JRE or placed in the lib/external/*.jar directory.

Examples:
  1. Complex boolean expression:
    {{ evaluate '/[0-9]+/.test(request.headers["request-id"]) && JSON.parse(request.body)["name"].contains("123123")' }}
  2. Using if statements:
    {{#evaluate}}
    if (key === 123) {
      'hello world 123'
    } else {
      'hello non 123 world'
    }
    {{/evaluate}}
  3. Render current epoch time:
    {{ evaluate 'new Date().getTime()' }}
  4. Render using print statement:
    {{ evaluate 'print("hello world print")'}}
Supported options:
  • language='js' - currently only js is supported, which is also the default
  • redirectOutput='RESULT' - RESULT will enable print statements LOGS will suppress print statements from the result and instead put them in the Traffic Parrot logs
Supported context items:
  • Use the keyword self to reference the current context object:
    {{#with 'test' }}{{ evaluate 'self' }}{{/with}}
  • Use the name of any context variable to reference it:
    {{ evaluate 'name' }}
  • Use the syntax parameters[i] to reference parameters by index:
    {{ evaluate 'parameters[2]' 'zero' 'one' 'two' }}
  • Use the syntax options["name"] to reference named options by name:
    {{ evaluate 'options["example"]' example='value' }}

Extract data from request fields and attributes

The following additional helpers are available in both HTTP, JMS and File response templates.

xPath

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!
xPathList

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>
jsonPath

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 also use JsonPath conditional expressions, for example to check for the presence of a field:
{{#if (jsonPath request.body '$.[?(@.field)]') }}field is present{{/if}}
{{#if (not (jsonPath request.body '$.[?(@.field)]') ) }}field is not present{{/unless}}
{{#if (jsonPath request.body '$.[?(!(@.field))]') }}field is not present{{/unless}}
{{#unless (jsonPath request.body '$.[?(@.field)]') }}field is not present{{/unless}}
jsonPathList

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" }
]
regex

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 after
you will see a HTTP response:
Request value was: 001116059c5549a0tOACamel-116059c554a20tOH

Nested handlebars helpers

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

Loops and iterators

All block helpers defined in the handlebars specification are available. As an example the following use of {{#each}} in a response body will render all request headers:
<requestHeaders>
  {{#each request.headers}}
    <name>{{@key}}</name>
    <value>{{this}}</value>
  {{/each}}
</requestHeaders>

Variables

The standard {{#with}}defined in the handlebars specification is available. As an example the following use of {{#with}} in a response body allows reuse of the same variable multiple times in the block:
{{#with (jsonPath request.body '$.internalCode') as |internalCode|}}
  "internalCode": {{ internalCode }},
  "isoCountryCode": "{{select 'isoCountryCode from isoCountryCodes.csv where internalCode equals' internalCode }}"
{{/with}}

Conditionals (if, else)

Standard Handlebars block helpers

All block helpers defined in handlebars specification are available. As an example the following use of {{#if}} in a response body will render a different response based on availability of a request paramater:
{{#if request.query.password}}
  Password was present in the request.
{{else}}
  Please provide a password!
{{/if}}

Additional block helpers

There are also additional helpers available:
equal

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}}
It can also be used inline to render true or false:
{{ equal a b }}
ifEven

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}}

Assorted

  1. {{httpNow}} - Date in RFC 1123 format (HTTP date format), e.g. Date: {{httpNow}} can be rendered as Date: Sat, 01 Apr 2017 10:55:05 GMT. Typically used as a value for the "Date" header.
  2. {{size attributeName}} - Size or length of an array or collection, for example {{size (jsonPathList request.body '$.items')}}

Ask for new extensions

If you need new extensions just email us at support@trafficparrot.com we will will do our best to create them for you.

Disabling {{...}}

If you would like to use {{...}} notation in your templates and not having it interpreted as an attempt to use an extension you need to do one of the following things:
  1. For individual responses, use escaped syntax: \{{...}}
  2. Or, disable extensions globally by setting both trafficparrot.jms.handlebars.enabled=false and trafficparrot.http.handlebars.enabled=false in trafficparrot.properties and restarting Traffic Parrot

Create your own custom extensions

Introduction

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.

There are two types of extensions you can create:
  • Custom handlebar helpers. Simple, for basic usage. These extensions can be used to transform or generate dynamically the response content.
  • Custom response transformers for HTTP, JMS and Native IBM MQ. Complex, for advanced usage. These extensions can do advanced transformations, routing of responses, and many more.

Custom handlebar helpers

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: If you would like to add new Handlebars Helpers, have a look at the following example:
  1. Download and open the SDK workspace project in an IDE like IntelliJ IDEA or Eclipse
  2. Explore the project and the Java classes available in the SDK. Look for usages of TrafficParrotHelper They provide example usages of Handlebars Helpers. Have a look at both main and test classes.
  3. Create a new extension class based on the examples provided in the SDK (by extending TrafficParrotHelper class). For example:
    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()));
        }
    }
  4. Build the project ./mvnw clean install
  5. Copy target/trafficparrot-sdk-workspace-x.y.z.jar
    to directory trafficparrot-x.y.z/lib/virtualservice-x.y.z to use with HTTP
    or to directory trafficparrot-x.y.z/lib/external to use with JMS
  6. Look for properties trafficparrot.http.handlebars.helpers and trafficparrot.jms.handlebars.helpers in trafficparrot.properties and add your new class to the list of extension classes there. (Why do I have to edit the extensions properties every time?)
  7. Stop Traffic Parrot
  8. Start Traffic Parrot
  9. If the name of the class you have created is RandomInteger the extension you will use in response templates will be called {{randomInteger}} by default. The name can be changed by implementing the TrafficParrotHelper.getName method.
  10. Edit an existing mapping and put {{randomInteger}} anywhere in the response body.
  11. Make a request to the virtual service
  12. The response you receive should contain the randomly generated integer.

Custom HTTP response transformers

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:
  1. Download and open the SDK workspace project in an IDE like IntelliJ IDEA or Eclipse
  2. Explore the project and the Java classes available in the SDK. They provide example usages of HTTP response transformers. For example, see com.trafficparrot.sdk.example.http.RandomServerFailure
    Have a look at both main and test classes.
  3. Create a new extension class based on the examples provided in the SDK (by extending HttpResponseTransformer class). For example you can use the sample provided in the SDK com.trafficparrot.sdk.example.http.RandomServerFailure
    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();
        }
    }
  4. Build the project by running maven ./mvnw clean install
  5. Make sure the build is successful, you should see: [INFO] BUILD SUCCESS
  6. Copy target/trafficparrot-sdk-workspace-x.y.z.jar to directory trafficparrot-x.y.z/lib/external
  7. Look for property trafficparrot.http.responsetransformers in Traffic Parrot properties file trafficparrot-x.y.z/trafficparrot.properties and add your new class to the list of extension classes there. So you should see
    trafficparrot.http.responsetransformers=com.trafficparrot.sdk.example.http.RandomServerFailure
  8. Stop Traffic Parrot
  9. Start Traffic Parrot
  10. Create a HTTP request to response mapping in Traffic Parrot if you do not have one yet. For example, make a mapping "/hello" that return a 200 response with body "Hello World!"
  11. Note, since the code in your matcher has
        @Override
        public boolean applyGlobally() {
            return true;
        }
    
    it will be applied globally for all mappings.
  12. (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"]
    }
  13. Make a few requests to the virtual service with a web browser, Postman or curl.
  14. The response you receive should randomly fail, as defined in the response transformer RandomServerFailure.

Custom JMS response transformers

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:
  1. Download and open the SDK workspace project in an IDE like IntelliJ IDEA or Eclipse
  2. Explore the project and the Java classes available in the SDK. They provide example usages of JMS response transformers. Look for usages of JmsResponseTransformer and TextJmsResponseTransformer. Have a look at both main and test classes.
  3. Create a new extension class based on the examples provided in the SDK (by extending TextJmsResponseTransformer class). For 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;
    }
  4. Build the project ./mvnw clean install
  5. Copy trafficparrot-x.y.z/lib/external target/trafficparrot-sdk-workspace-x.y.z.jar to directory
  6. Look for property trafficparrot.jms.responsetransformers in trafficparrot.properties and add your new class to the list of extension classes there. (Why do I have to edit the extensions properties every time?)
  7. Stop Traffic Parrot
  8. Start Traffic Parrot
  9. Edit an existing JMS mapping and select ChooseResponseQueueBasedOnRequestMessageBody from the "Response transformer" dropdown.
  10. Start the JMS virtual service in replay mode and send a JMS request with content {"requestField": "foo"}
  11. The response message should be sent to queue response_queue_1
  12. Send a JMS request with content {"requestField": "bar"}
  13. The response message should be sent to queue response_queue_2

Custom Native IBM MQ response transformers

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:
  1. Download and open the SDK workspace project in an IDE like IntelliJ IDEA or Eclipse
  2. Explore the project and the Java classes available in the SDK. They provide example usages of JMS response transformers. Look for usages of IbmMqResponseTransformer.
  3. Create a new extension class based on the examples provided in the SDK (by extending IbmMqResponseTransformer class). See below for an example.
  4. Here is an example of a response transformer class. The class is a quite complex example, in which we transform and proxy the original request message, so there is no "response message" as such, but a "proxied and transformed request message" which is treated by traffic parrot as a response message. The purpose of this example is to demonstrate how powerful the plugins can be.
    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();
        }
    }
    
  5. Build the project ./mvnw clean install
  6. Copy target/trafficparrot-sdk-workspace-x.y.z.jar to directory trafficparrot-x.y.z/lib/external
  7. In the mapping JSON file define ibmMqResponseTransformerClassName, for example
    {
      "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
    }
  8. Stop Traffic Parrot
  9. Start Traffic Parrot
  10. Start the virtual service in replay mode and send a Native MQ request message to REAL.REQUEST.QUEUE with body <ns:transactions xmlns:ns="http://example.trafficparrot.com"><get><startDate>2020-01-03</startDate></get></ns:transactions>
  11. The response message should be a transformed request message (the body had a date moved in the past) sent to the real request queue.

Runtime properties

Extensions may access a standard set of runtime properties:

  • Properties specific to the virtual service named "properties", with directories relative to scenarios/ServiceName/*
  • Properties shared amongst all virtual services named "sharedProperties", with directories relative to the installation root (as defined in trafficparrot.virtualservice.trafficFilesRootUrl)

To access the properties in response transformers:

  • Properties properties = (Properties) parameters.get("properties");
  • Properties sharedProperties = (Properties) parameters.get("sharedProperties");

To access the properties in handlebars helpers:

  • Properties properties = (Properties) options.context.get("properties");
  • Properties sharedProperties = (Properties) options.context.get("sharedProperties");

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"));

Old version warning!

This documentation is for an old version of Traffic Parrot. There is a more recent Traffic Parrot version available for download at trafficparrot.com

Browse documentation for recent version