From 881bd24c9bb768418293d2278e24678ca49dab4c Mon Sep 17 00:00:00 2001 From: Brian Caruso Date: Sat, 9 Apr 2022 23:26:37 -0700 Subject: [PATCH 1/2] Unwrap Jackson2 ArrayNode in JsonNodeValueResolver (#964) --- .../handlebars/JsonNodeValueResolver.java | 26 ++++++- .../github/jknack/handlebars/Issue964.java | 67 +++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 handlebars-jackson2/src/test/java/com/github/jknack/handlebars/Issue964.java diff --git a/handlebars-jackson2/src/main/java/com/github/jknack/handlebars/JsonNodeValueResolver.java b/handlebars-jackson2/src/main/java/com/github/jknack/handlebars/JsonNodeValueResolver.java index 255b98da5..82cfe5b8a 100644 --- a/handlebars-jackson2/src/main/java/com/github/jknack/handlebars/JsonNodeValueResolver.java +++ b/handlebars-jackson2/src/main/java/com/github/jknack/handlebars/JsonNodeValueResolver.java @@ -17,11 +17,13 @@ */ package com.github.jknack.handlebars; +import java.util.AbstractList; import java.util.AbstractMap; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -84,7 +86,7 @@ public Object resolve(final Object context) { * 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. + * @return A primitive value, json object as a map, json array as a list, or null. */ private static Object resolve(final JsonNode node) { // binary node @@ -127,7 +129,12 @@ private static Object resolve(final JsonNode node) { if (node instanceof ObjectNode) { return toMap((ObjectNode) node); } - // container, array or null + // array node to list + if (node instanceof ArrayNode) { + return toList((ArrayNode) node); + } + + // container or literal null return node; } @@ -160,6 +167,21 @@ public Set> entrySet() { }; } + private static List toList(final ArrayNode node) { + return new AbstractList() { + + @Override + public Object get(int index) { + return resolve(node.get(index)); + } + + @Override + public int size() { + return node.size(); + } + }; + } + @Override public Set> propertySet(final Object context) { if (context instanceof ObjectNode) { diff --git a/handlebars-jackson2/src/test/java/com/github/jknack/handlebars/Issue964.java b/handlebars-jackson2/src/test/java/com/github/jknack/handlebars/Issue964.java new file mode 100644 index 000000000..dc9d021c4 --- /dev/null +++ b/handlebars-jackson2/src/test/java/com/github/jknack/handlebars/Issue964.java @@ -0,0 +1,67 @@ +package com.github.jknack.handlebars; + +import java.io.IOException; +import java.util.Iterator; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.jknack.handlebars.context.MapValueResolver; +import com.github.jknack.handlebars.helper.StringHelpers; + +import org.junit.Test; + +public class Issue964 extends AbstractTest { + + @Override + protected Object configureContext(final Object model) { + return Context.newBuilder(model) + .resolver(MapValueResolver.INSTANCE, JsonNodeValueResolver.INSTANCE) + .build(); + } + + @Test + public void shouldUnwrapJsonArraysAsIterables() throws IOException { + Hash helpers = $("join", StringHelpers.join); + JsonNode tree = new ObjectMapper().readTree("{\"pets\":[\"cat\",\"dog\",\"bird\"]}"); + shouldCompileTo("{{join this.pets \", \"}}", tree, helpers, "cat, dog, bird"); + } + + @Test + public void shouldUnwrapJsonArraysByIndex() throws IOException { + Hash helpers = $("join", StringHelpers.join); + JsonNode tree = new ObjectMapper().readTree("{\"pets\":[\"cat\",\"dog\",\"bird\"]}"); + shouldCompileTo("{{join this.pets.[0] this.pets.[1] this.pets.[2] \", \"}}", tree, helpers, "cat, dog, bird"); + } + + @Test + public void shouldUnwrapJsonArraysRecursively() throws IOException { + Hash helpers = $("elementAt", new ElementAtHelper(), "capitalize", StringHelpers.capitalize); + JsonNode tree = new ObjectMapper().readTree("{\"kidsPets\":[[\"cat\",\"dog\"],[\"bird\",\"mouse\"]]}"); + shouldCompileTo("{{capitalize (elementAt (elementAt this.kidsPets 0) 1)}}", tree, helpers, "Dog"); + } + + private static class ElementAtHelper implements Helper> { + + @Override + public Object apply(Iterable context, Options options) throws IOException { + int targetIndex = options.param(0); + int currentIndex = 0; + + Iterator loop = context.iterator(); + + while (loop.hasNext()) { + Object it = loop.next(); + if (currentIndex++ == targetIndex) { + return it; + } + } + + throw new IOException( + "Cannot get element at " + targetIndex + ". " + + "Iterable only has " + currentIndex + " elements." + ); + } + + } + +} From 0b94900d986478b063c53f8449971d689851bc24 Mon Sep 17 00:00:00 2001 From: Brian Caruso Date: Thu, 21 Apr 2022 12:37:58 -0700 Subject: [PATCH 2/2] Recursively resolve JSON Object nodes during entry iteration (#969) --- .../handlebars/JsonNodeValueResolver.java | 22 ++++++++++++- .../github/jknack/handlebars/Issue969.java | 33 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 handlebars-jackson2/src/test/java/com/github/jknack/handlebars/Issue969.java diff --git a/handlebars-jackson2/src/main/java/com/github/jknack/handlebars/JsonNodeValueResolver.java b/handlebars-jackson2/src/main/java/com/github/jknack/handlebars/JsonNodeValueResolver.java index 82cfe5b8a..e0e566773 100644 --- a/handlebars-jackson2/src/main/java/com/github/jknack/handlebars/JsonNodeValueResolver.java +++ b/handlebars-jackson2/src/main/java/com/github/jknack/handlebars/JsonNodeValueResolver.java @@ -160,7 +160,27 @@ public Set> entrySet() { Iterator> it = node.fields(); Set set = new LinkedHashSet(); while (it.hasNext()) { - set.add(it.next()); + Map.Entry current = it.next(); + + set.add( + new Map.Entry() { + + @Override + public String getKey() { + return current.getKey(); + } + + @Override + public Object getValue() { + return resolve(current.getValue()); + } + + @Override + public Object setValue(Object value) { + throw new UnsupportedOperationException(); + } + } + ); } return set; } diff --git a/handlebars-jackson2/src/test/java/com/github/jknack/handlebars/Issue969.java b/handlebars-jackson2/src/test/java/com/github/jknack/handlebars/Issue969.java new file mode 100644 index 000000000..33a4d0823 --- /dev/null +++ b/handlebars-jackson2/src/test/java/com/github/jknack/handlebars/Issue969.java @@ -0,0 +1,33 @@ +package com.github.jknack.handlebars; + +import java.io.IOException; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.jknack.handlebars.context.MapValueResolver; +import com.github.jknack.handlebars.helper.StringHelpers; + +import org.junit.Test; + +public class Issue969 extends AbstractTest { + + @Override + protected Object configureContext(final Object model) { + return Context.newBuilder(model) + .resolver(MapValueResolver.INSTANCE, JsonNodeValueResolver.INSTANCE) + .build(); + } + + @Override + protected Handlebars newHandlebars() { + return super.newHandlebars().with(EscapingStrategy.NOOP); + } + + @Test + public void shouldRecursivelyResolveEntries() throws IOException { + Hash helpers = $("join", StringHelpers.join); + JsonNode tree = new ObjectMapper().readTree("{\"pets\":[{\"type\":\"cat\",\"name\":\"alice\"},{\"type\":\"bird\",\"name\":\"bob\"}]}"); + shouldCompileTo("{{join this.pets \", \"}}", tree, helpers, "{type=cat, name=alice}, {type=bird, name=bob}"); + } + +}