From fbf3c48bf8c2a590d07edf732c54dcc68fadc883 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 8 Aug 2018 11:13:03 +0200 Subject: [PATCH] Fix setter detection in `configprops` endpoint Previously, the setter of a property whose second letter is upper-case ( such as `oAuth2Uri`) was not detected properly. The JavaBean spec states that, in such a case, the first letter should not be capitalized (i.e. the setter should be `setoAuth2Uri` rather than `setOAuth2Uri`). This commit makes sure that Jackson uses standard bean names and fixes the setter detection algorithm to take this case into account. Closes gh-13878 --- ...ConfigurationPropertiesReportEndpoint.java | 21 ++++- ...gurationPropertiesReportEndpointTests.java | 81 ++++++++++++++++++- 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java index d7275ba376b9..06515af9e67b 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java @@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.SerializationFeature; @@ -172,6 +173,7 @@ private Map safeSerialize(ObjectMapper mapper, Object bean, */ protected void configureObjectMapper(ObjectMapper mapper) { mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + mapper.configure(MapperFeature.USE_STD_BEAN_NAMING, true); mapper.setSerializationInclusion(Include.NON_NULL); applyConfigurationPropertiesFilter(mapper); applySerializationModifier(mapper); @@ -375,7 +377,7 @@ private boolean isReadable(BeanDescription beanDesc, BeanPropertyWriter writer) private AnnotatedMethod findSetter(BeanDescription beanDesc, BeanPropertyWriter writer) { - String name = "set" + StringUtils.capitalize(writer.getName()); + String name = "set" + determineAccessorSuffix(writer.getName()); Class type = writer.getType().getRawClass(); AnnotatedMethod setter = beanDesc.findMethod(name, new Class[] { type }); // The enabled property of endpoints returns a boolean primitive but is set @@ -386,6 +388,23 @@ private AnnotatedMethod findSetter(BeanDescription beanDesc, return setter; } + /** + * Determine the accessor suffix of the specified {@code propertyName}, see + * section 8.8 "Capitalization of inferred names" of the JavaBean specs for more + * details. + * @param propertyName the property name to turn into an accessor suffix + * @return the accessor suffix for {@code propertyName} + */ + private String determineAccessorSuffix(String propertyName) { + if (propertyName.length() > 1 + && Character.isUpperCase(propertyName.charAt(1))) { + return propertyName; + } + else { + return StringUtils.capitalize(propertyName); + } + } + } /** diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java index 2c131961a1f0..cfc56337c551 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java @@ -44,6 +44,7 @@ * * @author Dave Syer * @author Andy Wilkinson + * @author Stephane Nicoll */ public class ConfigurationPropertiesReportEndpointTests { @@ -120,6 +121,24 @@ public void keySanitizationWithCustomPatternUsingCompositeKeys() { }); } + @Test + public void nonCamelCaseProperty() { + load((context, properties) -> { + Map nestedProperties = properties.getBeans() + .get("testProperties").getProperties(); + assertThat(nestedProperties.get("myURL")).isEqualTo("https://example.com"); + }); + } + + @Test + public void simpleBoolean() { + load((context, properties) -> { + Map nestedProperties = properties.getBeans() + .get("testProperties").getProperties(); + assertThat(nestedProperties.get("simpleBoolean")).isEqualTo(true); + }); + } + @Test public void mixedBoolean() { load((context, properties) -> { @@ -129,6 +148,24 @@ public void mixedBoolean() { }); } + @Test + public void mixedCase() { + load((context, properties) -> { + Map nestedProperties = properties.getBeans() + .get("testProperties").getProperties(); + assertThat(nestedProperties.get("mIxedCase")).isEqualTo("mixed"); + }); + } + + @Test + public void singleLetterProperty() { + load((context, properties) -> { + Map nestedProperties = properties.getBeans() + .get("testProperties").getProperties(); + assertThat(nestedProperties.get("z")).isEqualTo("zzz"); + }); + } + @Test @SuppressWarnings("unchecked") public void listsAreSanitized() { @@ -219,8 +256,16 @@ public static class TestProperties { private String myTestProperty = "654321"; + private String myURL = "https://example.com"; + + private boolean simpleBoolean = true; + private Boolean mixedBoolean = true; + private String mIxedCase = "mixed"; + + private String z = "zzz"; + private Map secrets = new HashMap<>(); private Hidden hidden = new Hidden(); @@ -254,14 +299,46 @@ public void setMyTestProperty(String myTestProperty) { this.myTestProperty = myTestProperty; } - public boolean isMixedBoolean() { - return (this.mixedBoolean != null) ? this.mixedBoolean : false; + public String getMyURL() { + return this.myURL; + } + + public void setMyURL(String myURL) { + this.myURL = myURL; + } + + public boolean isSimpleBoolean() { + return this.simpleBoolean; + } + + public void setSimpleBoolean(boolean simpleBoolean) { + this.simpleBoolean = simpleBoolean; } public void setMixedBoolean(Boolean mixedBoolean) { this.mixedBoolean = mixedBoolean; } + public boolean isMixedBoolean() { + return (this.mixedBoolean != null) ? this.mixedBoolean : false; + } + + public String getmIxedCase() { + return this.mIxedCase; + } + + public void setmIxedCase(String mIxedCase) { + this.mIxedCase = mIxedCase; + } + + public String getZ() { + return this.z; + } + + public void setZ(String z) { + this.z = z; + } + public Map getSecrets() { return this.secrets; }