Add failure analyzer for invalid config property name

Closes gh-9545
pull/9669/head
Madhura Bhave 7 years ago
parent c40668ecbd
commit b19b515549

@ -18,6 +18,7 @@ package org.springframework.boot.context.properties.source;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@ -174,6 +175,7 @@ public final class ConfigurationPropertyName
* value.
* @param elementValue the single element value to append
* @return a new {@link ConfigurationPropertyName}
* @throws InvalidConfigurationPropertyNameException if elementValue is not valid
*/
public ConfigurationPropertyName append(String elementValue) {
if (elementValue == null) {
@ -182,9 +184,12 @@ public final class ConfigurationPropertyName
process(elementValue, '.', (value, start, end, indexed) -> Assert.isTrue(
start == 0,
() -> "Element value '" + elementValue + "' must be a single item"));
Assert.isTrue(
isIndexed(elementValue) || ElementValidator.isValidElement(elementValue),
() -> "Element value '" + elementValue + "' is not valid");
if (!isIndexed(elementValue)) {
List<Character> invalidChars = ElementValidator.getInvalidChars(elementValue);
if (!invalidChars.isEmpty()) {
throw new InvalidConfigurationPropertyNameException(invalidChars, elementValue);
}
}
int length = this.elements.length;
CharSequence[] elements = new CharSequence[length + 1];
System.arraycopy(this.elements, 0, elements, 0, length);
@ -436,14 +441,13 @@ public final class ConfigurationPropertyName
* Return a {@link ConfigurationPropertyName} for the specified string.
* @param name the source name
* @return a {@link ConfigurationPropertyName} instance
* @throws IllegalArgumentException if the name is not valid
* @throws InvalidConfigurationPropertyNameException if the name is not valid
*/
public static ConfigurationPropertyName of(CharSequence name) {
Assert.notNull(name, "Name must not be null");
if (name.length() >= 1
&& (name.charAt(0) == '.' || name.charAt(name.length() - 1) == '.')) {
throw new IllegalArgumentException(
"Configuration property name '" + name + "' is not valid");
throw new InvalidConfigurationPropertyNameException(Collections.singletonList('.'), name);
}
if (name.length() == 0) {
return EMPTY;
@ -451,8 +455,12 @@ public final class ConfigurationPropertyName
List<CharSequence> elements = new ArrayList<CharSequence>(10);
process(name, '.', (elementValue, start, end, indexed) -> {
if (elementValue.length() > 0) {
Assert.isTrue(indexed || ElementValidator.isValidElement(elementValue),
() -> "Configuration property name '" + name + "' is not valid");
if (!indexed) {
List<Character> invalidChars = ElementValidator.getInvalidChars(elementValue);
if (!invalidChars.isEmpty()) {
throw new InvalidConfigurationPropertyNameException(invalidChars, name);
}
}
elements.add(elementValue);
}
});
@ -651,12 +659,7 @@ public final class ConfigurationPropertyName
}
public static boolean isValidElement(CharSequence elementValue) {
for (int i = 0; i < elementValue.length(); i++) {
if (!isValidChar(elementValue.charAt(i), i)) {
return false;
}
}
return true;
return getInvalidChars(elementValue).isEmpty();
}
public static boolean isValidChar(char ch, int index) {
@ -668,6 +671,17 @@ public final class ConfigurationPropertyName
return isAlpha || isNumeric || ch == '-';
}
private static List<Character> getInvalidChars(CharSequence elementValue) {
List<Character> chars = new ArrayList<>();
for (int i = 0; i < elementValue.length(); i++) {
char ch = elementValue.charAt(i);
if (!isValidChar(ch, i)) {
chars.add(ch);
}
}
return chars;
}
}
}

@ -0,0 +1,49 @@
/*
* 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.context.properties.source;
import java.util.List;
/**
* Exception thrown when {@link ConfigurationPropertyName} has invalid
* characters.
*
* @author Madhura Bhave
* @since 2.0.0
*/
public class InvalidConfigurationPropertyNameException extends RuntimeException {
private final List<Character> invalidCharacters;
private final CharSequence name;
public InvalidConfigurationPropertyNameException(List<Character> invalidCharacters, CharSequence name) {
super("Configuration property name '" + name + "' is not valid");
this.invalidCharacters = invalidCharacters;
this.name = name;
}
public List<Character> getInvalidCharacters() {
return this.invalidCharacters;
}
public CharSequence getName() {
return this.name;
}
}

@ -0,0 +1,57 @@
/*
* 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.stream.Collectors;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyNameException;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
/**
* An {@link AbstractFailureAnalyzer} that performs analysis of failures
* caused by {@link InvalidConfigurationPropertyNameException}.
*
* @author Madhura Bhave
* @since 2.0.0
*/
public class InvalidConfigurationPropertyNameFailureAnalyzer extends AbstractFailureAnalyzer<InvalidConfigurationPropertyNameException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, InvalidConfigurationPropertyNameException cause) {
BeanCreationException exception = findCause(rootFailure, BeanCreationException.class);
String description = buildDescription(cause, exception);
String action = String.format("Modify '%s' so that it conforms to the canonical names requirements.", cause.getName());
return new FailureAnalysis(description, action, cause);
}
private String buildDescription(InvalidConfigurationPropertyNameException cause, BeanCreationException exception) {
StringBuilder description = new StringBuilder(
String.format("Configuration property name '%s' is not valid:%n", cause.getName()));
String invalid = cause.getInvalidCharacters().stream().map(c -> "'" + c.toString() + "'").collect(Collectors.joining(","));
description.append(String.format("%n Invalid characters: %s", invalid));
if (exception != null) {
description.append(String.format("%n Bean: %s", exception.getBeanName()));
}
description.append(String.format("%n Reason: Canonical names should be kebab-case('-' separated), lowercase alpha-numeric characters" +
" and must start with a letter"));
return description.toString();
}
}

