From e6e18ef8f2c554e395e0c5a75a8cac00bdb5692c Mon Sep 17 00:00:00 2001 From: Eric Bottard Date: Thu, 29 Aug 2013 10:33:22 +0200 Subject: [PATCH] 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. --- build.gradle | 2 +- .../json/JsonPropertyAccessor.java | 128 ++++++++++++++++++ .../json/JsonPropertyAccessorTests.java | 116 ++++++++++++++++ 3 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 spring-integration-core/src/main/java/org/springframework/integration/json/JsonPropertyAccessor.java create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/json/JsonPropertyAccessorTests.java diff --git a/build.gradle b/build.gradle index 52d4d80150..e7a2bca870 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonPropertyAccessor.java b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonPropertyAccessor.java new file mode 100644 index 0000000000..5c41cfb7a6 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonPropertyAccessor.java @@ -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; + } + +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPropertyAccessorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPropertyAccessorTests.java new file mode 100644 index 0000000000..3dfa07b214 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPropertyAccessorTests.java @@ -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 = "?"; + evaluate(xml, "what", Object.class); + } + + @Test(expected = SpelEvaluationException.class) + public void testUnsupportedJson() throws Exception { + String json = "\"literal\""; + evaluate(json, "foo", Object.class); + } + + private T evaluate(Object target, String expression, Class expectedType) { + return parser.parseExpression(expression).getValue(context, target, expectedType); + } + +}