diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/DefaultConfiguringBeanFactoryPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/DefaultConfiguringBeanFactoryPostProcessor.java index 5a43b5a211..df507984f0 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/DefaultConfiguringBeanFactoryPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/DefaultConfiguringBeanFactoryPostProcessor.java @@ -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)); } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java index 37d631dbee..29f2c9380e 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java @@ -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"; diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonNodeWrapperToJsonNodeConverter.java b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonNodeWrapperToJsonNodeConverter.java new file mode 100644 index 0000000000..75daf74c4f --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonNodeWrapperToJsonNodeConverter.java @@ -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 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()); + } + +} 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 4c9187c177..3064ddd4f5 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-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 { + interface JsonNodeWrapper extends Comparable { - /** - * 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 { + static class ComparableJsonNode implements JsonNodeWrapper { - 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> - implements WrappedJsonNode { + static class ArrayNodeAsList extends AbstractList implements JsonNodeWrapper { - 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> iterator() { + public Iterator iterator() { - return new Iterator>() { + return new Iterator() { - private final Iterator delegate = ArrayNodeAsList.this.node.iterator(); + private final Iterator 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; + } + } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/ToStringFriendlyJsonNodeToStringConverter.java b/spring-integration-core/src/main/java/org/springframework/integration/json/ToStringFriendlyJsonNodeToStringConverter.java deleted file mode 100644 index 6a012cff80..0000000000 --- a/spring-integration-core/src/main/java/org/springframework/integration/json/ToStringFriendlyJsonNodeToStringConverter.java +++ /dev/null @@ -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 { - - @Override - public String convert(JsonPropertyAccessor.ToStringFriendlyJsonNode source) { - return source.toString(); - } - -} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/expression/ParentContextTests.java b/spring-integration-core/src/test/java/org/springframework/integration/expression/ParentContextTests.java index 65c4b5473e..4960f677ce 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/expression/ParentContextTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/expression/ParentContextTests.java @@ -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); 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 b37921795c..435c013f6f 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-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 = "?"; - 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();