From 85aead337f9063fdfcd41ee230765e89ea99a7cf Mon Sep 17 00:00:00 2001 From: Paul Martin Date: Mon, 6 Mar 2017 23:22:22 +0000 Subject: [PATCH] INT-4241: Wrap ArrayNodes with ArrayNodeAsList JIRA: https://jira.spring.io/browse/INT-4241 To allow `ArrayNode` to be treated as `List` in SpEL expressions, wrap them to the internal `ArrayNodeAsList` implementation Fix Checkstyle errors * Add `iterator()` support to the `ArrayNodeAsList` --- .../json/JsonPropertyAccessor.java | 108 +++++++++++++++--- .../json/JsonPropertyAccessorTests.java | 60 +++++++++- 2 files changed, 150 insertions(+), 18 deletions(-) diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonPropertyAccessor.java b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonPropertyAccessor.java index d44c7d8b49..d7c9fb5fc5 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonPropertyAccessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonPropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2016 the original author or authors. + * Copyright 2013-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,8 @@ package org.springframework.integration.json; import java.io.IOException; +import java.util.AbstractList; +import java.util.Iterator; import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; @@ -36,6 +38,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; * * @author Eric Bottard * @author Artem Bilan + * @author Paul Martin * @since 3.0 */ public class JsonPropertyAccessor implements PropertyAccessor { @@ -44,7 +47,7 @@ public class JsonPropertyAccessor implements PropertyAccessor { * The kind of types this can work with. */ private static final Class[] SUPPORTED_CLASSES = new Class[] { String.class, ToStringFriendlyJsonNode.class, - ObjectNode.class, ArrayNode.class }; + ArrayNodeAsList.class, ObjectNode.class, ArrayNode.class }; // Note: ObjectMapper is thread-safe private ObjectMapper objectMapper = new ObjectMapper(); @@ -58,7 +61,12 @@ public class JsonPropertyAccessor implements PropertyAccessor { public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { ContainerNode container = asJson(target); Integer index = maybeIndex(name); - return ((index != null && container.has(index)) || container.has(name)); + if (container instanceof ArrayNode) { + return index != null; + } + else { + return ((index != null && container.has(index)) || container.has(name)); + } } private ContainerNode assertContainerNode(JsonNode json) throws AccessException { @@ -79,6 +87,10 @@ public class JsonPropertyAccessor implements PropertyAccessor { ToStringFriendlyJsonNode wrapper = (ToStringFriendlyJsonNode) target; return assertContainerNode(wrapper.node); } + else if (target instanceof ArrayNodeAsList) { + ArrayNodeAsList wrapper = (ArrayNodeAsList) target; + return assertContainerNode(wrapper.node); + } else if (target instanceof String) { try { JsonNode json = this.objectMapper.readTree((String) target); @@ -113,16 +125,10 @@ public class JsonPropertyAccessor implements PropertyAccessor { ContainerNode container = asJson(target); Integer index = maybeIndex(name); if (index != null && container.has(index)) { - return new TypedValue(wrap(container.get(index))); + return typedValue(container.get(index)); } else { - JsonNode json = container.get(name); - if (json != null) { - return new TypedValue(wrap(json)); - } - else { - return TypedValue.NULL; - } + return typedValue(container.get(name)); } } @@ -141,15 +147,43 @@ public class JsonPropertyAccessor implements PropertyAccessor { this.objectMapper = objectMapper; } - private ToStringFriendlyJsonNode wrap(JsonNode json) { - return new ToStringFriendlyJsonNode(json); + private static TypedValue typedValue(JsonNode json) { + if (json == null) { + return TypedValue.NULL; + } + else { + return new TypedValue(wrap(json)); + } } - public static class ToStringFriendlyJsonNode { + public static WrappedJsonNode wrap(JsonNode json) { + if (json == null) { + return null; + } + else if (json instanceof ArrayNode) { + return new ArrayNodeAsList((ArrayNode) json); + } + else { + return new ToStringFriendlyJsonNode(json); + } + } + + /** + * The base interface for wrapped {@link JsonNode}. + * @since 5.0 + */ + public interface WrappedJsonNode { + + } + + /** + * A {@link WrappedJsonNode} implementation to represent {@link JsonNode} as string. + */ + public static class ToStringFriendlyJsonNode implements WrappedJsonNode { private final JsonNode node; - public ToStringFriendlyJsonNode(JsonNode node) { + ToStringFriendlyJsonNode(JsonNode node) { this.node = node; } @@ -186,4 +220,48 @@ public class JsonPropertyAccessor implements PropertyAccessor { } + /** + * An {@link AbstractList} implementation around {@link ArrayNode} with {@link WrappedJsonNode} aspect. + * @since 5.0 + */ + public static class ArrayNodeAsList extends AbstractList implements WrappedJsonNode { + + private final ArrayNode node; + + ArrayNodeAsList(ArrayNode node) { + this.node = node; + } + + @Override + public WrappedJsonNode get(int index) { + return wrap(this.node.get(index)); + } + + @Override + public int size() { + return this.node.size(); + } + + @Override + public Iterator iterator() { + + return new Iterator() { + + private final Iterator delegate = ArrayNodeAsList.this.node.iterator(); + + @Override + public boolean hasNext() { + return this.delegate.hasNext(); + } + + @Override + public WrappedJsonNode next() { + return wrap(this.delegate.next()); + } + + }; + } + + } + } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPropertyAccessorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPropertyAccessorTests.java index 79d603f30e..9e3a858376 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPropertyAccessorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPropertyAccessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import java.util.List; + import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Test; @@ -30,13 +32,16 @@ import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; /** * Tests for {@link JsonPropertyAccessor}. * * @author Eric Bottard * @author Artem Bilan + * @author Paul Martin * @since 3.0 */ public class JsonPropertyAccessorTests { @@ -79,8 +84,28 @@ public class JsonPropertyAccessorTests { @Test public void testArrayLookup() throws Exception { - Object json = mapper.readTree("[3, 4, 5]"); + ArrayNode json = (ArrayNode) mapper.readTree("[3, 4, 5]"); // JsonNode actual = evaluate("1", json, JsonNode.class); // Does not work + // Have to wrap the root array because ArrayNode itself is not a container + Object actual = evaluate(JsonPropertyAccessor.wrap(json), "[1]", Object.class); + assertEquals("4", actual.toString()); + } + + @Test + public void testArrayNegativeIndex() throws Exception { + JsonNode json = mapper.readTree("{\"foo\":[3, 4, 5]}"); + assertNull(evaluate(json, "foo[-1]", Object.class)); + } + + @Test(expected = SpelEvaluationException.class) + public void testArrayIndexOutOfBounds() throws Exception { + JsonNode json = mapper.readTree("{\"foo\":[3, 4, 5]}"); + evaluate(json, "foo[3]", Object.class); + } + + @Test + public void testArrayLookupWithStringIndex() throws Exception { + Object json = mapper.readTree("[3, 4, 5]"); // JsonNode actual = evaluate("'1'", json, JsonNode.class); // Does not work Object actual = evaluate(json, "['1']", Object.class); assertEquals("4", actual.toString()); @@ -88,13 +113,42 @@ public class JsonPropertyAccessorTests { @Test public void testNestedArrayConstruct() throws Exception { + ArrayNode json = (ArrayNode) mapper.readTree("[[3], [4, 5], []]"); + // JsonNode actual = evaluate("1.1", json, JsonNode.class); // Does not work + Object actual = evaluate(JsonPropertyAccessor.wrap(json), "[1][1]", Object.class); + assertEquals("5", actual.toString()); + } + + @Test + public void testNestedArrayConstructWithStringIndex() throws Exception { Object json = mapper.readTree("[[3], [4, 5], []]"); // JsonNode actual = evaluate("1.1", json, JsonNode.class); // Does not work - // JsonNode actual = evaluate("[1][1]", json, JsonNode.class); // Does not work Object actual = evaluate(json, "['1']['1']", Object.class); assertEquals("5", actual.toString()); } + @Test + public void testArrayResult() throws Exception { + Object json = mapper.readTree( + "{\"foo\": {\"bar\": [ { \"fizz\": 5, \"buzz\": 6 }, {\"fizz\": 7}, {\"fizz\": 8} ] } }"); + // Filter the bar array to return only the fizz value of each element (to prove that SPeL considers bar + // an array/list) + List actualArray = evaluate(json, "foo.bar.![fizz]", List.class); + assertEquals("Array size", 3, actualArray.size()); + assertEquals("[0]", "5", evaluate(actualArray, "[0]", Object.class).toString()); + assertEquals("[1]", "7", evaluate(actualArray, "[1]", Object.class).toString()); + assertEquals("[2]", "8", evaluate(actualArray, "[2]", Object.class).toString()); + } + + @Test + public void testEmptyArrayResult() throws Exception { + Object json = mapper.readTree( + "{\"foo\": {\"bar\": [ { \"fizz\": 5, \"buzz\": 6 }, {\"fizz\": 7}, {\"fizz\": 8} ] } }"); + // Filter bar objects so that none match + List actualArray = evaluate(json, "foo.bar.?[fizz=='0']", List.class); + assertEquals(0, actualArray.size()); + } + @Test public void testNestedHashConstruct() throws Exception { Object json = mapper.readTree("{\"foo\": {\"bar\": 4, \"fizz\": 5} }");