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:
Eric Bottard
2013-08-29 10:33:22 +02:00
committed by Gary Russell
parent db8da81d16
commit e6e18ef8f2
3 changed files with 245 additions and 1 deletions

View File

@@ -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'

View File

@@ -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;
}
}

View File

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