Make JsonPropertyAccessor returned type directly

* add `Comparable` contract to the initial wrapper so that expressions
containing selection/projection with JsonNode filtering are now possible
* render directly the value of a ValueNode. SpEL parser can now evaluate
correctly the object value when the expression deals with filtering on
`JsonNode` values (<, >, ==...)
* the 2 above changes make this possible:
`property.^[name.getTarget().asText() == 'value1'].name` can now be
written as simply as `property.^[name == 'value1'].name`. The expression
is now fully compatible with other PropertyAccessor types
* rename `ToStringFriendlyJsonNode` to `JsonNodeWrapper` as it reflects
more the capability of the new wrapper
* add a GenericConverter to be able to convert the `JsonNodeWrapper`
class into a `JsonNode` (or its derivative classes) directly while
calling `expression.getValue(..., JsonNode.class)`
* add the possibility to access list items in a json-path like index
	=> a negative number will begin from the end of the list <=
* return null value when `JsonNode` cannot handle the property name
(or index)

* Change Json Converter
* Clean up the code style
* Convert JsonPropertyAccessor tests to JUnit 5
This commit is contained in:
Pierre Lakreb
2020-05-03 08:08:23 -04:00
committed by Artem Bilan
parent 7a84d6797e
commit b58b0e5ffa
7 changed files with 315 additions and 210 deletions

View File

