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`
This commit is contained in:
@@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -17,6 +17,8 @@
|
|||||||
package org.springframework.integration.json;
|
package org.springframework.integration.json;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.AbstractList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
import org.springframework.expression.AccessException;
|
import org.springframework.expression.AccessException;
|
||||||
import org.springframework.expression.EvaluationContext;
|
import org.springframework.expression.EvaluationContext;
|
||||||
@@ -36,6 +38,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
|||||||
*
|
*
|
||||||
* @author Eric Bottard
|
* @author Eric Bottard
|
||||||
* @author Artem Bilan
|
* @author Artem Bilan
|
||||||
|
* @author Paul Martin
|
||||||
* @since 3.0
|
* @since 3.0
|
||||||
*/
|
*/
|
||||||
public class JsonPropertyAccessor implements PropertyAccessor {
|
public class JsonPropertyAccessor implements PropertyAccessor {
|
||||||
@@ -44,7 +47,7 @@ public class JsonPropertyAccessor implements PropertyAccessor {
|
|||||||
* The kind of types this can work with.
|
* The kind of types this can work with.
|
||||||
*/
|
*/
|
||||||
private static final Class<?>[] SUPPORTED_CLASSES = new Class<?>[] { String.class, ToStringFriendlyJsonNode.class,
|
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
|
// Note: ObjectMapper is thread-safe
|
||||||
private ObjectMapper objectMapper = new ObjectMapper();
|
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 {
|
public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
|
||||||
ContainerNode<?> container = asJson(target);
|
ContainerNode<?> container = asJson(target);
|
||||||
Integer index = maybeIndex(name);
|
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 {
|
private ContainerNode<?> assertContainerNode(JsonNode json) throws AccessException {
|
||||||
@@ -79,6 +87,10 @@ public class JsonPropertyAccessor implements PropertyAccessor {
|
|||||||
ToStringFriendlyJsonNode wrapper = (ToStringFriendlyJsonNode) target;
|
ToStringFriendlyJsonNode wrapper = (ToStringFriendlyJsonNode) target;
|
||||||
return assertContainerNode(wrapper.node);
|
return assertContainerNode(wrapper.node);
|
||||||
}
|
}
|
||||||
|
else if (target instanceof ArrayNodeAsList) {
|
||||||
|
ArrayNodeAsList wrapper = (ArrayNodeAsList) target;
|
||||||
|
return assertContainerNode(wrapper.node);
|
||||||
|
}
|
||||||
else if (target instanceof String) {
|
else if (target instanceof String) {
|
||||||
try {
|
try {
|
||||||
JsonNode json = this.objectMapper.readTree((String) target);
|
JsonNode json = this.objectMapper.readTree((String) target);
|
||||||
@@ -113,16 +125,10 @@ public class JsonPropertyAccessor implements PropertyAccessor {
|
|||||||
ContainerNode<?> container = asJson(target);
|
ContainerNode<?> container = asJson(target);
|
||||||
Integer index = maybeIndex(name);
|
Integer index = maybeIndex(name);
|
||||||
if (index != null && container.has(index)) {
|
if (index != null && container.has(index)) {
|
||||||
return new TypedValue(wrap(container.get(index)));
|
return typedValue(container.get(index));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
JsonNode json = container.get(name);
|
return typedValue(container.get(name));
|
||||||
if (json != null) {
|
|
||||||
return new TypedValue(wrap(json));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return TypedValue.NULL;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,15 +147,43 @@ public class JsonPropertyAccessor implements PropertyAccessor {
|
|||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ToStringFriendlyJsonNode wrap(JsonNode json) {
|
private static TypedValue typedValue(JsonNode json) {
|
||||||
return new ToStringFriendlyJsonNode(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;
|
private final JsonNode node;
|
||||||
|
|
||||||
public ToStringFriendlyJsonNode(JsonNode node) {
|
ToStringFriendlyJsonNode(JsonNode node) {
|
||||||
this.node = 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<WrappedJsonNode> 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<WrappedJsonNode> iterator() {
|
||||||
|
|
||||||
|
return new Iterator<WrappedJsonNode>() {
|
||||||
|
|
||||||
|
private final Iterator<JsonNode> delegate = ArrayNodeAsList.this.node.iterator();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return this.delegate.hasNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WrappedJsonNode next() {
|
||||||
|
return wrap(this.delegate.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.assertThat;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
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.standard.SpelExpressionParser;
|
||||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link JsonPropertyAccessor}.
|
* Tests for {@link JsonPropertyAccessor}.
|
||||||
*
|
*
|
||||||
* @author Eric Bottard
|
* @author Eric Bottard
|
||||||
* @author Artem Bilan
|
* @author Artem Bilan
|
||||||
|
* @author Paul Martin
|
||||||
* @since 3.0
|
* @since 3.0
|
||||||
*/
|
*/
|
||||||
public class JsonPropertyAccessorTests {
|
public class JsonPropertyAccessorTests {
|
||||||
@@ -79,8 +84,28 @@ public class JsonPropertyAccessorTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testArrayLookup() throws Exception {
|
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
|
// 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
|
// JsonNode actual = evaluate("'1'", json, JsonNode.class); // Does not work
|
||||||
Object actual = evaluate(json, "['1']", Object.class);
|
Object actual = evaluate(json, "['1']", Object.class);
|
||||||
assertEquals("4", actual.toString());
|
assertEquals("4", actual.toString());
|
||||||
@@ -88,13 +113,42 @@ public class JsonPropertyAccessorTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testNestedArrayConstruct() throws Exception {
|
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], []]");
|
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
|
||||||
// JsonNode actual = evaluate("[1][1]", json, JsonNode.class); // Does not work
|
|
||||||
Object actual = evaluate(json, "['1']['1']", Object.class);
|
Object actual = evaluate(json, "['1']['1']", Object.class);
|
||||||
assertEquals("5", actual.toString());
|
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
|
@Test
|
||||||
public void testNestedHashConstruct() throws Exception {
|
public void testNestedHashConstruct() throws Exception {
|
||||||
Object json = mapper.readTree("{\"foo\": {\"bar\": 4, \"fizz\": 5} }");
|
Object json = mapper.readTree("{\"foo\": {\"bar\": 4, \"fizz\": 5} }");
|
||||||
|
|||||||
Reference in New Issue
Block a user