Skip to content

Commit

Permalink
Optimised the XPath Handlebars helper's performance by caching parsed…
Browse files Browse the repository at this point in the history
… XML documents and evaluated XPath expressions
  • Loading branch information
tomakehurst committed Feb 26, 2020
1 parent fb15c65 commit 92e2e10
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.io.IOException;

import com.github.jknack.handlebars.Context;
import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.Template;
import com.github.tomakehurst.wiremock.common.Exceptions;
Expand Down Expand Up @@ -56,8 +57,17 @@ private static Template uncheckedCompileTemplate(Handlebars handlebars, String t
}
}

public String apply(Object context) throws IOException {
public String apply(Object contextData) throws IOException {
final RenderCache renderCache = new RenderCache();
Context context = Context
.newBuilder(contextData)
.combine("renderCache", renderCache)
.build();

StringBuilder sb = new StringBuilder();
return sb.append(startContent).append(template.apply(context)).append(endContent).toString();
return sb.append(startContent)
.append(template.apply(context))
.append(endContent)
.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.github.tomakehurst.wiremock.extension.responsetemplating;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static java.util.Arrays.asList;

public class RenderCache {

private final Map<Key, Object> cache = new HashMap<>();

public void put(Key key, Object value) {
cache.put(key, value);
}

@SuppressWarnings("unchecked")
public <T> T get(Key key) {
return (T) cache.get(key);
}

public static class Key {
private final Class<?> forClass;
private final List<?> elements;

public static Key keyFor(Class<?> forClass, Object... elements) {
return new Key(forClass, asList(elements));
}

private Key(Class<?> forClass, List<?> elements) {
this.forClass = forClass;
this.elements = elements;
}

@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Key{");
sb.append("forClass=").append(forClass);
sb.append(", elements=").append(elements);
sb.append('}');
return sb.toString();
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Key key = (Key) o;
return forClass.equals(key.forClass) &&
elements.equals(key.elements);
}

@Override
public int hashCode() {
return Objects.hash(forClass, elements);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import com.github.jknack.handlebars.Options;
import com.github.tomakehurst.wiremock.common.Xml;
import com.github.tomakehurst.wiremock.extension.responsetemplating.RenderCache;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
Expand Down Expand Up @@ -73,16 +74,14 @@ public Object apply(final String inputXml, final Options options) throws IOExcep
final String xPathInput = options.param(0);

Document doc;
try (final StringReader reader = new StringReader(inputXml)) {
InputSource source = new InputSource(reader);
doc = localDocBuilder.get().parse(source);
try {
doc = getDocument(inputXml, options);
} catch (SAXException se) {
return handleError(inputXml + " is not valid XML");
}

try {
XPath xPath = localXPath.get();
Node node = (Node) xPath.evaluate(getXPathPrefix() + xPathInput, doc, NODE);
Node node = getNode(getXPathPrefix() + xPathInput, doc, options);

if (node == null) {
return "";
Expand All @@ -94,6 +93,38 @@ public Object apply(final String inputXml, final Options options) throws IOExcep
}
}

private Node getNode(String xPathExpression, Document doc, Options options) throws XPathExpressionException {
RenderCache renderCache = getRenderCache(options);
RenderCache.Key cacheKey = RenderCache.Key.keyFor(Document.class, xPathExpression, doc);
Node node = renderCache.get(cacheKey);

if (node == null) {
XPath xPath = localXPath.get();
node = (Node) xPath.evaluate(xPathExpression, doc, NODE);
renderCache.put(cacheKey, node);
}

return node;
}

private Document getDocument(String xml, Options options) throws SAXException, IOException {
RenderCache renderCache = getRenderCache(options);
RenderCache.Key cacheKey = RenderCache.Key.keyFor(Document.class, xml);
Document document = renderCache.get(cacheKey);
if (document == null) {
try (final StringReader reader = new StringReader(xml)) {
InputSource source = new InputSource(reader);
document = localDocBuilder.get().parse(source);
renderCache.put(cacheKey, document);
}
}

return document;
}

private RenderCache getRenderCache(Options options) {
return options.get("renderCache", null);
}

/**
* No prefix by default. It allows to extend this class with a specified prefix. Just overwrite this method to do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,49 +15,71 @@
*/
package com.github.tomakehurst.wiremock.extension.responsetemplating.helpers;

import com.github.jknack.handlebars.Context;
import com.github.jknack.handlebars.Helper;
import com.github.jknack.handlebars.Options;
import com.github.tomakehurst.wiremock.extension.responsetemplating.RenderCache;
import org.hamcrest.Matcher;
import org.junit.Assert;
import org.junit.Before;

import java.io.IOException;
import java.util.ArrayList;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;

public abstract class HandlebarsHelperTestBase {

protected RenderCache renderCache;

@Before
public void initRenderCache() {
renderCache = new RenderCache();
}

protected static final String FAIL_GRACEFULLY_MSG = "Handlebars helper should fail gracefully and show the issue directly in the response.";

protected static <T> void testHelperError(Helper<T> helper,
protected <T> void testHelperError(Helper<T> helper,
T content,
String pathExpression,
Matcher<String> expectation) {
try {
assertThat((String) helper.apply(content, createOptions(pathExpression)), expectation);
assertThat((String) renderHelperValue(helper, content, pathExpression), expectation);
} catch (final IOException e) {
Assert.fail(FAIL_GRACEFULLY_MSG);
}
}

protected static <T> void testHelper(Helper<T> helper,
@SuppressWarnings("unchecked")
protected <R, C> R renderHelperValue(Helper<C> helper, C content, String parameter) throws IOException {
return (R) helper.apply(content, createOptions(parameter));
}

protected <T> void testHelper(Helper<T> helper,
T content,
String optionParam,
String expected) throws IOException {
testHelper(helper, content, optionParam, is(expected));
}

protected static <T> void testHelper(Helper<T> helper,
protected <T> void testHelper(Helper<T> helper,
T content,
String optionParam,
Matcher<String> expected) throws IOException {
assertThat(helper.apply(content, createOptions(optionParam)).toString(), expected);
}

protected static Options createOptions(String optionParam) {
return new Options(null, null, null, null, null, null,
protected Options createOptions(String optionParam) {
return createOptions(optionParam, renderCache);
}

protected Options createOptions(String optionParam, RenderCache renderCache) {
Context context = Context.newBuilder(null)
.combine("renderCache", renderCache)
.build();

return new Options(null, null, null, context, null, null,
new Object[]{optionParam}, null, new ArrayList<String>(0));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@
package com.github.tomakehurst.wiremock.extension.responsetemplating.helpers;

import com.github.tomakehurst.wiremock.extension.Parameters;
import com.github.tomakehurst.wiremock.extension.responsetemplating.RenderCache;
import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer;
import com.github.tomakehurst.wiremock.http.ResponseDefinition;
import com.github.tomakehurst.wiremock.testsupport.WireMatchers;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.integration.junit4.JMock;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.IOException;

Expand All @@ -30,6 +35,7 @@
import static com.github.tomakehurst.wiremock.testsupport.WireMatchers.equalToXml;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.jmock.Expectations.anything;
import static org.junit.Assert.assertThat;

public class HandlebarsXPathHelperTest extends HandlebarsHelperTestBase {
Expand Down Expand Up @@ -108,6 +114,31 @@ public void rendersABlankWhenTheInputXmlIsAbsent() {
testHelperError(helper, null, "/test", is(""));
}

@Test
public void returnsCorrectResultWhenSameExpressionUsedTwiceOnIdenticalDocuments() throws Exception {
String one = renderHelperValue(helper, "<test>one</test>", "/test/text()");
String two = renderHelperValue(helper, "<test>one</test>", "/test/text()");

assertThat(one, is("one"));
assertThat(two, is("one"));
}

@Test
public void returnsCorrectResultWhenSameExpressionUsedTwiceOnDifferentDocuments() throws Exception {
String one = renderHelperValue(helper, "<test>one</test>", "/test/text()");
String two = renderHelperValue(helper, "<test>two</test>", "/test/text()");

assertThat(one, is("one"));
assertThat(two, is("two"));
}

@Test
public void returnsCorrectResultWhenDifferentExpressionsUsedOnSameDocument() throws Exception {
String one = renderHelperValue(helper, "<test><one>1</one><two>2</two></test>", "/test/one/text()");
String two = renderHelperValue(helper, "<test><one>1</one><two>2</two></test>", "/test/two/text()");

assertThat(one, is("1"));
assertThat(two, is("2"));
}

}

0 comments on commit 92e2e10

Please sign in to comment.