diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index c50069a24c..01258edbc7 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -34,6 +34,8 @@ Project: jackson-databind (requested by @nathanukey) #4849 Not able to deserialize Enum with default typing after upgrading 2.15.4 -> 2.17.1 (reported by Kornel Zemla) +#4863: Add basic Stream support in `JsonNode`: `valueStream()`, `entryStream()`, + `forEachEntry()` 2.18.3 (not yet released) diff --git a/src/main/java/tools/jackson/databind/JsonNode.java b/src/main/java/tools/jackson/databind/JsonNode.java index 85116aac33..081ba18f90 100644 --- a/src/main/java/tools/jackson/databind/JsonNode.java +++ b/src/main/java/tools/jackson/databind/JsonNode.java @@ -3,6 +3,8 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.util.*; +import java.util.function.BiConsumer; +import java.util.stream.Stream; import tools.jackson.core.*; import tools.jackson.databind.exc.JsonNodeException; @@ -1004,6 +1006,44 @@ public Set> properties() { return Collections.emptySet(); } + /** + * Returns a stream of all value nodes of this Node, iff + * this node is an {@code ArrayNode} or {@code ObjectNode}. + * In case of {@code Object} node, property names (keys) are not included, only values. + * For other types of nodes, returns empty stream. + * + * @since 2.19 + */ + public Stream valueStream() { + return ClassUtil.emptyStream(); + } + + /** + * Returns a stream of all value nodes of this Node, iff + * this node is an an {@code ObjectNode}. + * For other types of nodes, returns empty stream. + * + * @since 2.19 + */ + public Stream> entryStream() { + return ClassUtil.emptyStream(); + } + + /** + * If this node is an {@code ObjectNode}, erforms the given action for each entry + * until all entries have been processed or the action throws an exception. + * Exceptions thrown by the action are relayed to the caller. + * For other node types, no action is performed. + *