@@ -86,6 +86,7 @@ import org.springframework.util.ClassUtils;
* @author Artem Bilan
* @author Gary Russell
* @author Michael Wiles
* @author Pierre Lakreb
*
* @see IntegrationContextUtils
*/
@@ -411,17 +412,17 @@ class DefaultConfiguringBeanFactoryPostProcessor
private void jsonNodeToString(int registryId) {
if (!this.beanFactory.containsBean(
IntegrationContextUtils.TO_STRING_FRIENDLY_JSON_NODE_TO_STRING_CONVERTER_BEAN_NAME) &&
IntegrationContextUtils.JSON_NODE_WRAPPER_TO_JSON_NODE_CONVERTER) &&
!REGISTRIES_PROCESSED.contains(registryId) && JacksonPresent.isJackson2Present()) {
this.registry.registerBeanDefinition(
IntegrationContextUtils.TO_STRING_FRIENDLY_JSON_NODE_TO_STRING_CONVERTER_BEAN_NAME,
IntegrationContextUtils.JSON_NODE_WRAPPER_TO_JSON_NODE_CONVERTER,
BeanDefinitionBuilder.genericBeanDefinition(IntegrationConfigUtils.BASE_PACKAGE +
".json.ToStringFriendlyJsonNodeToStringConverter")
".json.JsonNodeWrapperToJsonNodeConverter")
.getBeanDefinition());
INTEGRATION_CONVERTER_INITIALIZER.registerConverter(this.registry,
new RuntimeBeanReference(
IntegrationContextUtils.TO_STRING_FRIENDLY_JSON_NODE_TO_STRING_CONVERTER_BEAN_NAME));
IntegrationContextUtils.JSON_NODE_WRAPPER_TO_JSON_NODE_CONVERTER));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
@@ -40,6 +40,7 @@ import org.springframework.util.Assert;
* @author Artem Bilan
* @author Gary Russell
* @author Oleg Zhurakousky
* @author Pierre Lakreb
*/
public abstract class IntegrationContextUtils {
@@ -91,8 +92,7 @@ public abstract class IntegrationContextUtils {
public static final String GLOBAL_CHANNEL_INTERCEPTOR_PROCESSOR_BEAN_NAME = "globalChannelInterceptorProcessor";
public static final String TO_STRING_FRIENDLY_JSON_NODE_TO_STRING_CONVERTER_BEAN_NAME =
"toStringFriendlyJsonNodeToStringConverter";
public static final String JSON_NODE_WRAPPER_TO_JSON_NODE_CONVERTER = "jsonNodeWrapperToJsonNodeConverter";
public static final String INTEGRATION_LIFECYCLE_ROLE_CONTROLLER = "integrationLifecycleRoleController";

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2021 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.json;
import java.util.Collections;
import java.util.Set;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.integration.json.JsonPropertyAccessor.JsonNodeWrapper;
import com.fasterxml.jackson.databind.JsonNode;
/**
* The {@link Converter} implementation for the conversion of {@link JsonPropertyAccessor.JsonNodeWrapper} to
* {@link JsonNode}, when the {@link JsonPropertyAccessor.JsonNodeWrapper} can be a result of the expression
* for JSON in case of the {@link JsonPropertyAccessor} usage.
*
* @author Pierre Lakreb
* @author Artem Bilan
*
* @since 5.5
*/
class JsonNodeWrapperToJsonNodeConverter implements GenericConverter {
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(JsonNodeWrapper.class, JsonNode.class));
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
return targetType.getObjectType().cast(((JsonNodeWrapper<?>) source).getRealNode());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 the original author or authors.
* Copyright 2013-2021 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.
@@ -16,6 +16,7 @@
package org.springframework.integration.json;
import java.io.IOException;
import java.util.AbstractList;
import java.util.Iterator;
@@ -23,14 +24,13 @@ import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypedValue;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ContainerNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* A SpEL {@link PropertyAccessor} that knows how to read properties from JSON objects.
@@ -40,6 +40,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
* @author Artem Bilan
* @author Paul Martin
* @author Gary Russell
* @author Pierre Lakreb
*
* @since 3.0
*/
@@ -49,15 +50,12 @@ 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,
ArrayNodeAsList.class,
ObjectNode.class,
ArrayNode.class
JsonNodeWrapper.class,
JsonNode.class
};
// Note: ObjectMapper is thread-safe
private ObjectMapper objectMapper = new ObjectMapper();
public void setObjectMapper(ObjectMapper objectMapper) {
@@ -72,42 +70,24 @@ public class JsonPropertyAccessor implements PropertyAccessor {
@Override
public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
ContainerNode<?> container = asJson(target);
JsonNode node = asJson(target);
Integer index = maybeIndex(name);
if (container instanceof ArrayNode) {
if (node instanceof ArrayNode) {
return index != null;
}
else {
return ((index != null && container.has(index)) || container.has(name));
}
return true;
}
private ContainerNode<?> assertContainerNode(JsonNode json) throws AccessException {
if (json instanceof ContainerNode) {
return (ContainerNode<?>) json;
private JsonNode asJson(Object target) throws AccessException {
if (target instanceof JsonNode) {
return (JsonNode) target;
}
else {
throw new AccessException(
"Can not act on json that is not a ContainerNode: " + json.getClass().getSimpleName());
}
}
private ContainerNode<?> asJson(Object target) throws AccessException {
if (target instanceof ContainerNode) {
return (ContainerNode<?>) target;
}
else if (target instanceof ToStringFriendlyJsonNode) {
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 JsonNodeWrapper) {
return ((JsonNodeWrapper<?>) target).getRealNode();
}
else if (target instanceof String) {
try {
JsonNode json = this.objectMapper.readTree((String) target);
return assertContainerNode(json);
return this.objectMapper.readTree((String) target);
}
catch (JsonProcessingException e) {
throw new AccessException("Exception while trying to deserialize String", e);
@@ -121,7 +101,7 @@ public class JsonPropertyAccessor implements PropertyAccessor {
/**
* Return an integer if the String property name can be parsed as an int, or null otherwise.
*/
private Integer maybeIndex(String name) {
private static Integer maybeIndex(String name) {
try {
return Integer.valueOf(name);
}
@@ -131,14 +111,14 @@ public class JsonPropertyAccessor implements PropertyAccessor {
}
@Override
public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
ContainerNode<?> container = asJson(target);
public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
JsonNode node = asJson(target);
Integer index = maybeIndex(name);
if (index != null && container.has(index)) {
return typedValue(container.get(index));
if (index != null && node.has(index)) {
return typedValue(node.get(index));
}
else {
return typedValue(container.get(name));
return typedValue(node.get(name));
}
}
@@ -152,138 +132,163 @@ public class JsonPropertyAccessor implements PropertyAccessor {
throw new UnsupportedOperationException("Write is not supported");
}
private static TypedValue typedValue(JsonNode json) {
private static TypedValue typedValue(JsonNode json) throws AccessException {
if (json == null) {
return TypedValue.NULL;
}
else {
return new TypedValue(wrap(json));
else if (json.isValueNode()) {
return new TypedValue(getValue(json));
}
return new TypedValue(wrap(json));
}
public static WrappedJsonNode<?> wrap(JsonNode json) {
private static Object getValue(JsonNode json) throws AccessException {
if (json.isTextual()) {
return json.textValue();
}
else if (json.isNumber()) {
return json.numberValue();
}
else if (json.isBoolean()) {
return json.asBoolean();
}
else if (json.isNull()) {
return null;
}
else if (json.isBinary()) {
try {
return json.binaryValue();
}
catch (IOException e) {
throw new AccessException(
"Can not get content of binary value : " + json);
}
}
throw new IllegalArgumentException("Json is not ValueNode.");
}
public static Object wrap(JsonNode json) throws AccessException {
if (json == null) {
return null;
}
else if (json instanceof ArrayNode) {
return new ArrayNodeAsList((ArrayNode) json);
}
else if (json.isValueNode()) {
return getValue(json);
}
else {
return new ToStringFriendlyJsonNode(json);
return new ComparableJsonNode(json);
}
}
/**
* The base interface for wrapped {@link JsonNode}.
* @since 5.0
*/
public interface WrappedJsonNode<T extends JsonNode> {
interface JsonNodeWrapper<T> extends Comparable<T> {
/**
* Return the wrapped {@link JsonNode}
* @return the wrapped JsonNode
*/
T getTarget();
String toString();
JsonNode getRealNode();
}
/**
* A {@link WrappedJsonNode} implementation to represent {@link JsonNode} as string.
*/
public static class ToStringFriendlyJsonNode implements WrappedJsonNode<JsonNode> {
static class ComparableJsonNode implements JsonNodeWrapper<ComparableJsonNode> {
private final JsonNode node;
private final JsonNode delegate;
ToStringFriendlyJsonNode(JsonNode node) {
this.node = node;
ComparableJsonNode(JsonNode delegate) {
this.delegate = delegate;
}
@Override
public JsonNode getTarget() {
return this.node;
public JsonNode getRealNode() {
return this.delegate;
}
@Override
public String toString() {
if (this.node == null) {
return "null";
}
if (this.node.isValueNode()) {
// This is to avoid quotes around a TextNode for example
return this.node.asText();
}
else {
return this.node.toString();
}
return this.delegate.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ToStringFriendlyJsonNode that = (ToStringFriendlyJsonNode) o;
return (this.node == that.node) || (this.node != null && this.node.equals(that.node));
}
@Override
public int hashCode() {
return this.node != null ? this.node.toString().hashCode() : 0;
public int compareTo(ComparableJsonNode o) {
return this.delegate.equals(o.delegate) ? 0 : 1;
}
}
/**
* An {@link AbstractList} implementation around {@link ArrayNode} with {@link WrappedJsonNode} aspect.
* An {@link AbstractList} implementation around {@link ArrayNode} with {@link JsonNodeWrapper} aspect.
* @since 5.0
*/
public static class ArrayNodeAsList extends AbstractList<WrappedJsonNode<?>>
implements WrappedJsonNode<ArrayNode> {
static class ArrayNodeAsList extends AbstractList<Object> implements JsonNodeWrapper<Object> {
private final ArrayNode node;
private final ArrayNode delegate;
ArrayNodeAsList(ArrayNode node) {
this.node = node;
this.delegate = node;
}
@Override
public ArrayNode getTarget() {
return this.node;
public JsonNode getRealNode() {
return this.delegate;
}
@Override
public WrappedJsonNode<?> get(int index) {
return wrap(this.node.get(index));
public String toString() {
return this.delegate.toString();
}
@Override
public Object get(int index) {
if (index < 0) {
// negative index can be handled with that conversion
index = this.delegate.size() + index;
}
try {
return wrap(this.delegate.get(index));
}
catch (AccessException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public int size() {
return this.node.size();
return this.delegate.size();
}
@Override
public Iterator<WrappedJsonNode<?>> iterator() {
public Iterator<Object> iterator() {
return new Iterator<WrappedJsonNode<?>>() {
return new Iterator<Object>() {
private final Iterator<JsonNode> delegate = ArrayNodeAsList.this.node.iterator();
private final Iterator<JsonNode> it = ArrayNodeAsList.this.delegate.iterator();
@Override
public boolean hasNext() {
return this.delegate.hasNext();
return this.it.hasNext();
}
@Override
public WrappedJsonNode<?> next() {
return wrap(this.delegate.next());
public Object next() {
try {
return wrap(this.it.next());
}
catch (AccessException e) {
throw new IllegalArgumentException(e);
}
}
};
}
@Override
public int compareTo(Object o) {
if (o instanceof JsonNodeWrapper<?>) {
return this.delegate.equals(((JsonNodeWrapper<?>) o).getRealNode()) ? 0 : 1;
}
return this.delegate.equals(o) ? 0 : 1;
}
}
}

View File

@@ -1,38 +0,0 @@
/*
* Copyright 2014-2019 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.json;
import org.springframework.core.convert.converter.Converter;
/**
* The {@link Converter} implementation for the conversion of
* {@link JsonPropertyAccessor.ToStringFriendlyJsonNode} to {@link String},
* when the {@link JsonPropertyAccessor.ToStringFriendlyJsonNode} can be a result of the
* expression for JSON in case of the {@link JsonPropertyAccessor} usage.
*
* @author Artem Bilan
* @since 4.1
*/
class ToStringFriendlyJsonNodeToStringConverter
implements Converter<JsonPropertyAccessor.ToStringFriendlyJsonNode, String> {
@Override
public String convert(JsonPropertyAccessor.ToStringFriendlyJsonNode source) {
return source.toString();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 the original author or authors.
* Copyright 2013-2021 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.
@@ -26,7 +26,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.Test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
@@ -51,6 +51,7 @@ import org.springframework.messaging.support.GenericMessage;
/**
* @author Gary Russell
* @author Artem Bilan
* @author Pierre Lakreb
*
* @since 3.0
*/
@@ -167,25 +168,25 @@ public class ParentContextTests {
assertThat(out.getPayload()).isEqualTo("FOO");
assertThat(parent
.containsBean(IntegrationContextUtils.TO_STRING_FRIENDLY_JSON_NODE_TO_STRING_CONVERTER_BEAN_NAME))
.containsBean(IntegrationContextUtils.JSON_NODE_WRAPPER_TO_JSON_NODE_CONVERTER))
.isTrue();
assertThat(child
.containsBean(IntegrationContextUtils.TO_STRING_FRIENDLY_JSON_NODE_TO_STRING_CONVERTER_BEAN_NAME))
.containsBean(IntegrationContextUtils.JSON_NODE_WRAPPER_TO_JSON_NODE_CONVERTER))
.isTrue();
Object converterRegistrar = parent.getBean(IntegrationContextUtils.CONVERTER_REGISTRAR_BEAN_NAME);
assertThat(converterRegistrar).isNotNull();
Set<?> converters = TestUtils.getPropertyValue(converterRegistrar, "converters", Set.class);
boolean toStringFriendlyJsonNodeToStringConverterPresent = false;
boolean jsonNodeWrapperToJsonNodeConverterPresent = false;
for (Object converter : converters) {
if ("ToStringFriendlyJsonNodeToStringConverter".equals(converter.getClass().getSimpleName())) {
toStringFriendlyJsonNodeToStringConverterPresent = true;
if ("JsonNodeWrapperToJsonNodeConverter".equals(converter.getClass().getSimpleName())) {
jsonNodeWrapperToJsonNodeConverterPresent = true;
break;
}
}
assertThat(toStringFriendlyJsonNodeToStringConverterPresent).isTrue();
assertThat(jsonNodeWrapperToJsonNodeConverterPresent).isTrue();
MessageChannel input = parent.getBean("testJsonNodeToStringConverterInputChannel", MessageChannel.class);
PollableChannel output = parent.getBean("testJsonNodeToStringConverterOutputChannel", PollableChannel.class);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 the original author or authors.
* Copyright 2013-2021 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,20 +17,25 @@
package org.springframework.integration.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.json.JsonPropertyAccessor.ComparableJsonNode;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* Tests for {@link JsonPropertyAccessor}.
@@ -38,6 +43,8 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
* @author Eric Bottard
* @author Artem Bilan
* @author Paul Martin
* @author Pierre Lakreb
*
* @since 3.0
*/
public class JsonPropertyAccessorTests {
@@ -48,63 +55,66 @@ public class JsonPropertyAccessorTests {
private final ObjectMapper mapper = new ObjectMapper();
@Before
@BeforeEach
public void setup() {
context.addPropertyAccessor(new JsonPropertyAccessor());
ConverterRegistry converterRegistry = (ConverterRegistry) DefaultConversionService.getSharedInstance();
converterRegistry.addConverter(new JsonNodeWrapperToJsonNodeConverter());
}
@Test
public void testSimpleLookup() throws Exception {
Object json = mapper.readTree("{\"foo\": \"bar\"}");
Object value = evaluate(json, "foo", Object.class);
assertThat(value).isInstanceOf(JsonPropertyAccessor.ToStringFriendlyJsonNode.class);
assertThat(value.toString()).isEqualTo("bar");
Object json2 = mapper.readTree("{\"foo\": \"bar\"}");
Object value2 = evaluate(json2, "foo", Object.class);
assertThat(value2).isInstanceOf(JsonPropertyAccessor.ToStringFriendlyJsonNode.class);
JsonNode json = mapper.readTree("{\"foo\": \"bar\"}");
String value = evaluate(json, "foo", String.class);
assertThat(value).isInstanceOf(String.class);
assertThat(value).isEqualTo("bar");
JsonNode json2 = mapper.readTree("{\"foo\": \"bar\"}");
String value2 = evaluate(json2, "foo", String.class);
assertThat(value2).isInstanceOf(String.class);
assertThat(value.equals(value2)).isTrue();
assertThat(value2.hashCode()).isEqualTo(value.hashCode());
}
@Test(expected = SpelEvaluationException.class)
public void testUnsupportedJsonConstruct() throws Exception {
Object json = mapper.readTree("\"foo\"");
evaluate(json, "fizz", Object.class);
@Test
public void testTextNode() throws Exception {
JsonNode json = mapper.readTree("\"foo\"");
String result = evaluate(json, "#root", String.class);
assertThat(result).isEqualTo("\"foo\"");
}
@Test(expected = SpelEvaluationException.class)
@Test
public void testMissingProperty() throws Exception {
Object json = mapper.readTree("{\"foo\": \"bar\"}");
evaluate(json, "fizz", Object.class);
JsonNode json = mapper.readTree("{\"foo\": \"bar\"}");
assertThat(evaluate(json, "fizz", String.class)).isNull();
}
@Test
public void testArrayLookup() throws Exception {
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);
assertThat(actual.toString()).isEqualTo("4");
// Have to wrap the root array because ArrayNode itself is not a List
Integer actual = evaluate(JsonPropertyAccessor.wrap(json), "[1]", Integer.class);
assertThat(actual).isEqualTo(4);
}
@Test
public void testArrayNegativeIndex() throws Exception {
JsonNode json = mapper.readTree("{\"foo\":[3, 4, 5]}");
assertThat(evaluate(json, "foo[-1]", Object.class)).isNull();
// help access json list items with json-path negative index
assertThat(evaluate(json, "foo[-1]", Integer.class)).isEqualTo(5);
}
@Test(expected = SpelEvaluationException.class)
@Test
public void testArrayIndexOutOfBounds() throws Exception {
JsonNode json = mapper.readTree("{\"foo\":[3, 4, 5]}");
evaluate(json, "foo[3]", Object.class);
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(() -> 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);
assertThat(actual.toString()).isEqualTo("4");
JsonNode json = mapper.readTree("[3, 4, 5]");
Integer actual = evaluate(json, "['1']", Integer.class);
assertThat(actual).isEqualTo(4);
}
@Test
@@ -112,63 +122,138 @@ public class JsonPropertyAccessorTests {
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);
assertThat(actual.toString()).isEqualTo("5");
assertThat(actual).isEqualTo(5);
}
@Test
public void testNestedArrayConstructWithStringIndex() throws Exception {
Object json = mapper.readTree("[[3], [4, 5], []]");
// JsonNode actual = evaluate("1.1", json, JsonNode.class); // Does not work
Object actual = evaluate(json, "['1']['1']", Object.class);
assertThat(actual.toString()).isEqualTo("5");
assertThat(actual).isEqualTo(5);
}
@Test
public void testArrayResult() throws Exception {
public void testArrayProjectionResult() 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);
assertThat(actualArray.size()).as("Array size").isEqualTo(3);
assertThat(evaluate(actualArray, "[0]", Object.class).toString()).as("[0]").isEqualTo("5");
assertThat(evaluate(actualArray, "[1]", Object.class).toString()).as("[1]").isEqualTo("7");
assertThat(evaluate(actualArray, "[2]", Object.class).toString()).as("[2]").isEqualTo("8");
assertThat(actualArray).hasSize(3);
assertThat(evaluate(actualArray, "[0]", Object.class)).isEqualTo(5);
assertThat(evaluate(actualArray, "[1]", Object.class)).isEqualTo(7);
assertThat(evaluate(actualArray, "[2]", Object.class)).isEqualTo(8);
}
@Test
public void testEmptyArrayResult() throws Exception {
public void testFilterOnArraySelection() 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);
assertThat(actualArray.size()).isEqualTo(0);
List<?> actualArray = evaluate(json, "foo.bar.?[fizz == 0]", List.class);
assertThat(actualArray).hasSize(0);
// Filter bar objects so that one match
actualArray = evaluate(json, "foo.bar.?[fizz == 8]", List.class);
assertThat(actualArray).hasSize(1);
assertThat(((ComparableJsonNode) actualArray.get(0)).getRealNode()).isEqualTo(mapper.readTree("{\"fizz\": 8}"));
// Filter bar objects so several match
actualArray = evaluate(json, "foo.bar.?[fizz > 6]", List.class);
assertThat(actualArray).hasSize(2);
assertThat(((ComparableJsonNode) actualArray.get(0)).getRealNode()).isEqualTo(mapper.readTree("{\"fizz\": 7}"));
assertThat(((ComparableJsonNode) actualArray.get(1)).getRealNode()).isEqualTo(mapper.readTree("{\"fizz\": 8}"));
}
@Test
public void testNestedHashConstruct() throws Exception {
Object json = mapper.readTree("{\"foo\": {\"bar\": 4, \"fizz\": 5} }");
Object actual = evaluate(json, "foo.fizz", Object.class);
assertThat(actual.toString()).isEqualTo("5");
assertThat(actual).isEqualTo(5);
}
@Test
public void testImplicitStringConversion() throws Exception {
public void testImplicitStringConversion() {
String json = "{\"foo\": {\"bar\": 4, \"fizz\": 5} }";
Object actual = evaluate(json, "foo.fizz", Object.class);
assertThat(actual.toString()).isEqualTo("5");
assertThat(actual).isEqualTo(5);
}
@Test(expected = SpelEvaluationException.class)
public void testUnsupportedString() throws Exception {
@Test
public void testSelectorAccess() {
String json = "{\"property\":[{\"name\":\"value1\"},{\"name\":\"value2\"}]}";
Object actual = evaluate(json, "property.^[name == 'value1'].name", Object.class);
assertThat(actual).isEqualTo("value1");
}
@Test
public void testJsonGetValueConversionAsJsonNode() throws Exception {
String json = "{\"property\":[{\"name\":\"value1\"},{\"name\":\"value2\"}]}";
// use JsonNode conversion
Object node = evaluate(json, "property.^[name == 'value1']", JsonNode.class);
assertThat(node).isInstanceOf(JsonNode.class);
assertThat(((JsonNode) node)).isEqualTo(mapper.readTree("{\"name\":\"value1\"}"));
}
@Test
public void testJsonGetValueConversionAsObjectNode() throws Exception {
String json = "{\"property\":[{\"name\":\"value1\"},{\"name\":\"value2\"}]}";
// use ObjectNode conversion
Object node = evaluate(json, "property.^[name == 'value1']", JsonNode.class);
assertThat(node).isInstanceOf(ObjectNode.class);
assertThat(((ObjectNode) node)).isEqualTo(mapper.readTree("{\"name\":\"value1\"}"));
}
@Test
public void testJsonGetValueConversionAsArrayNode() throws Exception {
String json = "{\"property\":[{\"name\":\"value1\"},{\"name\":\"value2\"}]}";
// use ArrayNode conversion
Object node = evaluate(json, "property", ArrayNode.class);
assertThat(node).isInstanceOf(ArrayNode.class);
assertThat(((ArrayNode) node)).isEqualTo(mapper.readTree("[{\"name\":\"value1\"},{\"name\":\"value2\"}]"));
}
@Test
public void testJsonGetValueConversionAsString() {
String json = "{\"property\":[{\"name\":\"value1\"},{\"name\":\"value2\"}]}";
// use ArrayNode conversion
Object node = evaluate(json, "#root", String.class);
assertThat(node).isInstanceOf(String.class);
assertThat(((String) node)).isEqualTo("{\"property\":[{\"name\":\"value1\"},{\"name\":\"value2\"}]}");
}
@Test
public void testSelectorComparingJsonNode() throws Exception {
String json = "{\"property\":[{\"name\":\"value1\"},{\"name\":\"value2\"}], " +
"\"property2\":[{\"name\":\"value1\"},{\"name\":\"value2\"}]}";
Object actual = evaluate(json, "property[0] eq property2[0]", Object.class);
assertThat(actual).isEqualTo(true);
}
@Test
public void testSelectorComparingArrayNode() throws Exception {
String json = "{\"property\":[{\"name\":\"value1\"},{\"name\":\"value2\"}], " +
"\"property2\":[{\"name\":\"value1\"},{\"name\":\"value2\"}]}";
Object actual = evaluate(json, "property eq property2", Object.class);
assertThat(actual).isEqualTo(true);
}
@Test
public void testUnsupportedString() {
String xml = "<what>?</what>";
evaluate(xml, "what", Object.class);
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(() -> evaluate(xml, "what", Object.class));
}
@Test(expected = SpelEvaluationException.class)
public void testUnsupportedJson() throws Exception {
@Test
public void testUnsupportedJson() {
String json = "\"literal\"";
evaluate(json, "foo", Object.class);
assertThat(evaluate(json, "foo", Object.class)).isNull();
}
@Test
@@ -176,8 +261,8 @@ public class JsonPropertyAccessorTests {
Expression expression = parser.parseExpression("foo");
Object json = mapper.readTree("{\"foo\": \"bar\"}");
Object value = expression.getValue(this.context, json);
assertThat(value).isInstanceOf(JsonPropertyAccessor.ToStringFriendlyJsonNode.class);
assertThat(value.toString()).isEqualTo("bar");
assertThat(value).isInstanceOf(String.class);
assertThat(value).isEqualTo("bar");
Object json2 = mapper.readTree("{}");
Object value2 = expression.getValue(this.context, json2);
assertThat(value2).isNull();