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

@@ -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();