diff --git a/spring-web/src/main/java/org/springframework/web/context/support/JacksonObjectMapperFactoryBean.java b/spring-web/src/main/java/org/springframework/web/context/support/JacksonObjectMapperFactoryBean.java new file mode 100644 index 0000000000..dec3e926d6 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/support/JacksonObjectMapperFactoryBean.java @@ -0,0 +1,250 @@ +/* + * Copyright 2002-2012 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.web.context.support; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.Map; + +import org.codehaus.jackson.JsonGenerator; +import org.codehaus.jackson.JsonParser; +import org.codehaus.jackson.map.AnnotationIntrospector; +import org.codehaus.jackson.map.DeserializationConfig; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.SerializationConfig; +import org.springframework.beans.FatalBeanException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; + +/** + * A FactoryBean for creating a Jackson {@link ObjectMapper} with setters to + * enable or disable Jackson features from within XML configuration. + * + *

Example usage with MappingJacksonHttpMessageConverter:

+ *
+ * <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">
+ * 	<property name="objectMapper">
+ * 		<bean class="org.springframework.web.context.support.JacksonObjectMapperFactoryBean"
+ * 			p:autoDetectFields="false"
+ * 			p:autoDetectGettersSetters="false"
+ * 			p:annotationIntrospector-ref="jaxbAnnotationIntrospector" />
+ * 	</property>
+ * </bean>
+ * 
+ * + *

Example usage with MappingJacksonJsonView:

+ *
+ * <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView">
+ * 	<property name="objectMapper">
+ * 		<bean class="org.springframework.web.context.support.JacksonObjectMapperFactoryBean"
+ * 			p:autoDetectFields="false"
+ * 			p:autoDetectGettersSetters="false"
+ * 			p:annotationIntrospector-ref="jaxbAnnotationIntrospector" />
+ * 	</property>
+ * </bean>
+ * 
+ * + *

In case there are no specific setters provided (for some rarely used + * options), you can still use the more general methods + * {@link #setFeaturesToEnable(Object[])} and {@link #setFeaturesToDisable(Object[])}. + * + *

+ * <bean class="org.springframework.web.context.support.JacksonObjectMapperFactoryBean">
+ * 	<property name="featuresToEnable">
+ * 		<array>
+ * 			<util:constant static-field="org.codehaus.jackson.map.SerializationConfig$Feature.WRAP_ROOT_VALUE"/>
+ * 			<util:constant static-field="org.codehaus.jackson.map.SerializationConfig$Feature.CLOSE_CLOSEABLE"/>
+ * 		</array>
+ * 	</property>
+ * 	<property name="featuresToDisable">
+ * 		<array>
+ * 			<util:constant static-field="org.codehaus.jackson.map.DeserializationConfig$Feature.USE_ANNOTATIONS"/>
+ * 		</array>
+ * 	</property>
+ * </bean>
+ * 
+ * + *

Note: This BeanFctory is singleton, so if you need more than one, you'll + * need to configure multiple instances. + * + * @author Dmitry Katsubo + * @author Rossen Stoyanchev + * + * @since 3.2 + */ +public class JacksonObjectMapperFactoryBean implements FactoryBean, InitializingBean { + + private ObjectMapper objectMapper; + + private Map features = new HashMap(); + + private AnnotationIntrospector annotationIntrospector; + + private DateFormat dateFormat; + + /** + * Set the ObjectMapper instance to use. + * If not set an instance will be created using the default constructor. + */ + public void setObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + /** + * Define annotationIntrospector for + * {@link SerializationConfig#setAnnotationIntrospector(AnnotationIntrospector)}. + */ + public void setAnnotationIntrospector(AnnotationIntrospector annotationIntrospector) { + this.annotationIntrospector = annotationIntrospector; + } + + /** + * Define the date/time format with the given string, which is in turn used + * to create a {@link SimpleDateFormat}. + * @see #setDateFormat(DateFormat) + */ + public void setSimpleDateFormat(String format) { + this.dateFormat = new SimpleDateFormat(format); + } + + /** + * Define the format for date/time with the given {@link DateFormat} instance. + * @see #setSimpleDateFormat(String) + */ + public void setDateFormat(DateFormat dateFormat) { + this.dateFormat = dateFormat; + } + + /** + * Shortcut for {@link SerializationConfig.Feature#AUTO_DETECT_FIELDS} and + * {@link DeserializationConfig.Feature#AUTO_DETECT_FIELDS}. + */ + public void setAutoDetectFields(boolean autoDetectFields) { + this.features.put(DeserializationConfig.Feature.AUTO_DETECT_FIELDS, Boolean.valueOf(autoDetectFields)); + this.features.put(SerializationConfig.Feature.AUTO_DETECT_FIELDS, Boolean.valueOf(autoDetectFields)); + } + + /** + * Shortcut for {@link SerializationConfig.Feature#AUTO_DETECT_GETTERS} and + * {@link DeserializationConfig.Feature#AUTO_DETECT_SETTERS}. + */ + public void setAutoDetectGettersSetters(boolean autoDetectGettersSetters) { + this.features.put(SerializationConfig.Feature.AUTO_DETECT_GETTERS, Boolean.valueOf(autoDetectGettersSetters)); + this.features.put(DeserializationConfig.Feature.AUTO_DETECT_SETTERS, Boolean.valueOf(autoDetectGettersSetters)); + } + + /** + * Shortcut for {@link SerializationConfig.Feature#FAIL_ON_EMPTY_BEANS}. + */ + public void setFailOnEmptyBeans(boolean failOnEmptyBeans) { + this.features.put(SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS, Boolean.valueOf(failOnEmptyBeans)); + } + + /** + * Shortcut for {@link SerializationConfig.Feature#INDENT_OUTPUT}. + */ + public void setIndentOutput(boolean indentOutput) { + this.features.put(SerializationConfig.Feature.INDENT_OUTPUT, Boolean.valueOf(indentOutput)); + } + + /** + * Specify features to enable. + * @see SerializationConfig.Feature + * @see DeserializationConfig.Feature + * @see JsonParser.Feature + * @see JsonGenerator.Feature + */ + public void setFeaturesToEnable(Object[] featuresToEnable) { + if (featuresToEnable == null) { + throw new FatalBeanException("featuresToEnable property should not be null"); + } + for (Object feature : featuresToEnable) { + this.features.put(feature, Boolean.TRUE); + } + } + + /** + * Specify features to disable. + * @see SerializationConfig.Feature + * @see DeserializationConfig.Feature + * @see JsonParser.Feature + * @see JsonGenerator.Feature + */ + public void setFeaturesToDisable(Object[] featuresToDisable) { + if (featuresToDisable == null) { + throw new FatalBeanException("featuresToDisable property should not be null"); + } + for (Object feature : featuresToDisable) { + this.features.put(feature, Boolean.FALSE); + } + } + + public ObjectMapper getObject() { + return this.objectMapper; + } + + public Class getObjectType() { + return ObjectMapper.class; + } + + public boolean isSingleton() { + return true; + } + + /** + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + public void afterPropertiesSet() throws FatalBeanException { + if (this.objectMapper == null) { + this.objectMapper = new ObjectMapper(); + } + + if (this.annotationIntrospector != null) { + this.objectMapper.getSerializationConfig().setAnnotationIntrospector(annotationIntrospector); + this.objectMapper.getDeserializationConfig().setAnnotationIntrospector(annotationIntrospector); + } + + if (this.dateFormat != null) { + // Deprecated for 1.8+, use + // objectMapper.setDateFormat(dateFormat); + this.objectMapper.getSerializationConfig().setDateFormat(this.dateFormat); + } + + for (Map.Entry entry : features.entrySet()) { + setFeatureEnabled(entry.getKey(), entry.getValue().booleanValue()); + } + } + + private void setFeatureEnabled(Object feature, boolean enabled) { + if (feature instanceof DeserializationConfig.Feature) { + this.objectMapper.configure((DeserializationConfig.Feature) feature, enabled); + } + else if (feature instanceof SerializationConfig.Feature) { + this.objectMapper.configure((SerializationConfig.Feature) feature, enabled); + } + else if (feature instanceof JsonParser.Feature) { + this.objectMapper.configure((JsonParser.Feature) feature, enabled); + } + else if (feature instanceof JsonGenerator.Feature) { + this.objectMapper.configure((JsonGenerator.Feature) feature, enabled); + } + else { + throw new FatalBeanException("Unknown feature class " + feature.getClass().getName()); + } + } +} diff --git a/spring-web/src/test/java/org/springframework/web/context/support/JacksonObjectMapperFactoryBeanTests.java b/spring-web/src/test/java/org/springframework/web/context/support/JacksonObjectMapperFactoryBeanTests.java new file mode 100644 index 0000000000..c086abaf0e --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/context/support/JacksonObjectMapperFactoryBeanTests.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2012 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.web.context.support; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.text.SimpleDateFormat; + +import org.codehaus.jackson.JsonFactory; +import org.codehaus.jackson.JsonGenerator; +import org.codehaus.jackson.JsonParser; +import org.codehaus.jackson.map.DeserializationConfig; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.SerializationConfig; +import org.codehaus.jackson.map.introspect.NopAnnotationIntrospector; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.FatalBeanException; + +/** + * Test cases for {@link JacksonObjectMapperFactoryBean} class. + * + * @author Dmitry Katsubo + */ +public class JacksonObjectMapperFactoryBeanTests { + + private static final String DATE_FORMAT = "yyyy-MM-dd"; + + private JacksonObjectMapperFactoryBean factory; + + @Before + public void setUp() { + factory = new JacksonObjectMapperFactoryBean(); + } + + @Test(expected=FatalBeanException.class) + public void testSetFeaturesToEnableNull() throws Exception { + factory.setFeaturesToEnable(null); + factory.setFeaturesToEnable(new Object[0]); + } + + @Test(expected=FatalBeanException.class) + public void testSetFeaturesToDisableNull() { + factory.setFeaturesToDisable(null); + } + + @Test + public void testSetFeaturesToEnableEmpty() { + factory.setFeaturesToEnable(new Object[0]); + factory.setFeaturesToDisable(new Object[0]); + } + + @Test(expected = FatalBeanException.class) + public void testUnknownFeature() { + factory.setFeaturesToEnable(new Object[] { Boolean.TRUE }); + factory.afterPropertiesSet(); + } + + @Test + public void testBooleanSetters() { + factory.setAutoDetectFields(false); + factory.setAutoDetectGettersSetters(false); + factory.setFailOnEmptyBeans(false); + factory.setIndentOutput(true); + + factory.afterPropertiesSet(); + + ObjectMapper objectMapper = factory.getObject(); + + SerializationConfig serializeConfig = objectMapper.getSerializationConfig(); + DeserializationConfig deserializeConfig = objectMapper.getDeserializationConfig(); + + assertFalse(serializeConfig.isEnabled(SerializationConfig.Feature.AUTO_DETECT_FIELDS)); + assertFalse(deserializeConfig.isEnabled(DeserializationConfig.Feature.AUTO_DETECT_FIELDS)); + assertFalse(serializeConfig.isEnabled(SerializationConfig.Feature.AUTO_DETECT_GETTERS)); + assertFalse(deserializeConfig.isEnabled(DeserializationConfig.Feature.AUTO_DETECT_SETTERS)); + assertFalse(serializeConfig.isEnabled(SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS)); + assertTrue(serializeConfig.isEnabled(SerializationConfig.Feature.INDENT_OUTPUT)); + } + + @Test + public void testDateTimeFormatSetter() { + SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT); + + factory.setDateFormat(dateFormat); + factory.afterPropertiesSet(); + + assertEquals(dateFormat, factory.getObject().getSerializationConfig().getDateFormat()); + } + + @Test + public void testSimpleDateFormatStringSetter() { + SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT); + + factory.setSimpleDateFormat(DATE_FORMAT); + factory.afterPropertiesSet(); + + assertEquals(dateFormat, factory.getObject().getSerializationConfig().getDateFormat()); + } + + @Test + public void testSimpleSetup() { + factory.afterPropertiesSet(); + + assertNotNull(factory.getObject()); + assertTrue(factory.isSingleton()); + assertEquals(ObjectMapper.class, factory.getObjectType()); + } + + @Test + public void testCompleteSetup() { + NopAnnotationIntrospector annotationIntrospector = new NopAnnotationIntrospector(); + ObjectMapper objectMapper = new ObjectMapper(); + + assertTrue(factory.isSingleton()); + assertEquals(ObjectMapper.class, factory.getObjectType()); + + factory.setObjectMapper(objectMapper); + factory.setAnnotationIntrospector(annotationIntrospector); + factory.setFeaturesToEnable(new Object[] { + SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS, + DeserializationConfig.Feature.USE_ANNOTATIONS, + JsonParser.Feature.ALLOW_SINGLE_QUOTES, + JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS + }); + factory.setFeaturesToDisable(new Object[] { + SerializationConfig.Feature.AUTO_DETECT_GETTERS, + DeserializationConfig.Feature.AUTO_DETECT_FIELDS, + JsonParser.Feature.AUTO_CLOSE_SOURCE, + JsonGenerator.Feature.QUOTE_FIELD_NAMES + }); + + factory.afterPropertiesSet(); + + assertTrue(objectMapper == factory.getObject()); + + SerializationConfig serializeConfig = objectMapper.getSerializationConfig(); + DeserializationConfig deserializeConfig = objectMapper.getDeserializationConfig(); + JsonFactory jsonFactory = objectMapper.getJsonFactory(); + + assertTrue(annotationIntrospector == serializeConfig.getAnnotationIntrospector()); + assertTrue(annotationIntrospector == deserializeConfig.getAnnotationIntrospector()); + + assertTrue(serializeConfig.isEnabled(SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS)); + assertTrue(deserializeConfig.isEnabled(DeserializationConfig.Feature.USE_ANNOTATIONS)); + assertTrue(jsonFactory.isEnabled(JsonParser.Feature.ALLOW_SINGLE_QUOTES)); + assertTrue(jsonFactory.isEnabled(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS)); + + assertFalse(serializeConfig.isEnabled(SerializationConfig.Feature.AUTO_DETECT_GETTERS)); + assertFalse(deserializeConfig.isEnabled(DeserializationConfig.Feature.AUTO_DETECT_FIELDS)); + assertFalse(jsonFactory.isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE)); + assertFalse(jsonFactory.isEnabled(JsonGenerator.Feature.QUOTE_FIELD_NAMES)); + } +} diff --git a/src/dist/changelog.txt b/src/dist/changelog.txt index 5b491efc36..c15e1d84d9 100644 --- a/src/dist/changelog.txt +++ b/src/dist/changelog.txt @@ -8,6 +8,7 @@ Changes in version 3.2 M2 * spring-test module now depends on junit:junit-dep * raise RestClientException instead of IllegalArgumentException for unknown status codes +* add JacksonObjectMapperFactoryBean for configuring a Jackson ObjectMapper in XML Changes in version 3.2 M1 (2012-05-28) --------------------------------------