From 8aa741301310079b3093878cb8eb092d5c9a5207 Mon Sep 17 00:00:00 2001 From: Jens-Otto Larsen Date: Sun, 16 Aug 2026 21:01:34 +0200 Subject: [PATCH 1/2] Module for Jackson 3 --- handlebars-jackson3/pom.xml | 61 +++++ .../handlebars/jackson/JacksonHelper.java | 177 ++++++++++++++ .../jackson/JsonNodeValueResolver.java | 155 ++++++++++++ .../src/main/java/module-info.java | 9 + .../jknack/handlebars/jackson/Blog.java | 63 +++++ .../jknack/handlebars/jackson/Comment.java | 9 + .../handlebars/jackson/JacksonHelperTest.java | 160 ++++++++++++ .../jackson/JsonNodeValueResolverTest.java | 231 ++++++++++++++++++ 8 files changed, 865 insertions(+) create mode 100644 handlebars-jackson3/pom.xml create mode 100644 handlebars-jackson3/src/main/java/com/github/jknack/handlebars/jackson/JacksonHelper.java create mode 100644 handlebars-jackson3/src/main/java/com/github/jknack/handlebars/jackson/JsonNodeValueResolver.java create mode 100644 handlebars-jackson3/src/main/java/module-info.java create mode 100644 handlebars-jackson3/src/test/java/com/github/jknack/handlebars/jackson/Blog.java create mode 100644 handlebars-jackson3/src/test/java/com/github/jknack/handlebars/jackson/Comment.java create mode 100644 handlebars-jackson3/src/test/java/com/github/jknack/handlebars/jackson/JacksonHelperTest.java create mode 100644 handlebars-jackson3/src/test/java/com/github/jknack/handlebars/jackson/JsonNodeValueResolverTest.java diff --git a/handlebars-jackson3/pom.xml b/handlebars-jackson3/pom.xml new file mode 100644 index 00000000..c02a8c4a --- /dev/null +++ b/handlebars-jackson3/pom.xml @@ -0,0 +1,61 @@ + + + + + com.github.jknack + handlebars.java + 4.5.4 + + + 4.0.0 + handlebars-jackson3 + + JSON Jackson helpers + + + + com.github.jknack + handlebars + ${project.version} + + + + tools.jackson.core + jackson-databind + + + + + com.github.jknack + handlebars + ${project.version} + test + tests + + + + ch.qos.logback + logback-classic + test + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + org.hamcrest + hamcrest + test + + + + org.mockito + mockito-core + test + + + + diff --git a/handlebars-jackson3/src/main/java/com/github/jknack/handlebars/jackson/JacksonHelper.java b/handlebars-jackson3/src/main/java/com/github/jknack/handlebars/jackson/JacksonHelper.java new file mode 100644 index 00000000..c9fadb76 --- /dev/null +++ b/handlebars-jackson3/src/main/java/com/github/jknack/handlebars/jackson/JacksonHelper.java @@ -0,0 +1,177 @@ +/* + * Handlebars.java: https://github.com/jknack/handlebars.java + * Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 + * Copyright (c) 2012 Edgar Espina + */ +package com.github.jknack.handlebars.jackson; + +import com.github.jknack.handlebars.Handlebars; +import com.github.jknack.handlebars.Helper; +import com.github.jknack.handlebars.Options; +import tools.jackson.core.SerializableString; +import tools.jackson.core.io.CharacterEscapes; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectWriter; +import tools.jackson.databind.json.JsonMapper; + +import java.util.HashMap; +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +/** + * A Jackson 3.x helper. + * + *

Basic usage: + * + *

+ *  Handlebars hbs = new Handlebars();
+ *
+ *  hbs.registerHelper("json", JacksonHelper.INSTANCE);
+ *
+ *  ...
+ *
+ *  {{json model}}
+ * 
+ * + *

If model is null an empty string is returned. + * + *

You can change this using the default option: + * + *

+ *  {{json model default="{}"}}
+ * 
+ * + *

Using a view class: + * + *

+ *  {{json model view="foo.MyView"}}
+ * 
+ * + *

Using alias for views: + * + *

+ *  {{json model view="myView"}}
+ * 
+ * + *

Escape HTML chars: + * + *

+ *  {{json model escapeHTML=true}}
+ * 
+ * + *

Pretty printer: + * + *

+ *  {{json model pretty=true}}
+ * 
+ * + * @author edgar.espina, jolarsen + * @since 4.5.5 + */ +public class JacksonHelper implements Helper { + + /** + * Escape HTML chars from JSON content. See + * http://www.cowtowncoder.com/blog/archives/2012/08/entry_476.html + * + * @author edgar.espina, jolarsen + * @since 4.5.5 + */ + @SuppressWarnings("serial") + private static class HtmlEscapes extends CharacterEscapes { + + /** The escape table. */ + private final int[] escapeTable; + + { + // Start with set of characters known to require escaping (double-quote, + // backslash etc) + escapeTable = CharacterEscapes.standardAsciiEscapesForJSON(); + // and force escaping of a few others: + escapeTable['<'] = CharacterEscapes.ESCAPE_STANDARD; + escapeTable['>'] = CharacterEscapes.ESCAPE_STANDARD; + escapeTable['&'] = CharacterEscapes.ESCAPE_STANDARD; + escapeTable['\''] = CharacterEscapes.ESCAPE_STANDARD; + } + + @Override + public int[] getEscapeCodesForAscii() { + return escapeTable; + } + + @Override + public SerializableString getEscapeSequence(final int ch) { + return null; + } + } + + /** A singleton version of {@link JacksonHelper}. */ + public static final Helper INSTANCE = new JacksonHelper(); + + /** The JSON parser. */ + private final ObjectMapper mapper; + + /** Class alias registry. */ + private final Map> alias = new HashMap>(); + + /** + * Creates a new {@link JacksonHelper}. + * + * @param objectMapper The object's mapper. Required. + */ + public JacksonHelper(final ObjectMapper objectMapper) { + mapper = requireNonNull(objectMapper, "The object mapper is required."); + } + + /** Creates a new {@link JacksonHelper}. */ + private JacksonHelper() { + this(JsonMapper.builder().build()); + } + + @Override + public Object apply(final Object context, final Options options) { + if (context == null) { + return options.hash("default", ""); + } + + final String viewName = options.hash("view", ""); + try { + final Class viewClass = alias.get(viewName); + final ObjectWriter viewWriter = Handlebars.Utils.isEmpty(viewName) + ? mapper.writer() + : mapper.writerWithView(viewClass != null + ? viewClass + : getClass().getClassLoader().loadClass(viewName)); + + final boolean escapeHtml = options.hash("escapeHTML", false); + final boolean pretty = options.hash("pretty", false); + + final ObjectWriter writer = pretty + ? viewWriter.withDefaultPrettyPrinter() + : viewWriter; + + final ObjectWriter configuredWriter = escapeHtml + ? writer.with(new HtmlEscapes()) + : writer; + + return new Handlebars.SafeString(configuredWriter.writeValueAsString(context)); + } catch (ClassNotFoundException|NoClassDefFoundError ex) { + throw new IllegalArgumentException(viewName, ex); + } + } + + /** + * Add an alias for the given view class. + * + * @param alias The view alias. Required. + * @param viewClass The view class. Required. + * @return This helper. + */ + public JacksonHelper viewAlias(final String alias, final Class viewClass) { + this.alias.put( + requireNonNull(alias, "A view alias is required."), + requireNonNull(viewClass, "A view class is required.")); + return this; + } +} diff --git a/handlebars-jackson3/src/main/java/com/github/jknack/handlebars/jackson/JsonNodeValueResolver.java b/handlebars-jackson3/src/main/java/com/github/jknack/handlebars/jackson/JsonNodeValueResolver.java new file mode 100644 index 00000000..5d9d7ca7 --- /dev/null +++ b/handlebars-jackson3/src/main/java/com/github/jknack/handlebars/jackson/JsonNodeValueResolver.java @@ -0,0 +1,155 @@ +/* + * Handlebars.java: https://github.com/jknack/handlebars.java + * Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 + * Copyright (c) 2012 Edgar Espina + */ +package com.github.jknack.handlebars.jackson; + +import com.github.jknack.handlebars.ValueResolver; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.BigIntegerNode; +import tools.jackson.databind.node.BinaryNode; +import tools.jackson.databind.node.BooleanNode; +import tools.jackson.databind.node.DecimalNode; +import tools.jackson.databind.node.DoubleNode; +import tools.jackson.databind.node.IntNode; +import tools.jackson.databind.node.LongNode; +import tools.jackson.databind.node.NullNode; +import tools.jackson.databind.node.ObjectNode; +import tools.jackson.databind.node.POJONode; +import tools.jackson.databind.node.StringNode; + +import java.util.AbstractMap; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * Resolve a context stack value from {@link JsonNode}. + * + * @author edgar.espina, jolarsen + * @since 4.5.5 + */ +public enum JsonNodeValueResolver implements ValueResolver { + + /** The singleton instance. */ + INSTANCE; + + @Override + public Object resolve(final Object context, final String name) { + Object value = null; + if (context instanceof ArrayNode arrayNode) { + try { + if (name.equals("length")) { + return arrayNode.size(); + } + value = resolve(arrayNode.get(Integer.parseInt(name))); + } catch (NumberFormatException ex) { + // ignore undefined key and move on, see + // https://github.com/jknack/handlebars.java/pull/280 + } + } else if (context instanceof JsonNode jsonNode) { + value = resolve(jsonNode.get(name)); + } + return value == null ? UNRESOLVED : value; + } + + @Override + public Object resolve(final Object context) { + if (context instanceof JsonNode jsonNode) { + return resolve(jsonNode); + } + return UNRESOLVED; + } + + /** + * Resolve a {@link JsonNode} object to a primitive value. + * + * @param node A {@link JsonNode} object. + * @return A primitive value, json object, json array or null. + */ + private static Object resolve(final JsonNode node) { + // binary node + if (node instanceof BinaryNode binaryNode) { + return binaryNode.binaryValue(); + } + // boolean node + if (node instanceof BooleanNode booleanNode) { + return booleanNode.booleanValue(); + } + // null node + if (node instanceof NullNode) { + return null; + } + // numeric nodes + if (node instanceof BigIntegerNode bigIntegerNode) { + return bigIntegerNode.bigIntegerValue(); + } + if (node instanceof DecimalNode decimalNode) { + return decimalNode.decimalValue(); + } + if (node instanceof DoubleNode doubleNode) { + return doubleNode.doubleValue(); + } + if (node instanceof IntNode intNode) { + return intNode.intValue(); + } + if (node instanceof LongNode longNode) { + return longNode.longValue(); + } + // pojo + if (node instanceof POJONode pojoNode) { + return pojoNode.getPojo(); + } + // string + if (node instanceof StringNode stringNode) { + return stringNode.stringValue(); + } + // object node to map + if (node instanceof ObjectNode objectNode) { + return toMap(objectNode); + } + // container, array or null + return node; + } + + /** + * @param node A json node. + * @return A map from a json node. + */ + private static Map toMap(final ObjectNode node) { + return new AbstractMap() { + @Override + public Object get(final Object key) { + return resolve(node.get((String) key)); + } + + @Override + public int size() { + return node.size(); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public Set> entrySet() { + Set set = new LinkedHashSet>(); + node.propertyStream().forEach(set::add); + return set; + } + }; + } + + @Override + public Set> propertySet(final Object context) { + if (context instanceof ObjectNode node) { + Map result = new LinkedHashMap<>(); + node.propertyNames().forEach(name -> result.put(name, resolve(node.get(name)))); + return result.entrySet(); + } + return Collections.emptySet(); + } +} diff --git a/handlebars-jackson3/src/main/java/module-info.java b/handlebars-jackson3/src/main/java/module-info.java new file mode 100644 index 00000000..cc47d90e --- /dev/null +++ b/handlebars-jackson3/src/main/java/module-info.java @@ -0,0 +1,9 @@ +/** handlebars jackson3. */ +module com.github.jknack.handlebars.jackson { + exports com.github.jknack.handlebars.jackson; + + requires org.slf4j; + requires com.github.jknack.handlebars; + requires tools.jackson.core; + requires tools.jackson.databind; +} diff --git a/handlebars-jackson3/src/test/java/com/github/jknack/handlebars/jackson/Blog.java b/handlebars-jackson3/src/test/java/com/github/jknack/handlebars/jackson/Blog.java new file mode 100644 index 00000000..de199552 --- /dev/null +++ b/handlebars-jackson3/src/test/java/com/github/jknack/handlebars/jackson/Blog.java @@ -0,0 +1,63 @@ +/* + * Handlebars.java: https://github.com/jknack/handlebars.java + * Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 + * Copyright (c) 2012 Edgar Espina + */ +package com.github.jknack.handlebars.jackson; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonView; + +import java.util.ArrayList; +import java.util.List; + +@JsonPropertyOrder({"title", "body", "comments"}) +public class Blog { + + public static class Views { + public static class Public {} + } + + @JsonView(Views.Public.class) + private String title; + + private String body; + + private List comments = new ArrayList(); + + public Blog(final String title, final String body) { + this.title = title; + this.body = body; + } + + public Blog() {} + + public String getTitle() { + return title; + } + + public String getBody() { + return body; + } + + public Blog add(final Comment comment) { + comments.add(comment); + return this; + } + + public List getComments() { + return comments; + } + + public void setBody(final String body) { + this.body = body; + } + + public void setComments(final List comments) { + this.comments = comments; + } + + public void setTitle(final String title) { + this.title = title; + } +} diff --git a/handlebars-jackson3/src/test/java/com/github/jknack/handlebars/jackson/Comment.java b/handlebars-jackson3/src/test/java/com/github/jknack/handlebars/jackson/Comment.java new file mode 100644 index 00000000..5e6803c1 --- /dev/null +++ b/handlebars-jackson3/src/test/java/com/github/jknack/handlebars/jackson/Comment.java @@ -0,0 +1,9 @@ +/* + * Handlebars.java: https://github.com/jknack/handlebars.java + * Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 + * Copyright (c) 2012 Edgar Espina + */ +package com.github.jknack.handlebars.jackson; + +public record Comment(String author, String comment) { +} diff --git a/handlebars-jackson3/src/test/java/com/github/jknack/handlebars/jackson/JacksonHelperTest.java b/handlebars-jackson3/src/test/java/com/github/jknack/handlebars/jackson/JacksonHelperTest.java new file mode 100644 index 00000000..cc40b923 --- /dev/null +++ b/handlebars-jackson3/src/test/java/com/github/jknack/handlebars/jackson/JacksonHelperTest.java @@ -0,0 +1,160 @@ +/* + * Handlebars.java: https://github.com/jknack/handlebars.java + * Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 + * Copyright (c) 2012 Edgar Espina + */ +package com.github.jknack.handlebars.jackson; + +import com.github.jknack.handlebars.Handlebars; +import com.github.jknack.handlebars.HandlebarsException; +import com.github.jknack.handlebars.Template; +import com.github.jknack.handlebars.jackson.Blog.Views.Public; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static com.github.jknack.handlebars.jackson.JacksonHelperTest.IgnoreWindowsLineMatcher.equalsToStringIgnoringWindowsNewLine; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Unit test for {@link JacksonHelper}. + * + * @author edgar.espina, jolarsen + * @since 4.5.5 + */ +class JacksonHelperTest { + + @Test + void toJSON() throws IOException { + Handlebars handlebars = new Handlebars(); + handlebars.registerHelper("@json", JacksonHelper.INSTANCE); + + Template template = handlebars.compileInline("{{@json this}}"); + + assertThat( + template.apply(new Blog("First Post", "...")), + equalsToStringIgnoringWindowsNewLine( + "{\"title\":\"First Post\",\"body\":\"...\",\"comments\":[]}")); + } + + @Test + void toPrettyJSON() throws IOException { + Handlebars handlebars = new Handlebars(); + handlebars.registerHelper("@json", JacksonHelper.INSTANCE); + + Template template = handlebars.compileInline("{{@json this pretty=true}}"); + + assertThat( + template.apply(new Blog("First Post", "...")), + equalsToStringIgnoringWindowsNewLine( + """ + { + "title" : "First Post", + "body" : "...", + "comments" : [ ] + }""")); + } + + @Test + void toJSONViewExclusive() throws IOException { + Handlebars handlebars = new Handlebars(); + + handlebars.registerHelper("@json", JacksonHelper.INSTANCE); + + Template template = + handlebars.compileInline( + "{{@json this view=\"com.github.jknack.handlebars.jackson.Blog$Views$Public\"}}"); + + assertThat( + template.apply(new Blog("First Post", "...")), + equalsToStringIgnoringWindowsNewLine("{\"title\":\"First Post\"}")); + } + + @Test + void toJSONAliasViewExclusive() throws IOException { + Handlebars handlebars = new Handlebars(); + + ObjectMapper mapper = JsonMapper.builder().build(); + + handlebars.registerHelper("@json", new JacksonHelper(mapper).viewAlias("myView", Public.class)); + + Template template = handlebars.compileInline("{{@json this view=\"myView\"}}"); + + assertThat( + template.apply(new Blog("First Post", "...")), + equalsToStringIgnoringWindowsNewLine("{\"title\":\"First Post\"}")); + } + + @Test + void jsonViewNotFound() { + Assertions.assertThrows( + HandlebarsException.class, + () -> { + Handlebars handlebars = new Handlebars(); + + ObjectMapper mapper = JsonMapper.builder().build(); + + handlebars.registerHelper("@json", new JacksonHelper(mapper)); + + Template template = handlebars.compileInline("{{@json this view=\"missing.ViewClass\"}}"); + + assertThat( + template.apply(new Blog("First Post", "...")), + equalsToStringIgnoringWindowsNewLine("{\"title\":\"First Post\"}")); + }); + } + + @Test + void escapeHtml() throws IOException { + Handlebars handlebars = new Handlebars(); + handlebars.registerHelper("@json", JacksonHelper.INSTANCE); + + Map model = new HashMap(); + model.put("script", ""); + + assertThat( + handlebars.compileInline("{{@json this}}").apply(model), + equalsToStringIgnoringWindowsNewLine( + "{\"script\":\"\"}")); + + assertThat( + handlebars.compileInline("{{@json this escapeHTML=true}}").apply(model), + equalsToStringIgnoringWindowsNewLine( + "{\"script\":\"\\u003Cscript" + + " text=\\\"text/javascript\\\"\\u003E\\u003C/script\\u003E\"}")); + } + + static class IgnoreWindowsLineMatcher extends TypeSafeMatcher { + + private final String value; + + private IgnoreWindowsLineMatcher(String value) { + this.value = value; + } + + static IgnoreWindowsLineMatcher equalsToStringIgnoringWindowsNewLine(String value) { + return new IgnoreWindowsLineMatcher(value); + } + + @Override + protected boolean matchesSafely(String item) { + return item.replace("\r\n", "\n").equals(value); + } + + @Override + public void describeTo(Description description) { + description + .appendText("a string ") + .appendText("ignoring \\r") + .appendText(" ") + .appendValue(value); + } + } +} diff --git a/handlebars-jackson3/src/test/java/com/github/jknack/handlebars/jackson/JsonNodeValueResolverTest.java b/handlebars-jackson3/src/test/java/com/github/jknack/handlebars/jackson/JsonNodeValueResolverTest.java new file mode 100644 index 00000000..d5adeb53 --- /dev/null +++ b/handlebars-jackson3/src/test/java/com/github/jknack/handlebars/jackson/JsonNodeValueResolverTest.java @@ -0,0 +1,231 @@ +/* + * Handlebars.java: https://github.com/jknack/handlebars.java + * Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 + * Copyright (c) 2012 Edgar Espina + */ +package com.github.jknack.handlebars.jackson; + +import com.github.jknack.handlebars.Context; +import com.github.jknack.handlebars.Handlebars; +import com.github.jknack.handlebars.ValueResolver; +import com.github.jknack.handlebars.context.MapValueResolver; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.BigIntegerNode; +import tools.jackson.databind.node.BinaryNode; +import tools.jackson.databind.node.DecimalNode; +import tools.jackson.databind.node.LongNode; +import tools.jackson.databind.node.NullNode; +import tools.jackson.databind.node.POJONode; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class JsonNodeValueResolverTest { + + @Test + void resolveValueNode() throws IOException { + Handlebars handlebars = new Handlebars(); + + Map root = new LinkedHashMap(); + root.put("string", "abc"); + root.put("int", 678); + root.put("long", 6789L); + root.put("float", 7.13f); + root.put("double", 3.14d); + root.put("bool", true); + + assertEquals( + "abc 678 6789 7.13 3.14 true", + handlebars + .compileInline("{{string}} {{int}} {{long}} {{float}} {{double}} {{bool}}") + .apply(context(root))); + } + + @Test + void nullMustBeResolvedToUnresolved() { + Assertions.assertEquals( + ValueResolver.UNRESOLVED, JsonNodeValueResolver.INSTANCE.resolve(null, "nothing")); + } + + @Test + void resolveBinaryNode() { + String name = "binary"; + byte[] result = new byte[] {1}; + + JsonNode node = mock(JsonNode.class); + BinaryNode value = BinaryNode.valueOf(result); + when(node.get(name)).thenReturn(value); + + Assertions.assertEquals(result, JsonNodeValueResolver.INSTANCE.resolve(node, name)); + + verify(node).get(name); + } + + @Test + void resolveNullNode() { + String name = "null"; + Object result = ValueResolver.UNRESOLVED; + + JsonNode node = mock(JsonNode.class); + NullNode value = NullNode.instance; + when(node.get(name)).thenReturn(value); + + Assertions.assertEquals(result, JsonNodeValueResolver.INSTANCE.resolve(node, name)); + + verify(node).get(name); + } + + @Test + void resolveBigIntegerNode() { + String name = "bigInt"; + BigInteger result = BigInteger.ONE; + + JsonNode node = mock(JsonNode.class); + JsonNode value = BigIntegerNode.valueOf(result); + when(node.get(name)).thenReturn(value); + + Assertions.assertEquals(result, JsonNodeValueResolver.INSTANCE.resolve(node, name)); + + verify(node).get(name); + } + + @Test + void resolveDecimalNode() { + String name = "decimal"; + BigDecimal result = BigDecimal.TEN; + + JsonNode node = mock(JsonNode.class); + JsonNode value = DecimalNode.valueOf(result); + when(node.get(name)).thenReturn(value); + + Assertions.assertEquals(result, JsonNodeValueResolver.INSTANCE.resolve(node, name)); + + verify(node).get(name); + } + + @Test + void resolveLongNode() { + String name = "long"; + long result = 678L; + + JsonNode node = mock(JsonNode.class); + JsonNode value = LongNode.valueOf(result); + when(node.get(name)).thenReturn(value); + + Assertions.assertEquals(result, JsonNodeValueResolver.INSTANCE.resolve(node, name)); + + verify(node).get(name); + } + + @Test + void resolvePojoNode() { + String name = "pojo"; + Object result = new Object(); + + JsonNode node = mock(JsonNode.class); + JsonNode value = new POJONode(result); + when(node.get(name)).thenReturn(value); + + Assertions.assertEquals(result, JsonNodeValueResolver.INSTANCE.resolve(node, name)); + + verify(node).get(name); + } + + @Test + void propertySet() { + Map root = new LinkedHashMap(); + root.put("string", "abc"); + root.put("int", 678); + root.put("double", 3.14d); + root.put("bool", true); + + Assertions.assertEquals( + root.entrySet(), JsonNodeValueResolver.INSTANCE.propertySet(node(root))); + } + + @Test + void emptyPropertySet() { + Set> propertySet = + JsonNodeValueResolver.INSTANCE.propertySet(new Object()); + assertNotNull(propertySet); + assertEquals(0, propertySet.size()); + } + + @Test + void resolveObjectNode() throws IOException { + Handlebars handlebars = new Handlebars(); + Object item = + new Object() { + @SuppressWarnings("unused") + public String getKey() { + return "pojo"; + } + }; + + Map root = new HashMap(); + root.put("pojo", item); + + assertEquals("pojo", handlebars.compileInline("{{pojo.key}}").apply(context(root))); + } + + @Test + void resolveSimpleArrayNode() throws IOException { + Handlebars handlebars = new Handlebars(); + + Map root = new HashMap(); + root.put("array", new Object[] {1, 2, 3}); + + assertEquals( + "123", + handlebars.compileInline("{{array.[0]}}{{array.[1]}}{{array.[2]}}").apply(context(root))); + assertEquals( + "123", handlebars.compileInline("{{#array}}{{this}}{{/array}}").apply(context(root))); + } + + @Test + void resolveArrayNode() throws IOException { + Handlebars handlebars = new Handlebars(); + + Object item = + new Object() { + @SuppressWarnings("unused") + public String getKey() { + return "pojo"; + } + }; + + Map root = new HashMap(); + root.put("array", new Object[] {item}); + + assertEquals("pojo", handlebars.compileInline("{{array.[0].key}}").apply(context(root))); + assertEquals( + "pojo", handlebars.compileInline("{{#array}}{{key}}{{/array}}").apply(context(root))); + } + + public static JsonNode node(final Object object) { + JsonMapper mapper = JsonMapper.builder().build(); + JsonNode node = mapper.readTree(mapper.writeValueAsString(object)); + return node; + } + + public static Context context(final Object object) { + return Context.newBuilder(node(object)) + .resolver(MapValueResolver.INSTANCE, JsonNodeValueResolver.INSTANCE) + .build(); + } +} From 944ad982853f5da526392d87da6dc44a395423a6 Mon Sep 17 00:00:00 2001 From: Jens-Otto Larsen Date: Sun, 16 Aug 2026 21:01:54 +0200 Subject: [PATCH 2/2] Module for Jackson 3, root --- pom.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pom.xml b/pom.xml index 406e7714..6ddd598f 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ handlebars-helpers handlebars-springmvc handlebars-jackson + handlebars-jackson3 handlebars-guava-cache handlebars-caffeine handlebars-maven-plugin @@ -107,6 +108,13 @@ ${jackson2-version} + + + tools.jackson.core + jackson-databind + ${jackson3-version} + + ch.qos.logback @@ -493,6 +501,7 @@ UTF-8 2.22.1 + 3.2.1 6.2.9 0.8.12 4.13.2