/*
* Copyright 2013-2017 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
*
* http://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.io.IOException;
import java.util.AbstractList;
import java.util.Iterator;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypedValue;
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 on Jackson JSON objects.
*
* @author Eric Bottard
* @author Artem Bilan
* @author Paul Martin
* @since 3.0
*/
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 };
// Note: ObjectMapper is thread-safe
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public Class<?>[] getSpecificTargetClasses() {
return SUPPORTED_CLASSES;
}
@Override
public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
ContainerNode<?> container = asJson(target);
Integer index = maybeIndex(name);
if (container instanceof ArrayNode) {
return index != null;
}
else {
return ((index != null && container.has(index)) || container.has(name));
}
}
private ContainerNode<?> assertContainerNode(JsonNode json) throws AccessException {
if (json instanceof ContainerNode) {
return (ContainerNode<?>) json;
}
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 String) {
try {
JsonNode json = this.objectMapper.readTree((String) target);
return assertContainerNode(json);
}
catch (JsonProcessingException e) {
throw new AccessException("Exception while trying to deserialize String", e);
}
catch (IOException e) {
throw new AccessException("Exception while trying to deserialize String", e);
}
}
else {
throw new IllegalStateException("Can't happen. Check SUPPORTED_CLASSES");
}
}
/**
* Return an integer if the String property name can be parsed as an int, or null otherwise.
*/
private Integer maybeIndex(String name) {
try {
return Integer.valueOf(name);
}
catch (NumberFormatException e) {
return null;
}
}
@Override
public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
ContainerNode<?> container = asJson(target);
Integer index = maybeIndex(name);
if (index != null && container.has(index)) {
return typedValue(container.get(index));
}
else {
return typedValue(container.get(name));
}
}
@Override
public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException {
return false;
}
@Override
public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException {
throw new UnsupportedOperationException("Write is not supported");
}
public void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "'objectMapper' cannot be null");
this.objectMapper = objectMapper;
}
private static TypedValue typedValue(JsonNode json) {
if (json == null) {
return TypedValue.NULL;
}
else {
return new TypedValue(wrap(json));
}
}
public static WrappedJsonNode wrap(JsonNode json) {
if (json == null) {
return null;
}
else if (json instanceof ArrayNode) {
return new ArrayNodeAsList((ArrayNode) json);
}
else {
return new ToStringFriendlyJsonNode(json);
}
}
/**
* The base interface for wrapped {@link JsonNode}.
* @since 5.0
*/
public interface WrappedJsonNode {
}
/**
* A {@link WrappedJsonNode} implementation to represent {@link JsonNode} as string.
*/
public static class ToStringFriendlyJsonNode implements WrappedJsonNode {
private final JsonNode node;
ToStringFriendlyJsonNode(JsonNode node) {
this.node = node;
}
@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();
}
}
@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;
}
}
/**
* An {@link AbstractList} implementation around {@link ArrayNode} with {@link WrappedJsonNode} aspect.
* @since 5.0
*/
public static class ArrayNodeAsList extends AbstractList<WrappedJsonNode> implements WrappedJsonNode {
private final ArrayNode node;
ArrayNodeAsList(ArrayNode node) {
this.node = node;
}
@Override
public WrappedJsonNode get(int index) {
return wrap(this.node.get(index));
}
@Override
public int size() {
return this.node.size();
}
@Override
public Iterator<WrappedJsonNode> iterator() {
return new Iterator<WrappedJsonNode>() {
private final Iterator<JsonNode> delegate = ArrayNodeAsList.this.node.iterator();
@Override
public boolean hasNext() {
return this.delegate.hasNext();
}
@Override
public WrappedJsonNode next() {
return wrap(this.delegate.next());
}
};
}
}
}