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:
committed by
Artem Bilan
parent
7a84d6797e
commit
b58b0e5ffa
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user