Skip to content

Commit b19b515

Browse files
committed
Add failure analyzer for invalid config property name
Closes gh-9545
1 parent c40668e commit b19b515

File tree

6 files changed

+234
-27
lines changed

6 files changed

+234
-27
lines changed

spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.ArrayList;
2020
import java.util.Collection;
21+
import java.util.Collections;
2122
import java.util.List;
2223
import java.util.Map;
2324
import java.util.function.Function;
@@ -174,6 +175,7 @@ public int getNumberOfElements() {
174175
* value.
175176
* @param elementValue the single element value to append
176177
* @return a new {@link ConfigurationPropertyName}
178+
* @throws InvalidConfigurationPropertyNameException if elementValue is not valid
177179
*/
178180
public ConfigurationPropertyName append(String elementValue) {
179181
if (elementValue == null) {
@@ -182,9 +184,12 @@ public ConfigurationPropertyName append(String elementValue) {
182184
process(elementValue, '.', (value, start, end, indexed) -> Assert.isTrue(
183185
start == 0,
184186
() -> "Element value '" + elementValue + "' must be a single item"));
185-
Assert.isTrue(
186-
isIndexed(elementValue) || ElementValidator.isValidElement(elementValue),
187-
() -> "Element value '" + elementValue + "' is not valid");
187+
if (!isIndexed(elementValue)) {
188+
List<Character> invalidChars = ElementValidator.getInvalidChars(elementValue);
189+
if (!invalidChars.isEmpty()) {
190+
throw new InvalidConfigurationPropertyNameException(invalidChars, elementValue);
191+
}
192+
}
188193
int length = this.elements.length;
189194
CharSequence[] elements = new CharSequence[length + 1];
190195
System.arraycopy(this.elements, 0, elements, 0, length);
@@ -436,23 +441,26 @@ public static boolean isValid(CharSequence name) {
436441
* Return a {@link ConfigurationPropertyName} for the specified string.
437442
* @param name the source name
438443
* @return a {@link ConfigurationPropertyName} instance
439-
* @throws IllegalArgumentException if the name is not valid
444+
* @throws InvalidConfigurationPropertyNameException if the name is not valid
440445
*/
441446
public static ConfigurationPropertyName of(CharSequence name) {
442447
Assert.notNull(name, "Name must not be null");
443448
if (name.length() >= 1
444449
&& (name.charAt(0) == '.' || name.charAt(name.length() - 1) == '.')) {
445-
throw new IllegalArgumentException(
446-
"Configuration property name '" + name + "' is not valid");
450+
throw new InvalidConfigurationPropertyNameException(Collections.singletonList('.'), name);
447451
}
448452
if (name.length() == 0) {
449453
return EMPTY;
450454
}
451455
List<CharSequence> elements = new ArrayList<CharSequence>(10);
452456
process(name, '.', (elementValue, start, end, indexed) -> {
453457
if (elementValue.length() > 0) {
454-
Assert.isTrue(indexed || ElementValidator.isValidElement(elementValue),
455-
() -> "Configuration property name '" + name + "' is not valid");
458+
if (!indexed) {
459+
List<Character> invalidChars = ElementValidator.getInvalidChars(elementValue);
460+
if (!invalidChars.isEmpty()) {
461+
throw new InvalidConfigurationPropertyNameException(invalidChars, name);
462+
}
463+
}
456464
elements.add(elementValue);
457465
}
458466
});
@@ -651,12 +659,7 @@ public boolean isValid() {
651659
}
652660

653661
public static boolean isValidElement(CharSequence elementValue) {
654-
for (int i = 0; i < elementValue.length(); i++) {
655-
if (!isValidChar(elementValue.charAt(i), i)) {
656-
return false;
657-
}
658-
}
659-
return true;
662+
return getInvalidChars(elementValue).isEmpty();
660663
}
661664

662665
public static boolean isValidChar(char ch, int index) {
@@ -668,6 +671,17 @@ public static boolean isValidChar(char ch, int index) {
668671
return isAlpha || isNumeric || ch == '-';
669672
}
670673

674+
private static List<Character> getInvalidChars(CharSequence elementValue) {
675+
List<Character> chars = new ArrayList<>();
676+
for (int i = 0; i < elementValue.length(); i++) {
677+
char ch = elementValue.charAt(i);
678+
if (!isValidChar(ch, i)) {
679+
chars.add(ch);
680+
}
681+
}
682+
return chars;
683+
}
684+
671685
}
672686

673687
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2012-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.context.properties.source;
18+
19+
import java.util.List;
20+
21+
/**
22+
* Exception thrown when {@link ConfigurationPropertyName} has invalid
23+
* characters.
24+
*
25+
* @author Madhura Bhave
26+
* @since 2.0.0
27+
*/
28+
public class InvalidConfigurationPropertyNameException extends RuntimeException {
29+
30+
private final List<Character> invalidCharacters;
31+
32+
private final CharSequence name;
33+
34+
public InvalidConfigurationPropertyNameException(List<Character> invalidCharacters, CharSequence name) {
35+
super("Configuration property name '" + name + "' is not valid");
36+
this.invalidCharacters = invalidCharacters;
37+
this.name = name;
38+
}
39+
40+
public List<Character> getInvalidCharacters() {
41+
return this.invalidCharacters;
42+
}
43+
44+
public CharSequence getName() {
45+
return this.name;
46+
}
47+
48+
}
49+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2012-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.diagnostics.analyzer;
18+
19+
import java.util.stream.Collectors;
20+
21+
import org.springframework.beans.factory.BeanCreationException;
22+
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyNameException;
23+
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
24+
import org.springframework.boot.diagnostics.FailureAnalysis;
25+
26+
/**
27+
* An {@link AbstractFailureAnalyzer} that performs analysis of failures
28+
* caused by {@link InvalidConfigurationPropertyNameException}.
29+
*
30+
* @author Madhura Bhave
31+
* @since 2.0.0
32+
*/
33+
public class InvalidConfigurationPropertyNameFailureAnalyzer extends AbstractFailureAnalyzer<InvalidConfigurationPropertyNameException> {
34+
35+
@Override
36+
protected FailureAnalysis analyze(Throwable rootFailure, InvalidConfigurationPropertyNameException cause) {
37+
BeanCreationException exception = findCause(rootFailure, BeanCreationException.class);
38+
String description = buildDescription(cause, exception);
39+
String action = String.format("Modify '%s' so that it conforms to the canonical names requirements.", cause.getName());
40+
return new FailureAnalysis(description, action, cause);
41+
}
42+
43+
private String buildDescription(InvalidConfigurationPropertyNameException cause, BeanCreationException exception) {
44+
StringBuilder description = new StringBuilder(
45+
String.format("Configuration property name '%s' is not valid:%n", cause.getName()));
46+
String invalid = cause.getInvalidCharacters().stream().map(c -> "'" + c.toString() + "'").collect(Collectors.joining(","));
47+
description.append(String.format("%n Invalid characters: %s", invalid));
48+
if (exception != null) {
49+
description.append(String.format("%n Bean: %s", exception.getBeanName()));
50+
}
51+
description.append(String.format("%n Reason: Canonical names should be kebab-case('-' separated), lowercase alpha-numeric characters" +
52+
" and must start with a letter"));
53+
return description.toString();
54+
}
55+
56+
}
57+

spring-boot/src/main/resources/META-INF/spring.factories

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ org.springframework.boot.diagnostics.analyzer.UnboundConfigurationPropertyFailur
4646
org.springframework.boot.diagnostics.analyzer.ConnectorStartFailureAnalyzer,\
4747
org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\
4848
org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\
49-
org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer
49+
org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer,\
50+
org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyNameFailureAnalyzer
5051

5152
# FailureAnalysisReporters
5253
org.springframework.boot.diagnostics.FailureAnalysisReporter=\

spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,35 +51,35 @@ public void ofNameShouldNotBeNull() throws Exception {
5151

5252
@Test
5353
public void ofNameShouldNotStartWithNumber() throws Exception {
54-
this.thrown.expect(IllegalArgumentException.class);
54+
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
5555
this.thrown.expectMessage("is not valid");
5656
ConfigurationPropertyName.of("1foo");
5757
}
5858

5959
@Test
6060
public void ofNameShouldNotStartWithDash() throws Exception {
61-
this.thrown.expect(IllegalArgumentException.class);
61+
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
6262
this.thrown.expectMessage("is not valid");
6363
ConfigurationPropertyName.of("-foo");
6464
}
6565

6666
@Test
6767
public void ofNameShouldNotStartWithDot() throws Exception {
68-
this.thrown.expect(IllegalArgumentException.class);
68+
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
6969
this.thrown.expectMessage("is not valid");
7070
ConfigurationPropertyName.of(".foo");
7171
}
7272

7373
@Test
7474
public void ofNameShouldNotEndWithDot() throws Exception {
75-
this.thrown.expect(IllegalArgumentException.class);
75+
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
7676
this.thrown.expectMessage("is not valid");
7777
ConfigurationPropertyName.of("foo.");
7878
}
7979

8080
@Test
8181
public void ofNameShouldNotContainUppercase() throws Exception {
82-
this.thrown.expect(IllegalArgumentException.class);
82+
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
8383
this.thrown.expectMessage("is not valid");
8484
ConfigurationPropertyName.of("fOo");
8585
}
@@ -92,7 +92,7 @@ public void ofNameShouldNotContainInvalidChars() throws Exception {
9292
ConfigurationPropertyName.of("foo" + c);
9393
fail("Did not throw for invalid char " + c);
9494
}
95-
catch (IllegalArgumentException ex) {
95+
catch (InvalidConfigurationPropertyNameException ex) {
9696
assertThat(ex.getMessage()).contains("is not valid");
9797
}
9898
}
@@ -163,21 +163,21 @@ public void ofNameWhenDoubleDotAndAssociative() throws Exception {
163163

164164
@Test
165165
public void ofNameWhenMissingCloseBracket() throws Exception {
166-
this.thrown.expect(IllegalArgumentException.class);
166+
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
167167
this.thrown.expectMessage("is not valid");
168168
ConfigurationPropertyName.of("[bar");
169169
}
170170

171171
@Test
172172
public void ofNameWhenMissingOpenBracket() throws Exception {
173-
this.thrown.expect(IllegalArgumentException.class);
173+
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
174174
this.thrown.expectMessage("is not valid");
175175
ConfigurationPropertyName.of("bar]");
176176
}
177177

178178
@Test
179179
public void ofNameWhenMultipleMismatchedBrackets() throws Exception {
180-
this.thrown.expect(IllegalArgumentException.class);
180+
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
181181
this.thrown.expectMessage("is not valid");
182182
ConfigurationPropertyName.of("[a[[[b]ar]");
183183
}
@@ -192,7 +192,7 @@ public void ofNameWhenNestedBrackets() throws Exception {
192192

193193
@Test
194194
public void ofNameWithWhitespaceInName() throws Exception {
195-
this.thrown.expect(IllegalArgumentException.class);
195+
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
196196
this.thrown.expectMessage("is not valid");
197197
ConfigurationPropertyName.of("foo. bar");
198198
}
@@ -420,8 +420,8 @@ public void appendWhenIndexedShouldAppendWithBrackets() throws Exception {
420420

421421
@Test
422422
public void appendWhenElementNameIsNotValidShouldThrowException() throws Exception {
423-
this.thrown.expect(IllegalArgumentException.class);
424-
this.thrown.expectMessage("Element value '1bar' is not valid");
423+
this.thrown.expect(InvalidConfigurationPropertyNameException.class);
424+
this.thrown.expectMessage("Configuration property name '1bar' is not valid");
425425
ConfigurationPropertyName.of("foo").append("1bar");
426426
}
427427

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2012-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.diagnostics.analyzer;
18+
19+
import org.junit.Test;
20+
21+
import org.springframework.beans.factory.BeanCreationException;
22+
import org.springframework.boot.context.properties.ConfigurationProperties;
23+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
24+
import org.springframework.boot.diagnostics.FailureAnalysis;
25+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
26+
import org.springframework.context.annotation.Bean;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
30+
/**
31+
* Tests for {@link InvalidConfigurationPropertyNameFailureAnalyzer}.
32+
*
33+
* @author Madhura Bhave
34+
*/
35+
public class InvalidConfigurationPropertyNameFailureAnalyzerTests {
36+
37+
private InvalidConfigurationPropertyNameFailureAnalyzer analyzer = new InvalidConfigurationPropertyNameFailureAnalyzer();
38+
39+
@Test
40+
public void analysisWhenRootCauseIsBeanCreationFailureShouldContainBeanName() throws Exception {
41+
BeanCreationException failure = createFailure(InvalidPrefixConfiguration.class);
42+
FailureAnalysis analysis = this.analyzer.analyze(failure);
43+
assertThat(analysis.getDescription()).contains(String.format(
44+
"%n Invalid characters: %s%n Bean: %s%n Reason: %s",
45+
"'F','P'", "invalidPrefixProperties", "Canonical names should be kebab-case('-' separated), " +
46+
"lowercase alpha-numeric characters and must start with a letter"));
47+
}
48+
49+
private BeanCreationException createFailure(Class<?> configuration) {
50+
try {
51+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
52+
context.register(configuration);
53+
context.refresh();
54+
context.close();
55+
return null;
56+
}
57+
catch (BeanCreationException ex) {
58+
return ex;
59+
}
60+
}
61+
62+
@EnableConfigurationProperties(InvalidPrefixProperties.class)
63+
static class InvalidPrefixConfiguration {
64+
65+
@Bean(name = "invalidPrefixProperties")
66+
public InvalidPrefixProperties invalidPrefixProperties() {
67+
return new InvalidPrefixProperties();
68+
}
69+
70+
}
71+
72+
@ConfigurationProperties("FooPrefix")
73+
static class InvalidPrefixProperties {
74+
75+
private String value;
76+
77+
public String getValue() {
78+
return this.value;
79+
}
80+
81+
public void setValue(String value) {
82+
this.value = value;
83+
}
84+
}
85+
86+
}

0 commit comments

Comments
 (0)