From 9ef3a448e6d13a47f0eb9e0c1118015ffb5d7966 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Mon, 1 May 2017 10:11:49 -0700 Subject: [PATCH] Restore BindFailureAnalyzer support Closes gh-9026 --- .../analyzer/BindFailureAnalyzer.java | 56 +---- .../BindValidationFailureAnalyzer.java | 116 +++++++++ ...dConfigurationPropertyFailureAnalyzer.java | 64 +++++ .../main/resources/META-INF/spring.factories | 2 + .../analyzer/BindFailureAnalyzerTests.java | 124 +--------- .../BindValidationFailureAnalyzerTests.java | 221 ++++++++++++++++++ ...igurationPropertyFailureAnalyzerTests.java | 127 ++++++++++ 7 files changed, 545 insertions(+), 165 deletions(-) create mode 100644 spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindValidationFailureAnalyzer.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/UnboundConfigurationPropertyFailureAnalyzer.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindValidationFailureAnalyzerTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/UnboundConfigurationPropertyFailureAnalyzerTests.java diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java index f2888d25d4..bde3fac3c0 100644 --- a/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java +++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java @@ -19,18 +19,14 @@ package org.springframework.boot.diagnostics.analyzer; import org.springframework.boot.context.properties.bind.BindException; import org.springframework.boot.context.properties.bind.UnboundConfigurationPropertiesException; import org.springframework.boot.context.properties.bind.validation.BindValidationException; -import org.springframework.boot.context.properties.bind.validation.ValidationErrors; import org.springframework.boot.context.properties.source.ConfigurationProperty; import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; import org.springframework.boot.diagnostics.FailureAnalysis; -import org.springframework.boot.origin.Origin; import org.springframework.util.StringUtils; -import org.springframework.validation.FieldError; -import org.springframework.validation.ObjectError; /** * An {@link AbstractFailureAnalyzer} that performs analysis of failures caused by a - * {@link BindException}. + * {@link BindException} excluding {@link BindValidationException} and {@link UnboundConfigurationPropertiesException}. * * @author Andy Wilkinson * @author Madhura Bhave @@ -39,54 +35,12 @@ class BindFailureAnalyzer extends AbstractFailureAnalyzer { @Override protected FailureAnalysis analyze(Throwable rootFailure, BindException cause) { - if (cause.getCause() instanceof BindValidationException) { - return analyzeBindValidationException(cause, - (BindValidationException) cause.getCause()); - } - else if (cause.getCause() instanceof UnboundConfigurationPropertiesException) { - return analyzeUnboundConfigurationPropertiesException(cause, - (UnboundConfigurationPropertiesException) cause.getCause()); - } - return analyzeGenericBindException(cause); - } - - private FailureAnalysis analyzeBindValidationException(BindException cause, - BindValidationException validationException) { - ValidationErrors errors = validationException.getValidationErrors(); - if (!errors.hasErrors()) { + Throwable rootCause = cause.getCause(); + if (rootCause instanceof BindValidationException || + rootCause instanceof UnboundConfigurationPropertiesException) { return null; } - StringBuilder description = new StringBuilder( - String.format("Binding to target %s failed:%n", cause.getTarget())); - for (ObjectError error : errors) { - if (error instanceof FieldError) { - appendFieldError(description, (FieldError) error); - } - description.append( - String.format("%n Reason: %s%n", error.getDefaultMessage())); - } - return getFailureAnalysis(description, cause); - } - - private void appendFieldError(StringBuilder description, FieldError error) { - Origin origin = Origin.from(error); - description.append(String.format("%n Property: %s", - error.getObjectName() + "." + error.getField())); - description.append(String.format("%n Value: %s", error.getRejectedValue())); - if (origin != null) { - description.append(String.format("%n Origin: %s", origin)); - } - } - - private FailureAnalysis analyzeUnboundConfigurationPropertiesException( - BindException cause, UnboundConfigurationPropertiesException exception) { - StringBuilder description = new StringBuilder( - String.format("Binding to target %s failed:%n", cause.getTarget())); - for (ConfigurationProperty property : exception.getUnboundProperties()) { - buildDescription(description, property); - description.append(String.format("%n Reason: %s", exception.getMessage())); - } - return getFailureAnalysis(description, cause); + return analyzeGenericBindException(cause); } private FailureAnalysis analyzeGenericBindException(BindException cause) { diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindValidationFailureAnalyzer.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindValidationFailureAnalyzer.java new file mode 100644 index 0000000000..9327cc30f3 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindValidationFailureAnalyzer.java @@ -0,0 +1,116 @@ +/* + * Copyright 2012-2017 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.diagnostics.analyzer; + +import java.util.List; + +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.validation.BindValidationException; +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.origin.Origin; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; + +/** + * An {@link AbstractFailureAnalyzer} that performs analysis of any bind validation failures + * caused by {@link BindValidationException} or {@link org.springframework.validation.BindException}. + * + * @author Madhura Bhave + */ +public class BindValidationFailureAnalyzer extends AbstractFailureAnalyzer { + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, Throwable cause) { + ExceptionDetails details = getBindValidationExceptionDetails(rootFailure); + if (details == null) { + return null; + } + return analyzeBindValidationException(details); + } + + private ExceptionDetails getBindValidationExceptionDetails(Throwable rootFailure) { + BindValidationException validationException = findCause(rootFailure, BindValidationException.class); + if (validationException != null) { + BindException target = findCause(rootFailure, BindException.class); + List errors = validationException.getValidationErrors().getAllErrors(); + return new ExceptionDetails(errors, target, validationException); + } + org.springframework.validation.BindException bindException = findCause(rootFailure, org.springframework.validation.BindException.class); + if (bindException != null) { + List errors = bindException.getAllErrors(); + return new ExceptionDetails(errors, bindException.getTarget(), bindException); + } + return null; + } + + private FailureAnalysis analyzeBindValidationException(ExceptionDetails details) { + StringBuilder description = new StringBuilder( + String.format("Binding to target %s failed:%n", details.getTarget())); + for (ObjectError error : details.getErrors()) { + if (error instanceof FieldError) { + appendFieldError(description, (FieldError) error); + } + description.append( + String.format("%n Reason: %s%n", error.getDefaultMessage())); + } + return getFailureAnalysis(description, details.getCause()); + } + + private void appendFieldError(StringBuilder description, FieldError error) { + Origin origin = Origin.from(error); + description.append(String.format("%n Property: %s", + error.getObjectName() + "." + error.getField())); + description.append(String.format("%n Value: %s", error.getRejectedValue())); + if (origin != null) { + description.append(String.format("%n Origin: %s", origin)); + } + } + + private FailureAnalysis getFailureAnalysis(Object description, Throwable cause) { + return new FailureAnalysis(description.toString(), + "Update your application's configuration", cause); + } + + private static class ExceptionDetails { + + private List errors; + + private Object target; + + private Throwable cause; + + ExceptionDetails(List errors, Object target, Throwable cause) { + this.errors = errors; + this.target = target; + this.cause = cause; + } + + public Object getTarget() { + return this.target; + } + + public List getErrors() { + return this.errors; + } + + public Throwable getCause() { + return this.cause; + } + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/UnboundConfigurationPropertyFailureAnalyzer.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/UnboundConfigurationPropertyFailureAnalyzer.java new file mode 100644 index 0000000000..3598e414d6 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/UnboundConfigurationPropertyFailureAnalyzer.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2017 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.diagnostics.analyzer; + +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.UnboundConfigurationPropertiesException; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; + +/** + * An {@link AbstractFailureAnalyzer} that performs analysis of failures caused by any + * {@link UnboundConfigurationPropertiesException}. + * + * @author Madhura Bhave + */ +public class UnboundConfigurationPropertyFailureAnalyzer extends AbstractFailureAnalyzer { + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, UnboundConfigurationPropertiesException cause) { + BindException exception = findCause(rootFailure, BindException.class); + return analyzeUnboundConfigurationPropertiesException(exception, cause); + } + + private FailureAnalysis analyzeUnboundConfigurationPropertiesException( + BindException cause, UnboundConfigurationPropertiesException exception) { + StringBuilder description = new StringBuilder( + String.format("Binding to target %s failed:%n", cause.getTarget())); + for (ConfigurationProperty property : exception.getUnboundProperties()) { + buildDescription(description, property); + description.append(String.format("%n Reason: %s", exception.getMessage())); + } + return getFailureAnalysis(description, cause); + } + + private void buildDescription(StringBuilder description, + ConfigurationProperty property) { + if (property != null) { + description.append(String.format("%n Property: %s", property.getName())); + description.append(String.format("%n Value: %s", property.getValue())); + description.append(String.format("%n Origin: %s", property.getOrigin())); + } + } + + private FailureAnalysis getFailureAnalysis(Object description, BindException cause) { + return new FailureAnalysis(description.toString(), + "Update your application's configuration", cause); + } + +} diff --git a/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot/src/main/resources/META-INF/spring.factories index 2d81ab15c4..e9d02cb58a 100644 --- a/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot/src/main/resources/META-INF/spring.factories @@ -40,6 +40,8 @@ org.springframework.boot.diagnostics.FailureAnalyzer=\ org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\ +org.springframework.boot.diagnostics.analyzer.BindValidationFailureAnalyzer,\ +org.springframework.boot.diagnostics.analyzer.UnboundConfigurationPropertyFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.ConnectorStartFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\ diff --git a/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java index 978f4f5b7e..83df9e61ef 100644 --- a/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java @@ -18,15 +18,10 @@ package org.springframework.boot.diagnostics.analyzer; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; -import javax.validation.Valid; import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; -import org.junit.After; -import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.BeanCreationException; @@ -34,11 +29,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.diagnostics.FailureAnalysis; import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; -import org.springframework.validation.Errors; -import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; import static org.assertj.core.api.Assertions.assertThat; @@ -51,52 +43,19 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class BindFailureAnalyzerTests { - @Before - public void setup() { - LocaleContextHolder.setLocale(Locale.US); - } - - @After - public void cleanup() { - LocaleContextHolder.resetLocaleContext(); - } - @Test - public void bindExceptionWithFieldErrorsDueToValidationFailure() { - FailureAnalysis analysis = performAnalysis( - FieldValidationFailureConfiguration.class); - assertThat(analysis.getDescription()) - .contains(failure("test.foo.foo", "null", "may not be null")); - assertThat(analysis.getDescription()) - .contains(failure("test.foo.value", "0", "at least five")); - assertThat(analysis.getDescription()) - .contains(failure("test.foo.nested.bar", "null", "may not be null")); - } - - @Test - public void bindExceptionWithObjectErrorsDueToValidationFailure() throws Exception { - FailureAnalysis analysis = performAnalysis( - ObjectValidationFailureConfiguration.class); - assertThat(analysis.getDescription()) - .contains("Reason: This object could not be bound."); - } - - @Test - public void bindExceptionWithOriginDueToValidationFailure() throws Exception { - FailureAnalysis analysis = performAnalysis( - FieldValidationFailureConfiguration.class, "test.foo.value=4"); - assertThat(analysis.getDescription()) - .contains("Origin: \"test.foo.value\" from property source \"test\""); - } - - @Test - public void bindExceptionDueToUnboundElements() throws Exception { + public void analysisForUnboundElementsIsNull() throws Exception { FailureAnalysis analysis = performAnalysis( UnboundElementsFailureConfiguration.class, "test.foo.listValue[0]=hello", "test.foo.listValue[2]=world"); - assertThat(analysis.getDescription()).contains(failure("test.foo.listvalue[2]", - "world", "\"test.foo.listValue[2]\" from property source \"test\"", - "The elements [test.foo.listvalue[2]] were left unbound.")); + assertThat(analysis).isNull(); + } + + @Test + public void analysisForValidationExceptionIsNull() throws Exception { + FailureAnalysis analysis = performAnalysis( + FieldValidationFailureConfiguration.class, "test.foo.value=1"); + assertThat(analysis).isNull(); } @Test @@ -108,11 +67,6 @@ public class BindFailureAnalyzerTests { "Could not resolve placeholder 'BAR' in value \"${BAR}\"")); } - private static String failure(String property, String value, String reason) { - return String.format("Property: %s%n Value: %s%n Reason: %s", property, - value, reason); - } - private static String failure(String property, String value, String origin, String reason) { return String.format( @@ -155,16 +109,11 @@ public class BindFailureAnalyzerTests { sources.addFirst(new MapPropertySource("test", map)); } - @EnableConfigurationProperties(FieldValidationFailureProperties.class) + @EnableConfigurationProperties(BindValidationFailureAnalyzerTests.FieldValidationFailureProperties.class) static class FieldValidationFailureConfiguration { } - @EnableConfigurationProperties(ObjectErrorFailureProperties.class) - static class ObjectValidationFailureConfiguration { - - } - @EnableConfigurationProperties(UnboundElementsFailureProperties.class) static class UnboundElementsFailureConfiguration { @@ -179,23 +128,9 @@ public class BindFailureAnalyzerTests { @Validated static class FieldValidationFailureProperties { - @NotNull - private String foo; - @Min(value = 5, message = "at least five") private int value; - @Valid - private Nested nested = new Nested(); - - public String getFoo() { - return this.foo; - } - - public void setFoo(String foo) { - this.foo = foo; - } - public int getValue() { return this.value; } @@ -204,45 +139,6 @@ public class BindFailureAnalyzerTests { this.value = value; } - public Nested getNested() { - return this.nested; - } - - public void setNested(Nested nested) { - this.nested = nested; - } - - static class Nested { - - @NotNull - private String bar; - - public String getBar() { - return this.bar; - } - - public void setBar(String bar) { - this.bar = bar; - } - - } - - } - - @ConfigurationProperties("foo.bar") - @Validated - static class ObjectErrorFailureProperties implements Validator { - - @Override - public void validate(Object target, Errors errors) { - errors.reject("my.objectError", "This object could not be bound."); - } - - @Override - public boolean supports(Class clazz) { - return true; - } - } @ConfigurationProperties("test.foo") diff --git a/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindValidationFailureAnalyzerTests.java b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindValidationFailureAnalyzerTests.java new file mode 100644 index 0000000000..253cbf1d4e --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindValidationFailureAnalyzerTests.java @@ -0,0 +1,221 @@ +/* + * Copyright 2012-2017 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.diagnostics.analyzer; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import javax.validation.Valid; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; +import org.springframework.validation.FieldError; +import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BindValidationFailureAnalyzer}. + * + * @author Madhura Bhave + */ +public class BindValidationFailureAnalyzerTests { + + @Before + public void setup() { + LocaleContextHolder.setLocale(Locale.US); + } + + @After + public void cleanup() { + LocaleContextHolder.resetLocaleContext(); + } + + @Test + public void bindExceptionWithFieldErrorsDueToValidationFailure() { + FailureAnalysis analysis = performAnalysis( + FieldValidationFailureConfiguration.class); + assertThat(analysis.getDescription()) + .contains(failure("test.foo.foo", "null", "may not be null")); + assertThat(analysis.getDescription()) + .contains(failure("test.foo.value", "0", "at least five")); + assertThat(analysis.getDescription()) + .contains(failure("test.foo.nested.bar", "null", "may not be null")); + } + + @Test + public void bindExceptionWithOriginDueToValidationFailure() throws Exception { + FailureAnalysis analysis = performAnalysis( + FieldValidationFailureConfiguration.class, "test.foo.value=4"); + assertThat(analysis.getDescription()) + .contains("Origin: \"test.foo.value\" from property source \"test\""); + } + + @Test + public void bindExceptionWithObjectErrorsDueToValidationFailure() throws Exception { + FailureAnalysis analysis = performAnalysis( + ObjectValidationFailureConfiguration.class); + assertThat(analysis.getDescription()) + .contains("Reason: This object could not be bound."); + } + + @Test + public void otherBindExceptionShouldReturnAnalysis() throws Exception { + BindException cause = new BindException(new FieldValidationFailureProperties(), "fieldValidationFailureProperties"); + cause.addError(new FieldError("test", "value", "may not be null")); + BeanCreationException rootFailure = new BeanCreationException("bean creation failure", cause); + FailureAnalysis analysis = new BindValidationFailureAnalyzer().analyze(rootFailure, rootFailure); + assertThat(analysis.getDescription()) + .contains(failure("test.value", "null", "may not be null")); + } + + private static String failure(String property, String value, String reason) { + return String.format("Property: %s%n Value: %s%n Reason: %s", property, + value, reason); + } + + private FailureAnalysis performAnalysis(Class configuration, + String... environment) { + BeanCreationException failure = createFailure(configuration, environment); + assertThat(failure).isNotNull(); + return new BindValidationFailureAnalyzer().analyze(failure); + } + + private BeanCreationException createFailure(Class configuration, + String... environment) { + try { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + addEnvironment(context, environment); + context.register(configuration); + context.refresh(); + context.close(); + return null; + } + catch (BeanCreationException ex) { + return ex; + } + } + + private void addEnvironment(AnnotationConfigApplicationContext context, + String[] environment) { + MutablePropertySources sources = context.getEnvironment().getPropertySources(); + Map map = new HashMap<>(); + for (String pair : environment) { + int index = pair.indexOf("="); + String key = pair.substring(0, index > 0 ? index : pair.length()); + String value = index > 0 ? pair.substring(index + 1) : ""; + map.put(key.trim(), value.trim()); + } + sources.addFirst(new MapPropertySource("test", map)); + } + + @EnableConfigurationProperties(FieldValidationFailureProperties.class) + static class FieldValidationFailureConfiguration { + + } + + @EnableConfigurationProperties(ObjectErrorFailureProperties.class) + static class ObjectValidationFailureConfiguration { + + } + + @ConfigurationProperties("test.foo") + @Validated + static class FieldValidationFailureProperties { + + @NotNull + private String foo; + + @Min(value = 5, message = "at least five") + private int value; + + @Valid + private FieldValidationFailureProperties.Nested nested = new FieldValidationFailureProperties.Nested(); + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + public int getValue() { + return this.value; + } + + public void setValue(int value) { + this.value = value; + } + + public FieldValidationFailureProperties.Nested getNested() { + return this.nested; + } + + public void setNested(FieldValidationFailureProperties.Nested nested) { + this.nested = nested; + } + + static class Nested { + + @NotNull + private String bar; + + public String getBar() { + return this.bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + + } + + } + + @ConfigurationProperties("foo.bar") + @Validated + static class ObjectErrorFailureProperties implements Validator { + + @Override + public void validate(Object target, Errors errors) { + errors.reject("my.objectError", "This object could not be bound."); + } + + @Override + public boolean supports(Class clazz) { + return true; + } + + } +} diff --git a/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/UnboundConfigurationPropertyFailureAnalyzerTests.java b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/UnboundConfigurationPropertyFailureAnalyzerTests.java new file mode 100644 index 0000000000..af153a8e60 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/UnboundConfigurationPropertyFailureAnalyzerTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2012-2017 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.diagnostics.analyzer; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link UnboundConfigurationPropertyFailureAnalyzer}. + * + * @author Madhura Bhave + */ +public class UnboundConfigurationPropertyFailureAnalyzerTests { + + @Before + public void setup() { + LocaleContextHolder.setLocale(Locale.US); + } + + @After + public void cleanup() { + LocaleContextHolder.resetLocaleContext(); + } + + @Test + public void bindExceptionDueToUnboundElements() throws Exception { + FailureAnalysis analysis = performAnalysis( + UnboundElementsFailureConfiguration.class, "test.foo.listValue[0]=hello", + "test.foo.listValue[2]=world"); + assertThat(analysis.getDescription()).contains(failure("test.foo.listvalue[2]", + "world", "\"test.foo.listValue[2]\" from property source \"test\"", + "The elements [test.foo.listvalue[2]] were left unbound.")); + } + + private static String failure(String property, String value, String origin, + String reason) { + return String.format( + "Property: %s%n Value: %s%n Origin: %s%n Reason: %s", property, + value, origin, reason); + } + + private FailureAnalysis performAnalysis(Class configuration, + String... environment) { + BeanCreationException failure = createFailure(configuration, environment); + assertThat(failure).isNotNull(); + return new UnboundConfigurationPropertyFailureAnalyzer().analyze(failure); + } + + private BeanCreationException createFailure(Class configuration, + String... environment) { + try { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + addEnvironment(context, environment); + context.register(configuration); + context.refresh(); + context.close(); + return null; + } + catch (BeanCreationException ex) { + return ex; + } + } + + private void addEnvironment(AnnotationConfigApplicationContext context, + String[] environment) { + MutablePropertySources sources = context.getEnvironment().getPropertySources(); + Map map = new HashMap<>(); + for (String pair : environment) { + int index = pair.indexOf("="); + String key = pair.substring(0, index > 0 ? index : pair.length()); + String value = index > 0 ? pair.substring(index + 1) : ""; + map.put(key.trim(), value.trim()); + } + sources.addFirst(new MapPropertySource("test", map)); + } + + @EnableConfigurationProperties(UnboundElementsFailureProperties.class) + static class UnboundElementsFailureConfiguration { + + } + + @ConfigurationProperties("test.foo") + static class UnboundElementsFailureProperties { + + private List listValue; + + public List getListValue() { + return this.listValue; + } + + public void setListValue(List listValue) { + this.listValue = listValue; + } + } + +}