+ * Actions are performed in the order of entries, same as order returned by + * method {@link #properties()}. + * + * @param action Action to perform for each entry + */ + public void forEachEntry(BiConsumer action) { + // No-op for all but ObjectNode + } + /* /********************************************************************** /* Public API, find methods diff --git a/src/main/java/tools/jackson/databind/node/ArrayNode.java b/src/main/java/tools/jackson/databind/node/ArrayNode.java index 14f600e469..7c90160d54 100644 --- a/src/main/java/tools/jackson/databind/node/ArrayNode.java +++ b/src/main/java/tools/jackson/databind/node/ArrayNode.java @@ -3,6 +3,7 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.util.*; +import java.util.stream.Stream; import tools.jackson.core.*; import tools.jackson.core.tree.ArrayTreeNode; @@ -259,6 +260,11 @@ public JsonNode required(int index) { index, _children.size()); } + @Override // @since 2.19 + public Stream valueStream() { + return _children.stream(); + } + @Override public boolean equals(Comparator comparator, JsonNode o) { diff --git a/src/main/java/tools/jackson/databind/node/ContainerNode.java b/src/main/java/tools/jackson/databind/node/ContainerNode.java index 39d23ed0ad..edfd74ca8a 100644 --- a/src/main/java/tools/jackson/databind/node/ContainerNode.java +++ b/src/main/java/tools/jackson/databind/node/ContainerNode.java @@ -2,6 +2,7 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.util.stream.Stream; import tools.jackson.core.*; import tools.jackson.databind.JsonNode; @@ -54,6 +55,10 @@ protected ContainerNode(JsonNodeFactory nc) { @Override public abstract JsonNode get(String fieldName); + // Both ArrayNode and ObjectNode must re-implement + @Override // @since 2.19 + public abstract Stream valueStream(); + @Override protected abstract ObjectNode _withObject(JsonPointer origPtr, JsonPointer currentPtr, diff --git a/src/main/java/tools/jackson/databind/node/ObjectNode.java b/src/main/java/tools/jackson/databind/node/ObjectNode.java index a8b15d7827..81967d2afd 100644 --- a/src/main/java/tools/jackson/databind/node/ObjectNode.java +++ b/src/main/java/tools/jackson/databind/node/ObjectNode.java @@ -3,6 +3,8 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.util.*; +import java.util.function.BiConsumer; +import java.util.stream.Stream; import tools.jackson.core.*; import tools.jackson.core.tree.ObjectTreeNode; @@ -289,7 +291,22 @@ public Iterator> fields() { public Set> properties() { return _children.entrySet(); } - + + @Override // @since 2.19 + public Stream valueStream() { + return _children.values().stream(); + } + + @Override // @since 2.19 + public Stream> entryStream() { + return _children.entrySet().stream(); + } + + @Override // @since 2.19 + public void forEachEntry(BiConsumer action) { + _children.forEach(action); + } + @Override public boolean equals(Comparator comparator, JsonNode o) { diff --git a/src/main/java/tools/jackson/databind/util/ClassUtil.java b/src/main/java/tools/jackson/databind/util/ClassUtil.java index 1d1255a128..604c5cb594 100644 --- a/src/main/java/tools/jackson/databind/util/ClassUtil.java +++ b/src/main/java/tools/jackson/databind/util/ClassUtil.java @@ -5,6 +5,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.*; import java.util.*; +import java.util.stream.Stream; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonGenerator; @@ -21,7 +22,7 @@ public final class ClassUtil private final static Ctor[] NO_CTORS = new Ctor[0]; - private final static Iterator EMPTY_ITERATOR = Collections.emptyIterator(); + private final static Iterator EMPTY_ITERATOR = Collections.emptyIterator(); /* /********************************************************************** @@ -34,6 +35,16 @@ public static Iterator emptyIterator() { return (Iterator) EMPTY_ITERATOR; } + /** + * @since 2.19 + */ + public static Stream emptyStream() { + // Looking at its implementation, seems there ought to be simpler/more + // efficient way to create and return a shared singleton but... no luck + // so far. So just use this for convenience for now: + return Stream.empty(); + } + /* /********************************************************************** /* Methods that deal with inheritance diff --git a/src/test/java/tools/jackson/databind/node/ArrayNodeTest.java b/src/test/java/tools/jackson/databind/node/ArrayNodeTest.java index e0e2cdc12a..f152a430b7 100644 --- a/src/test/java/tools/jackson/databind/node/ArrayNodeTest.java +++ b/src/test/java/tools/jackson/databind/node/ArrayNodeTest.java @@ -1,8 +1,9 @@ package tools.jackson.databind.node; -import java.io.*; import java.math.BigDecimal; import java.math.BigInteger; +import java.util.Arrays; +import java.util.stream.Collectors; import java.util.ArrayList; import org.junit.jupiter.api.Test; @@ -13,8 +14,6 @@ import tools.jackson.databind.testutil.DatabindTestUtil; import tools.jackson.databind.util.RawValue; -import static java.util.Arrays.asList; - import static org.junit.jupiter.api.Assertions.*; /** @@ -24,7 +23,7 @@ public class ArrayNodeTest extends DatabindTestUtil { @Test - public void testDirectCreation() throws IOException + public void testDirectCreation() throws Exception { ArrayNode n = new ArrayNode(JsonNodeFactory.instance); @@ -108,7 +107,7 @@ public void testDirectCreation() throws IOException } @Test - public void testDirectCreation2() throws IOException + public void testDirectCreation2() throws Exception { JsonNodeFactory f = objectMapper().getNodeFactory(); ArrayList list = new ArrayList<>(); @@ -139,7 +138,7 @@ public void testDirectCreation2() throws IOException } @Test - public void testArraySet() throws IOException { + public void testArraySet() throws Exception { final ArrayNode array = JsonNodeFactory.instance.arrayNode(); for (int i = 0; i < 20; i++) { array.add("Original Data"); @@ -267,7 +266,7 @@ public void testAddAllWithNullInCollection() final ArrayNode array = JsonNodeFactory.instance.arrayNode(); // test - array.addAll(asList(null, JsonNodeFactory.instance.objectNode())); + array.addAll(Arrays.asList(null, JsonNodeFactory.instance.objectNode())); // assertions assertEquals(2, array.size()); @@ -478,4 +477,28 @@ public void testSimpleMismatch() throws Exception verifyException(e, "from Integer value (token `JsonToken.VALUE_NUMBER_INT`)"); } } + + // [databind#4863]: valueStream(), entryStream(), forEachEntry() + @Test + public void testStreamMethods() + { + ObjectMapper mapper = objectMapper(); + ArrayNode arr = mapper.createArrayNode(); + arr.add(1).add("foo"); + JsonNode n1 = arr.get(0); + JsonNode n2 = arr.get(1); + + // First, valueStream() testing + assertEquals(2, arr.valueStream().count()); + assertEquals(Arrays.asList(n1, n2), + arr.valueStream().collect(Collectors.toList())); + + // And then entryStream() (empty) + assertEquals(0, arr.entryStream().count()); + assertEquals(Arrays.asList(), + arr.entryStream().collect(Collectors.toList())); + + // And then empty forEachEntry() + arr.forEachEntry((k, v) -> { throw new UnsupportedOperationException(); }); + } } diff --git a/src/test/java/tools/jackson/databind/node/JsonNodeBasicTest.java b/src/test/java/tools/jackson/databind/node/JsonNodeBasicTest.java index 0e937b5bd5..b3c715d015 100644 --- a/src/test/java/tools/jackson/databind/node/JsonNodeBasicTest.java +++ b/src/test/java/tools/jackson/databind/node/JsonNodeBasicTest.java @@ -65,6 +65,8 @@ public void testBoolean() throws Exception // also, equality should work ok assertEquals(result, BooleanNode.valueOf(true)); assertEquals(result, BooleanNode.getTrue()); + + assertNonContainerStreamMethods(f); } @Test @@ -92,6 +94,8 @@ public void testBinary() throws Exception assertEquals("AAMD", new BinaryNode(data).asText()); assertNodeNumbersForNonNumeric(n); + + assertNonContainerStreamMethods(n2); } @Test @@ -110,6 +114,8 @@ public void testPOJO() assertNodeNumbersForNonNumeric(n); // but if wrapping actual number, use it assertNodeNumbers(new POJONode(Integer.valueOf(123)), 123, 123.0); + + assertNonContainerStreamMethods(n); } // [databind#743] diff --git a/src/test/java/tools/jackson/databind/node/NodeTestBase.java b/src/test/java/tools/jackson/databind/node/NodeTestBase.java index 87932ee974..bba7d2e211 100644 --- a/src/test/java/tools/jackson/databind/node/NodeTestBase.java +++ b/src/test/java/tools/jackson/databind/node/NodeTestBase.java @@ -34,4 +34,16 @@ protected void assertNodeNumbers(JsonNode n, int expInt, double expDouble) assertTrue(n.isEmpty()); } + + // Testing for non-ContainerNode (ValueNode) stream method implementations + // + // @since 2.19 + protected void assertNonContainerStreamMethods(ValueNode n) + { + assertEquals(0, n.valueStream().count()); + assertEquals(0, n.entryStream().count()); + + // And then empty forEachEntry() + n.forEachEntry((k, v) -> { throw new UnsupportedOperationException(); }); + } } diff --git a/src/test/java/tools/jackson/databind/node/NullNodeTest.java b/src/test/java/tools/jackson/databind/node/NullNodeTest.java index f9aeebc1fa..e4c9cc9c51 100644 --- a/src/test/java/tools/jackson/databind/node/NullNodeTest.java +++ b/src/test/java/tools/jackson/databind/node/NullNodeTest.java @@ -70,8 +70,9 @@ public void testBasicsWithNullNode() throws Exception assertNodeNumbersForNonNumeric(n); - // 2.4 assertEquals("null", n.asText()); + + assertNonContainerStreamMethods(n); } @Test diff --git a/src/test/java/tools/jackson/databind/node/NumberNodesTest.java b/src/test/java/tools/jackson/databind/node/NumberNodesTest.java index a0afebba43..c3a5cd0a1c 100644 --- a/src/test/java/tools/jackson/databind/node/NumberNodesTest.java +++ b/src/test/java/tools/jackson/databind/node/NumberNodesTest.java @@ -45,6 +45,8 @@ public void testShort() assertTrue(ShortNode.valueOf((short) 0).canConvertToLong()); assertTrue(ShortNode.valueOf(Short.MAX_VALUE).canConvertToLong()); assertTrue(ShortNode.valueOf(Short.MIN_VALUE).canConvertToLong()); + + assertNonContainerStreamMethods(n); } @Test @@ -103,6 +105,7 @@ public void testInt() assertTrue(IntNode.valueOf(Integer.MAX_VALUE).canConvertToLong()); assertTrue(IntNode.valueOf(Integer.MIN_VALUE).canConvertToLong()); + assertNonContainerStreamMethods(n); } @Test @@ -132,6 +135,8 @@ public void testLong() assertTrue(LongNode.valueOf(0L).canConvertToLong()); assertTrue(LongNode.valueOf(Long.MAX_VALUE).canConvertToLong()); assertTrue(LongNode.valueOf(Long.MIN_VALUE).canConvertToLong()); + + assertNonContainerStreamMethods(n); } @Test @@ -195,6 +200,8 @@ public void testDouble() throws Exception n = (DoubleNode) num; assertEquals(-0.0, n.doubleValue()); assertEquals("-0.0", String.valueOf(n.doubleValue())); + + assertNonContainerStreamMethods(n); } @Test @@ -266,6 +273,8 @@ public void testFloat() assertTrue(FloatNode.valueOf(0L).canConvertToLong()); assertTrue(FloatNode.valueOf(Integer.MAX_VALUE).canConvertToLong()); assertTrue(FloatNode.valueOf(Integer.MIN_VALUE).canConvertToLong()); + + assertNonContainerStreamMethods(n); } @Test @@ -324,6 +333,8 @@ public void testDecimalNode() throws Exception // also, equality should work ok assertEquals(result, DecimalNode.valueOf(value)); + + assertNonContainerStreamMethods(n); } @Test @@ -389,6 +400,8 @@ public void testBigIntegerNode() throws Exception assertTrue(BigIntegerNode.valueOf(BigInteger.ZERO).canConvertToLong()); assertTrue(BigIntegerNode.valueOf(BigInteger.valueOf(Long.MAX_VALUE)).canConvertToLong()); assertTrue(BigIntegerNode.valueOf(BigInteger.valueOf(Long.MIN_VALUE)).canConvertToLong()); + + assertNonContainerStreamMethods(n); } @Test diff --git a/src/test/java/tools/jackson/databind/node/ObjectNodeTest.java b/src/test/java/tools/jackson/databind/node/ObjectNodeTest.java index 7e86375f0f..0eefd2cfa9 100644 --- a/src/test/java/tools/jackson/databind/node/ObjectNodeTest.java +++ b/src/test/java/tools/jackson/databind/node/ObjectNodeTest.java @@ -536,6 +536,34 @@ public void testPropertiesTraversal() throws Exception assertEquals("a/1,b/true,c/\"stuff\"", _toString(n)); } + // [databind#4863]: valueStream(), entryStream(), forEachEntry() + @Test + public void testStreamMethods() + { + ObjectMapper mapper = objectMapper(); + ObjectNode obj = mapper.createObjectNode(); + JsonNode n1 = obj.numberNode(42); + JsonNode n2 = obj.textNode("foo"); + + obj.set("a", n1); + obj.set("b", n2); + + // First, valueStream() testing + assertEquals(2, obj.valueStream().count()); + assertEquals(Arrays.asList(n1, n2), + obj.valueStream().collect(Collectors.toList())); + + // And then entryStream() (empty) + assertEquals(2, obj.entryStream().count()); + assertEquals(new ArrayList<>(obj.properties()), + obj.entryStream().collect(Collectors.toList())); + + // And then empty forEachEntry() + final LinkedHashMap map = new LinkedHashMap<>(); + obj.forEachEntry((k, v) -> { map.put(k, v); }); + assertEquals(obj.properties(), map.entrySet()); + } + private String _toString(JsonNode n) { return n.properties().stream() .map(e -> e.getKey() + "/" + e.getValue()) diff --git a/src/test/java/tools/jackson/databind/node/TextNodeTest.java b/src/test/java/tools/jackson/databind/node/TextNodeTest.java index 17f798d60d..4a7a8f3357 100644 --- a/src/test/java/tools/jackson/databind/node/TextNodeTest.java +++ b/src/test/java/tools/jackson/databind/node/TextNodeTest.java @@ -40,6 +40,8 @@ public void testText() assertTrue(TextNode.valueOf("true").asBoolean(false)); assertFalse(TextNode.valueOf("false").asBoolean(true)); assertFalse(TextNode.valueOf("false").asBoolean(false)); + + assertNonContainerStreamMethods(n); } @Test