diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.java index fe667a3e59..c3e2e56472 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -16,28 +16,88 @@ package org.springframework.boot.autoconfigure.gson; +import java.util.List; + import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; /** * {@link EnableAutoConfiguration Auto-configuration} for Gson. * * @author David Liu + * @author Ivan Golovko * @since 1.2.0 */ @Configuration @ConditionalOnClass(Gson.class) +@EnableConfigurationProperties(GsonProperties.class) public class GsonAutoConfiguration { @Bean - @ConditionalOnMissingBean - public Gson gson() { - return new Gson(); + public GsonBuilder gsonBuilder(List customizers) { + GsonBuilder builder = new GsonBuilder(); + customizers.forEach(c -> c.customize(builder)); + return builder; + } + + @Bean + @ConditionalOnMissingBean(Gson.class) + public Gson gson(GsonBuilder gsonBuilder) { + return gsonBuilder.create(); + } + + @Bean + public StandardGsonBuilderCustomizer standardGsonBuilderCustomizer( + GsonProperties gsonProperties) { + return new StandardGsonBuilderCustomizer(gsonProperties); + } + + private static final class StandardGsonBuilderCustomizer + implements GsonBuilderCustomizer, Ordered { + + private final GsonProperties properties; + + StandardGsonBuilderCustomizer(GsonProperties properties) { + this.properties = properties; + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public void customize(GsonBuilder builder) { + GsonProperties properties = this.properties; + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getGenerateNonExecutableJson) + .toCall(builder::generateNonExecutableJson); + map.from(properties::getExcludeFieldsWithoutExposeAnnotation) + .toCall(builder::excludeFieldsWithoutExposeAnnotation); + map.from(properties::getSerializeNulls).toCall(builder::serializeNulls); + map.from(properties::getEnableComplexMapKeySerialization) + .toCall(builder::enableComplexMapKeySerialization); + map.from(properties::getDisableInnerClassSerialization) + .toCall(builder::disableInnerClassSerialization); + map.from(properties::getLongSerializationPolicy) + .to(builder::setLongSerializationPolicy); + map.from(properties::getFieldNamingPolicy).to(builder::setFieldNamingPolicy); + map.from(properties::getPrettyPrinting).toCall(builder::setPrettyPrinting); + map.from(properties::getLenient).toCall(builder::setLenient); + map.from(properties::getDisableHtmlEscaping) + .toCall(builder::disableHtmlEscaping); + map.from(properties::getDateFormat).to(builder::setDateFormat); + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonBuilderCustomizer.java new file mode 100644 index 0000000000..cecf888ed4 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonBuilderCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2018 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.boot.autoconfigure.gson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * Callback interface that can be implemented by beans wishing to further customize the + * {@link Gson} via {@link GsonBuilder} retaining its default auto-configuration. + * + * @author Ivan Golovko + * @since 2.0.0 + */ +@FunctionalInterface +public interface GsonBuilderCustomizer { + + /** + * Customize the GsonBuilder. + * @param gsonBuilder the GsonBuilder to customize + */ + void customize(GsonBuilder gsonBuilder); +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonProperties.java new file mode 100644 index 0000000000..d918d5c52f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonProperties.java @@ -0,0 +1,184 @@ +/* + * Copyright 2012-2018 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.boot.autoconfigure.gson; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.LongSerializationPolicy; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties to configure {@link Gson}. + * + * @author Ivan Golovko + * @since 2.0.0 + */ +@ConfigurationProperties(prefix = "spring.gson") +public class GsonProperties { + + /** + * Whether to generate non executable JSON by prefixing the output with some special + * text. + */ + private Boolean generateNonExecutableJson; + + /** + * Whether to exclude all fields from consideration for serialization or + * deserialization that do not have the "Expose" annotation. + */ + private Boolean excludeFieldsWithoutExposeAnnotation; + + /** + * Whether to to serialize null fields. + */ + private Boolean serializeNulls; + + /** + * Whether to enabled serialization of complex map keys (i.e. non-primitives). + */ + private Boolean enableComplexMapKeySerialization; + + /** + * Whether to exclude inner classes during serialization. + */ + private Boolean disableInnerClassSerialization; + + /** + * Serialization policy for Long and long types. + */ + private LongSerializationPolicy longSerializationPolicy; + + /** + * The naming policy that should be applied to an object's field during serialization + * and deserialization. + */ + private FieldNamingPolicy fieldNamingPolicy; + + /** + * Whether to output serialized JSON that fits in a page for pretty printing. + */ + private Boolean prettyPrinting; + + /** + * Whether to be lenient about parsing JSON that doesn't conform to RFC 4627. + */ + private Boolean lenient; + + /** + * Whether to disable the escaping of HTML characters such as '<' '>' etc. + */ + private Boolean disableHtmlEscaping; + + /** + * The format to use when serializing Date objects. + */ + private String dateFormat; + + public Boolean getGenerateNonExecutableJson() { + return this.generateNonExecutableJson; + } + + public void setGenerateNonExecutableJson(Boolean generateNonExecutableJson) { + this.generateNonExecutableJson = generateNonExecutableJson; + } + + public Boolean getExcludeFieldsWithoutExposeAnnotation() { + return this.excludeFieldsWithoutExposeAnnotation; + } + + public void setExcludeFieldsWithoutExposeAnnotation( + Boolean excludeFieldsWithoutExposeAnnotation) { + this.excludeFieldsWithoutExposeAnnotation = excludeFieldsWithoutExposeAnnotation; + } + + public Boolean getSerializeNulls() { + return this.serializeNulls; + } + + public void setSerializeNulls(Boolean serializeNulls) { + this.serializeNulls = serializeNulls; + } + + public Boolean getEnableComplexMapKeySerialization() { + return this.enableComplexMapKeySerialization; + } + + public void setEnableComplexMapKeySerialization( + Boolean enableComplexMapKeySerialization) { + this.enableComplexMapKeySerialization = enableComplexMapKeySerialization; + } + + public Boolean getDisableInnerClassSerialization() { + return this.disableInnerClassSerialization; + } + + public void setDisableInnerClassSerialization( + Boolean disableInnerClassSerialization) { + this.disableInnerClassSerialization = disableInnerClassSerialization; + } + + public LongSerializationPolicy getLongSerializationPolicy() { + return this.longSerializationPolicy; + } + + public void setLongSerializationPolicy( + LongSerializationPolicy longSerializationPolicy) { + this.longSerializationPolicy = longSerializationPolicy; + } + + public FieldNamingPolicy getFieldNamingPolicy() { + return this.fieldNamingPolicy; + } + + public void setFieldNamingPolicy(FieldNamingPolicy fieldNamingPolicy) { + this.fieldNamingPolicy = fieldNamingPolicy; + } + + public Boolean getPrettyPrinting() { + return this.prettyPrinting; + } + + public void setPrettyPrinting(Boolean prettyPrinting) { + this.prettyPrinting = prettyPrinting; + } + + public Boolean getLenient() { + return this.lenient; + } + + public void setLenient(Boolean lenient) { + this.lenient = lenient; + } + + public Boolean getDisableHtmlEscaping() { + return this.disableHtmlEscaping; + } + + public void setDisableHtmlEscaping(Boolean disableHtmlEscaping) { + this.disableHtmlEscaping = disableHtmlEscaping; + } + + public String getDateFormat() { + return this.dateFormat; + } + + public void setDateFormat(String dateFormat) { + this.dateFormat = dateFormat; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfigurationTests.java index fe20419b88..5e2cc2cfc3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -16,12 +16,22 @@ package org.springframework.boot.autoconfigure.gson; +import java.lang.reflect.Field; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; -import org.junit.After; -import org.junit.Before; +import com.google.gson.LongSerializationPolicy; +import org.joda.time.DateTime; import org.junit.Test; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; import static org.assertj.core.api.Assertions.assertThat; @@ -29,36 +39,233 @@ import static org.assertj.core.api.Assertions.assertThat; * Tests for {@link GsonAutoConfiguration}. * * @author David Liu + * @author Ivan Golovko */ public class GsonAutoConfigurationTests { - AnnotationConfigApplicationContext context; - - @Before - public void setUp() { - this.context = new AnnotationConfigApplicationContext(); - } - - @After - public void tearDown() { - if (this.context != null) { - this.context.close(); - } - } + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GsonAutoConfiguration.class)); @Test public void gsonRegistration() { - this.context.register(GsonAutoConfiguration.class); - this.context.refresh(); - Gson gson = this.context.getBean(Gson.class); - assertThat(gson.toJson(new DataObject())).isEqualTo("{\"data\":\"hello\"}"); + this.contextRunner.run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.toJson(new DataObject())).isEqualTo("{\"data\":1}"); + }); + } + + @Test + public void generateNonExecutableJson() { + this.contextRunner + .withPropertyValues("spring.gson.generate-non-executable-json:true") + .run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.toJson(new DataObject())) + .isNotEqualTo("{\"data\":1}"); + assertThat(gson.toJson(new DataObject())).endsWith("{\"data\":1}"); + }); + } + + @Test + public void excludeFieldsWithoutExposeAnnotation() { + this.contextRunner + .withPropertyValues( + "spring.gson.exclude-fields-without-expose-annotation:true") + .run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.toJson(new DataObject())).isEqualTo("{}"); + }); + } + + @Test + public void serializeNulls() { + this.contextRunner.withPropertyValues("spring.gson.serialize-nulls:true") + .run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.serializeNulls()).isTrue(); + }); + } + + @Test + public void enableComplexMapKeySerialization() { + this.contextRunner + .withPropertyValues( + "spring.gson.enable-complex-map-key-serialization:true") + .run(context -> { + Gson gson = context.getBean(Gson.class); + Map original = new LinkedHashMap<>(); + original.put(new DataObject(), "a"); + assertThat(gson.toJson(original)).isEqualTo("[[{\"data\":1},\"a\"]]"); + }); + } + + @Test + public void notDisableInnerClassSerialization() { + this.contextRunner.run(context -> { + Gson gson = context.getBean(Gson.class); + WrapperObject wrapperObject = new WrapperObject(); + assertThat(gson.toJson(wrapperObject.new NestedObject())) + .isEqualTo("{\"data\":\"nested\"}"); + }); + } + + @Test + public void disableInnerClassSerialization() { + this.contextRunner + .withPropertyValues("spring.gson.disable-inner-class-serialization:true") + .run(context -> { + Gson gson = context.getBean(Gson.class); + WrapperObject wrapperObject = new WrapperObject(); + assertThat(gson.toJson(wrapperObject.new NestedObject())) + .isEqualTo("null"); + }); + } + + @Test + public void withLongSerializationPolicy() { + this.contextRunner.withPropertyValues( + "spring.gson.long-serialization-policy:" + LongSerializationPolicy.STRING) + .run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.toJson(new DataObject())) + .isEqualTo("{\"data\":\"1\"}"); + }); + } + + @Test + public void withFieldNamingPolicy() { + FieldNamingPolicy fieldNamingPolicy = FieldNamingPolicy.UPPER_CAMEL_CASE; + this.contextRunner + .withPropertyValues( + "spring.gson.field-naming-policy:" + fieldNamingPolicy) + .run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.fieldNamingStrategy()).isEqualTo(fieldNamingPolicy); + }); + } + + @Test + public void additionalGsonBuilderCustomization() { + this.contextRunner.withUserConfiguration(GsonBuilderCustomConfig.class) + .run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.toJson(new DataObject())).isEqualTo("{}"); + }); + } + + @Test + public void withPrettyPrinting() { + this.contextRunner.withPropertyValues("spring.gson.pretty-printing:true") + .run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.toJson(new DataObject())) + .isEqualTo("{\n \"data\": 1\n}"); + }); + } + + @Test + public void withoutLenient() throws Exception { + this.contextRunner.run(context -> { + Gson gson = context.getBean(Gson.class); + /* + * It seems, that lenient setting not work in version 2.8.2 We get access to + * it via reflection + */ + Field lenientField = gson.getClass().getDeclaredField("lenient"); + lenientField.setAccessible(true); + boolean lenient = lenientField.getBoolean(gson); + + assertThat(lenient).isFalse(); + }); + } + + @Test + public void withLenient() throws Exception { + this.contextRunner.withPropertyValues("spring.gson.lenient:true").run(context -> { + Gson gson = context.getBean(Gson.class); + + /* + * It seems, that lenient setting not work in version 2.8.0 of gson We get + * access to it via reflection + */ + Field lenientField = gson.getClass().getDeclaredField("lenient"); + lenientField.setAccessible(true); + boolean lenient = lenientField.getBoolean(gson); + + assertThat(lenient).isTrue(); + }); + } + + @Test + public void withHtmlEscaping() { + this.contextRunner.run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.htmlSafe()).isTrue(); + }); + } + + @Test + public void withoutHtmlEscaping() { + this.contextRunner.withPropertyValues("spring.gson.disable-html-escaping:true") + .run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.htmlSafe()).isFalse(); + }); + + } + + @Test + public void customDateFormat() { + this.contextRunner.withPropertyValues("spring.gson.date-format:H") + .run(context -> { + Gson gson = context.getBean(Gson.class); + DateTime dateTime = new DateTime(1988, 6, 25, 20, 30); + Date date = dateTime.toDate(); + assertThat(gson.toJson(date)).isEqualTo("\"20\""); + }); + } + + protected static class GsonBuilderCustomConfig { + + @Bean + public GsonBuilderCustomizer customSerializationExclusionStrategy() { + return (gsonBuilder) -> gsonBuilder + .addSerializationExclusionStrategy(new ExclusionStrategy() { + @Override + public boolean shouldSkipField(FieldAttributes fieldAttributes) { + return "data".equals(fieldAttributes.getName()); + } + + @Override + public boolean shouldSkipClass(Class aClass) { + return false; + } + }); + } + } public class DataObject { + public static final String STATIC_DATA = "bye"; + @SuppressWarnings("unused") - private String data = "hello"; + private Long data = 1L; + + public void setData(Long data) { + this.data = data; + } } + public class WrapperObject { + + @SuppressWarnings("unused") + class NestedObject { + + private String data = "nested"; + + } + } + } diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 9c5d4781c3..87a0a09b09 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -340,6 +340,19 @@ content into your application. Rather, pick only the properties that you need. spring.jackson.serialization.*= # Jackson on/off features that affect the way Java objects are serialized. spring.jackson.time-zone= # Time zone used when formatting dates. For instance, "America/Los_Angeles" or "GMT+10". + # GSON ({sc-spring-boot-autoconfigure}/gson/GsonProperties.{sc-ext}[GsonProperties]) + spring.gson.date-format= # The format to use when serializing Date objects. + spring.gson.disable-html-escaping= # Whether to disable the escaping of HTML characters such as '<' '>' etc. + spring.gson.disable-inner-class-serialization= # Whether to exclude inner classes during serialization. + spring.gson.enable-complex-map-key-serialization= # Whether to enabled serialization of complex map keys (i.e. non-primitives). + spring.gson.exclude-fields-without-expose-annotation= # Whether to exclude all fields from consideration for serialization or deserialization that do not have the "Expose" annotation. + spring.gson.field-naming-policy= # The naming policy that should be applied to an object's field during serialization and deserialization. + spring.gson.generate-non-executable-json= # Whether to generate non executable JSON by prefixing the output with some special text. + spring.gson.lenient= # Whether to be lenient about parsing JSON that doesn't conform to RFC 4627. + spring.gson.long-serialization-policy= # Serialization policy for Long and long types. + spring.gson.pretty-printing= # Whether to output serialized JSON that fits in a page for pretty printing. + spring.gson.serialize-nulls= # Whether to to serialize null fields. + # JERSEY ({sc-spring-boot-autoconfigure}/jersey/JerseyProperties.{sc-ext}[JerseyProperties]) spring.jersey.application-path= # Path that serves as the base URI for the application. If specified, overrides the value of "@ApplicationPath". spring.jersey.filter.order=0 # Jersey filter chain order.