INT-3122 Add JsonPropertyAccessor
Requires Jackson2 Bump Jackson2 version Remove write support Add String support Polishing Initialize objectMapper field; assert ObjectMapper is not null. Newlines at ends of files.
This commit is contained in:
committed by
Gary Russell
parent
db8da81d16
commit
e6e18ef8f2
@@ -51,7 +51,7 @@ subprojects { subproject ->
|
||||
groovyVersion = '2.1.0'
|
||||
hamcrestVersion = '1.3'
|
||||
jacksonVersion = '1.9.2'
|
||||
jackson2Version = '2.1.2'
|
||||
jackson2Version = '2.2.3'
|
||||
javaxActivationVersion = '1.1.1'
|
||||
junitVersion = '4.11'
|
||||
log4jVersion = '1.2.12'
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2013 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 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
|
||||
*/
|
||||
public class JsonPropertyAccessor implements PropertyAccessor {
|
||||
|
||||
/**
|
||||
* The kind of types this can work with.
|
||||
*/
|
||||
private static final Class<?>[] SUPPORTED_CLASSES =
|
||||
new Class[] { String.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);
|
||||
return ((index != null && container.has(index)) || container.has(name));
|
||||
}
|
||||
|
||||
private ContainerNode<?> asJson(Object target) throws AccessException {
|
||||
if (target instanceof ContainerNode) {
|
||||
return (ContainerNode<?>) target;
|
||||
}
|
||||
else if (target instanceof String) {
|
||||
try {
|
||||
JsonNode json = this.objectMapper.readTree((String) target);
|
||||
if (json instanceof ContainerNode) {
|
||||
return (ContainerNode<?>) json;
|
||||
}
|
||||
else {
|
||||
throw new AccessException("Can not act on json that is not a ContainerNode: "
|
||||
+ json.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
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 new TypedValue(container.get(index));
|
||||
}
|
||||
else {
|
||||
return new 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2013 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 static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.expression.spel.SpelEvaluationException;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Tests for {@link JsonPropertyAccessor}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class JsonPropertyAccessorTests {
|
||||
|
||||
private final SpelExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
private final StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
context.addPropertyAccessor(new JsonPropertyAccessor());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleLookup() throws Exception {
|
||||
Object json = mapper.readTree("{\"foo\": \"bar\"}");
|
||||
JsonNode actual = evaluate(json, "foo", JsonNode.class);
|
||||
assertEquals("bar", actual.asText());
|
||||
}
|
||||
|
||||
@Test(expected = SpelEvaluationException.class)
|
||||
public void testUnsupportedJsonConstruct() throws Exception {
|
||||
Object json = mapper.readTree("\"foo\"");
|
||||
evaluate(json, "fizz", JsonNode.class);
|
||||
}
|
||||
|
||||
@Test(expected = SpelEvaluationException.class)
|
||||
public void testMissingProperty() throws Exception {
|
||||
Object json = mapper.readTree("{\"foo\": \"bar\"}");
|
||||
evaluate(json, "fizz", JsonNode.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArrayLookup() throws Exception {
|
||||
Object json = mapper.readTree("[3, 4, 5]");
|
||||
// JsonNode actual = evaluate("1", json, JsonNode.class); // Does not work
|
||||
// JsonNode actual = evaluate("'1'", json, JsonNode.class); // Does not work
|
||||
JsonNode actual = evaluate(json, "['1']", JsonNode.class);
|
||||
assertEquals(4, actual.asInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNestedArrayConstruct() throws Exception {
|
||||
Object json = mapper.readTree("[[3], [4, 5], []]");
|
||||
// JsonNode actual = evaluate("1.1", json, JsonNode.class); // Does not work
|
||||
// JsonNode actual = evaluate("[1][1]", json, JsonNode.class); // Does not work
|
||||
JsonNode actual = evaluate(json, "['1']['1']", JsonNode.class);
|
||||
assertEquals(5, actual.asInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNestedHashConstruct() throws Exception {
|
||||
Object json = mapper.readTree("{\"foo\": {\"bar\": 4, \"fizz\": 5} }");
|
||||
JsonNode actual = evaluate(json, "foo.fizz", JsonNode.class);
|
||||
assertEquals(5, actual.asInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImplicitStringConversion() throws Exception {
|
||||
String json = "{\"foo\": {\"bar\": 4, \"fizz\": 5} }";
|
||||
JsonNode actual = evaluate(json, "foo.fizz", JsonNode.class);
|
||||
assertEquals(5, actual.asInt());
|
||||
}
|
||||
|
||||
@Test(expected = SpelEvaluationException.class)
|
||||
public void testUnsupportedString() throws Exception {
|
||||
String xml = "<what>?</what>";
|
||||
evaluate(xml, "what", Object.class);
|
||||
}
|
||||
|
||||
@Test(expected = SpelEvaluationException.class)
|
||||
public void testUnsupportedJson() throws Exception {
|
||||
String json = "\"literal\"";
|
||||
evaluate(json, "foo", Object.class);
|
||||
}
|
||||
|
||||
private <T> T evaluate(Object target, String expression, Class<T> expectedType) {
|
||||
return parser.parseExpression(expression).getValue(context, target, expectedType);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user