From e0dda2a4f0e76c46f690a1146eb905b3eb414f83 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 29 May 2013 17:51:39 +0200 Subject: [PATCH] DATACMNS-333 - Added Jackson 2 support to repository populators. Added necessary infrastructure to use Jackson 2 with repository populators. Expose a jackson2-populator XML element in the namespace to setup a Jackson2 based repository populator. --- pom.xml | 7 ++ .../config/RepositoryNameSpaceHandler.java | 1 + ...positoryPopulatorBeanDefinitionParser.java | 23 +++- ...ackson2RepositoryPopulatorFactoryBean.java | 49 ++++++++ .../init/Jackson2ResourceReader.java | 117 ++++++++++++++++++ .../config/spring-repository-1.6.xsd | 21 ++++ ...rBeanDefinitionParserIntegrationTests.java | 23 ++++ ...ackson2ResourceReaderIntegrationTests.java | 43 +++++++ .../data/repository/config/populators.xml | 5 +- template.mf | 1 + 10 files changed, 283 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/init/Jackson2RepositoryPopulatorFactoryBean.java create mode 100644 src/main/java/org/springframework/data/repository/init/Jackson2ResourceReader.java create mode 100644 src/test/java/org/springframework/data/repository/init/Jackson2ResourceReaderIntegrationTests.java diff --git a/pom.xml b/pom.xml index 602792117..d578d5011 100644 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,7 @@ 1.9.7 + 2.2.2 0.6.0.BUILD-SNAPSHOT DATACMNS @@ -69,6 +70,12 @@ ${jackson} true + + com.fasterxml.jackson.core + jackson-databind + ${jackson2} + true + org.springframework spring-web diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryNameSpaceHandler.java b/src/main/java/org/springframework/data/repository/config/RepositoryNameSpaceHandler.java index 7c314088d..b4603416a 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryNameSpaceHandler.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryNameSpaceHandler.java @@ -36,5 +36,6 @@ public class RepositoryNameSpaceHandler extends NamespaceHandlerSupport { public void init() { registerBeanDefinitionParser("unmarshaller-populator", PARSER); registerBeanDefinitionParser("jackson-populator", PARSER); + registerBeanDefinitionParser("jackson2-populator", PARSER); } } diff --git a/src/main/java/org/springframework/data/repository/config/ResourceReaderRepositoryPopulatorBeanDefinitionParser.java b/src/main/java/org/springframework/data/repository/config/ResourceReaderRepositoryPopulatorBeanDefinitionParser.java index 598790c1b..b2f32e358 100644 --- a/src/main/java/org/springframework/data/repository/config/ResourceReaderRepositoryPopulatorBeanDefinitionParser.java +++ b/src/main/java/org/springframework/data/repository/config/ResourceReaderRepositoryPopulatorBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 the original author or authors. + * Copyright 2012-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. @@ -15,9 +15,12 @@ */ package org.springframework.data.repository.config; +import java.util.Arrays; + import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.data.repository.init.Jackson2RepositoryPopulatorFactoryBean; import org.springframework.data.repository.init.JacksonRepositoryPopulatorFactoryBean; import org.springframework.data.repository.init.UnmarshallerRepositoryPopulatorFactoryBean; import org.springframework.util.StringUtils; @@ -38,8 +41,16 @@ public class ResourceReaderRepositoryPopulatorBeanDefinitionParser extends Abstr protected String getBeanClassName(Element element) { String name = element.getLocalName(); - return "unmarshaller-populator".equals(name) ? UnmarshallerRepositoryPopulatorFactoryBean.class.getName() - : JacksonRepositoryPopulatorFactoryBean.class.getName(); + + if ("unmarshaller-populator".equals(name)) { + return UnmarshallerRepositoryPopulatorFactoryBean.class.getName(); + } else if ("jackson-populator".equals(name)) { + return JacksonRepositoryPopulatorFactoryBean.class.getName(); + } else if ("jackson2-populator".equals(name)) { + return Jackson2RepositoryPopulatorFactoryBean.class.getName(); + } + + throw new IllegalStateException("Unsupported populator type " + name + "!"); } /* @@ -55,7 +66,7 @@ public class ResourceReaderRepositoryPopulatorBeanDefinitionParser extends Abstr if ("unmarshaller-populator".equals(localName)) { parseXmlPopulator(element, builder); - } else if ("jackson-populator".equals(localName)) { + } else if (Arrays.asList("jackson-populator", "jackson2-populator").contains(localName)) { parseJsonPopulator(element, builder); } } @@ -66,7 +77,7 @@ public class ResourceReaderRepositoryPopulatorBeanDefinitionParser extends Abstr * @param element * @param builder */ - private void parseJsonPopulator(Element element, BeanDefinitionBuilder builder) { + private static void parseJsonPopulator(Element element, BeanDefinitionBuilder builder) { String objectMapperRef = element.getAttribute("object-mapper-ref"); @@ -81,7 +92,7 @@ public class ResourceReaderRepositoryPopulatorBeanDefinitionParser extends Abstr * @param element * @param builder */ - private void parseXmlPopulator(Element element, BeanDefinitionBuilder builder) { + private static void parseXmlPopulator(Element element, BeanDefinitionBuilder builder) { String unmarshallerRefName = element.getAttribute("unmarshaller-ref"); diff --git a/src/main/java/org/springframework/data/repository/init/Jackson2RepositoryPopulatorFactoryBean.java b/src/main/java/org/springframework/data/repository/init/Jackson2RepositoryPopulatorFactoryBean.java new file mode 100644 index 000000000..96eb8c112 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/init/Jackson2RepositoryPopulatorFactoryBean.java @@ -0,0 +1,49 @@ +/* + * 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.data.repository.init; + +import org.springframework.beans.factory.FactoryBean; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * {@link FactoryBean} to set up a {@link ResourceReaderRepositoryPopulator} with a {@link Jackson2ResourceReader}. + * + * @author Oliver Gierke + * @since 1.6 + */ +public class Jackson2RepositoryPopulatorFactoryBean extends AbstractRepositoryPopulatorFactoryBean { + + private ObjectMapper mapper; + + /** + * Configures the {@link ObjectMapper} to be used. + * + * @param mapper + */ + public void setMapper(ObjectMapper mapper) { + this.mapper = mapper; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.init.AbstractRepositoryPopulatorFactoryBean#getResourceReader() + */ + @Override + protected ResourceReader getResourceReader() { + return new Jackson2ResourceReader(mapper); + } +} diff --git a/src/main/java/org/springframework/data/repository/init/Jackson2ResourceReader.java b/src/main/java/org/springframework/data/repository/init/Jackson2ResourceReader.java new file mode 100644 index 000000000..b47756af6 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/init/Jackson2ResourceReader.java @@ -0,0 +1,117 @@ +/* + * 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.data.repository.init; + +import static com.fasterxml.jackson.databind.DeserializationFeature.*; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.springframework.core.io.Resource; +import org.springframework.util.ClassUtils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * A {@link ResourceReader} using Jackson to read JSON into objects. + * + * @author Oliver Gierke + * @since 1.6 + */ +public class Jackson2ResourceReader implements ResourceReader { + + private static final String DEFAULT_TYPE_KEY = "_class"; + private static final ObjectMapper DEFAULT_MAPPER = new ObjectMapper(); + + static { + DEFAULT_MAPPER.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + private final ObjectMapper mapper; + private String typeKey = DEFAULT_TYPE_KEY; + + /** + * Creates a new {@link Jackson2ResourceReader}. + */ + public Jackson2ResourceReader() { + this(DEFAULT_MAPPER); + } + + /** + * Creates a new {@link Jackson2ResourceReader} using the given {@link ObjectMapper}. + * + * @param mapper + */ + public Jackson2ResourceReader(ObjectMapper mapper) { + this.mapper = mapper == null ? DEFAULT_MAPPER : mapper; + } + + /** + * Configures the JSON document's key to lookup the type to instantiate the object. Defaults to + * {@link Jackson2ResourceReader#DEFAULT_TYPE_KEY}. + * + * @param typeKey + */ + public void setTypeKey(String typeKey) { + this.typeKey = typeKey; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.init.ResourceReader#readFrom(org.springframework.core.io.Resource, java.lang.ClassLoader) + */ + public Object readFrom(Resource resource, ClassLoader classLoader) throws Exception { + + InputStream stream = resource.getInputStream(); + JsonNode node = mapper.reader(JsonNode.class).readTree(stream); + + if (node.isArray()) { + + Iterator elements = node.elements(); + List result = new ArrayList(); + + while (elements.hasNext()) { + JsonNode element = elements.next(); + result.add(readSingle(element, classLoader)); + } + + return result; + } + + return readSingle(node, classLoader); + } + + /** + * Reads the given {@link JsonNode} into an instance of the type encoded in it using the configured type key. + * + * @param node must not be {@literal null}. + * @param classLoader + * @return + */ + private Object readSingle(JsonNode node, ClassLoader classLoader) throws IOException { + + JsonNode typeNode = node.findValue(typeKey); + String typeName = typeNode == null ? null : typeNode.asText(); + + Class type = ClassUtils.resolveClassName(typeName, classLoader); + + return mapper.reader(type).readValue(node); + } +} diff --git a/src/main/resources/org/springframework/data/repository/config/spring-repository-1.6.xsd b/src/main/resources/org/springframework/data/repository/config/spring-repository-1.6.xsd index c4b5c2b42..b8512c445 100644 --- a/src/main/resources/org/springframework/data/repository/config/spring-repository-1.6.xsd +++ b/src/main/resources/org/springframework/data/repository/config/spring-repository-1.6.xsd @@ -95,6 +95,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/org/springframework/data/repository/config/ResourceReaderRepositoryPopulatorBeanDefinitionParserIntegrationTests.java b/src/test/java/org/springframework/data/repository/config/ResourceReaderRepositoryPopulatorBeanDefinitionParserIntegrationTests.java index a87144606..2f678a021 100644 --- a/src/test/java/org/springframework/data/repository/config/ResourceReaderRepositoryPopulatorBeanDefinitionParserIntegrationTests.java +++ b/src/test/java/org/springframework/data/repository/config/ResourceReaderRepositoryPopulatorBeanDefinitionParserIntegrationTests.java @@ -25,6 +25,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.core.io.ClassPathResource; +import org.springframework.data.repository.init.Jackson2ResourceReader; import org.springframework.data.repository.init.JacksonResourceReader; import org.springframework.data.repository.init.ResourceReaderRepositoryPopulator; import org.springframework.data.repository.init.UnmarshallingResourceReader; @@ -60,6 +61,28 @@ public class ResourceReaderRepositoryPopulatorBeanDefinitionParserIntegrationTes assertIsListOfClasspathResourcesWithPath(resources, "org/springframework/data/repository/init/data.json"); } + /** + * @see DATACMNS-333 + */ + @Test + public void registersJackson2InitializerCorrectly() { + + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); + reader.loadBeanDefinitions(new ClassPathResource("populators.xml", getClass())); + + BeanDefinition definition = beanFactory.getBeanDefinition("jackson2-populator"); + assertThat(definition, is(notNullValue())); + + Object bean = beanFactory.getBean("jackson2-populator"); + assertThat(bean, is(instanceOf(ResourceReaderRepositoryPopulator.class))); + Object resourceReader = ReflectionTestUtils.getField(bean, "reader"); + assertThat(resourceReader, is(instanceOf(Jackson2ResourceReader.class))); + + Object resources = ReflectionTestUtils.getField(bean, "resources"); + assertIsListOfClasspathResourcesWithPath(resources, "org/springframework/data/repository/init/data.json"); + } + /** * @see DATACMNS-58 */ diff --git a/src/test/java/org/springframework/data/repository/init/Jackson2ResourceReaderIntegrationTests.java b/src/test/java/org/springframework/data/repository/init/Jackson2ResourceReaderIntegrationTests.java new file mode 100644 index 000000000..5b7240897 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/init/Jackson2ResourceReaderIntegrationTests.java @@ -0,0 +1,43 @@ +/* + * 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.data.repository.init; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.Collection; + +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; + +/** + * Integration tests for {@link JacksonResourceReader}. + * + * @author Oliver Gierke + * @since 1.6 + */ +public class Jackson2ResourceReaderIntegrationTests { + + @Test + public void readsFileWithMultipleObjects() throws Exception { + + ResourceReader reader = new Jackson2ResourceReader(); + Object result = reader.readFrom(new ClassPathResource("data.json", getClass()), null); + + assertThat(result, is(instanceOf(Collection.class))); + assertThat((Collection) result, hasSize(1)); + } +} diff --git a/src/test/resources/org/springframework/data/repository/config/populators.xml b/src/test/resources/org/springframework/data/repository/config/populators.xml index 7459b3d04..a0b275524 100644 --- a/src/test/resources/org/springframework/data/repository/config/populators.xml +++ b/src/test/resources/org/springframework/data/repository/config/populators.xml @@ -2,13 +2,16 @@ + + diff --git a/template.mf b/template.mf index cd1922f52..61ce01679 100644 --- a/template.mf +++ b/template.mf @@ -5,6 +5,7 @@ Bundle-ManifestVersion: 2 Import-Package: sun.reflect;version="0";resolution:=optional Import-Template: + com.fasterxml.jackson.databind.*;version="${jackson2:[=.=.=,+1.0.0)}";resolution:=optional, com.mysema.query.*;version="${querydsl:[=.=.=,+1.0.0)}";resolution:=optional, javax.enterprise.*;version="${cdi:[=.=.=,+1.0.0)}";resolution:=optional, javax.inject.*;version="[1.0.0,2.0.0)";resolution:=optional,