@ -46,7 +46,8 @@ org.springframework.boot.diagnostics.analyzer.UnboundConfigurationPropertyFailur
org.springframework.boot.diagnostics.analyzer.ConnectorStartFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer
org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyNameFailureAnalyzer
# FailureAnalysisReporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\

@ -51,35 +51,35 @@ public class ConfigurationPropertyNameTests {
@Test
public void ofNameShouldNotStartWithNumber() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
this.thrown.expectMessage("is not valid");
ConfigurationPropertyName.of("1foo");
}
@Test
public void ofNameShouldNotStartWithDash() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
this.thrown.expectMessage("is not valid");
ConfigurationPropertyName.of("-foo");
}
@Test
public void ofNameShouldNotStartWithDot() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
this.thrown.expectMessage("is not valid");
ConfigurationPropertyName.of(".foo");
}
@Test
public void ofNameShouldNotEndWithDot() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
this.thrown.expectMessage("is not valid");
ConfigurationPropertyName.of("foo.");
}
@Test
public void ofNameShouldNotContainUppercase() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
this.thrown.expectMessage("is not valid");
ConfigurationPropertyName.of("fOo");
}
@ -92,7 +92,7 @@ public class ConfigurationPropertyNameTests {
ConfigurationPropertyName.of("foo" + c);
fail("Did not throw for invalid char " + c);
}
catch (IllegalArgumentException ex) {
catch (InvalidConfigurationPropertyNameException ex) {
assertThat(ex.getMessage()).contains("is not valid");
}
}
@ -163,21 +163,21 @@ public class ConfigurationPropertyNameTests {
@Test
public void ofNameWhenMissingCloseBracket() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
this.thrown.expectMessage("is not valid");
ConfigurationPropertyName.of("[bar");
}
@Test
public void ofNameWhenMissingOpenBracket() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
this.thrown.expectMessage("is not valid");
ConfigurationPropertyName.of("bar]");
}
@Test
public void ofNameWhenMultipleMismatchedBrackets() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
this.thrown.expectMessage("is not valid");
ConfigurationPropertyName.of("[a[[[b]ar]");
}
@ -192,7 +192,7 @@ public class ConfigurationPropertyNameTests {
@Test
public void ofNameWithWhitespaceInName() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
this.thrown.expectMessage("is not valid");
ConfigurationPropertyName.of("foo. bar");
}
@ -420,8 +420,8 @@ public class ConfigurationPropertyNameTests {
@Test
public void appendWhenElementNameIsNotValidShouldThrowException() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Element value '1bar' is not valid");
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
this.thrown.expectMessage("Configuration property name '1bar' is not valid");
ConfigurationPropertyName.of("foo").append("1bar");
}

@ -0,0 +1,86 @@
/*
* 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.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.annotation.Bean;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link InvalidConfigurationPropertyNameFailureAnalyzer}.
*
* @author Madhura Bhave
*/
public class InvalidConfigurationPropertyNameFailureAnalyzerTests {
private InvalidConfigurationPropertyNameFailureAnalyzer analyzer = new InvalidConfigurationPropertyNameFailureAnalyzer();
@Test
public void analysisWhenRootCauseIsBeanCreationFailureShouldContainBeanName() throws Exception {
BeanCreationException failure = createFailure(InvalidPrefixConfiguration.class);
FailureAnalysis analysis = this.analyzer.analyze(failure);
assertThat(analysis.getDescription()).contains(String.format(
"%n Invalid characters: %s%n Bean: %s%n Reason: %s",
"'F','P'", "invalidPrefixProperties", "Canonical names should be kebab-case('-' separated), " +
"lowercase alpha-numeric characters and must start with a letter"));
}
private BeanCreationException createFailure(Class<?> configuration) {
try {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(configuration);
context.refresh();
context.close();
return null;
}
catch (BeanCreationException ex) {
return ex;
}
}
@EnableConfigurationProperties(InvalidPrefixProperties.class)
static class InvalidPrefixConfiguration {
@Bean(name = "invalidPrefixProperties")
public InvalidPrefixProperties invalidPrefixProperties() {
return new InvalidPrefixProperties();
}
}
@ConfigurationProperties("FooPrefix")
static class InvalidPrefixProperties {
private String value;
public String getValue() {
return this.value;
}
public void setValue(String value) {
this.value = value;
}
}
}
Loading…
Cancel
Save