Skip to content

Commit 5863e6f

Browse files
committed
Fix class name in generated meta-data
Previously, the algorithm that computes the String representation of a class reference and a property type was shared. This lead to generic information for group's `type` and `sourceType` property. This commit separates that logic in two: `getQualifiedName` is now responsible to generate a fully qualified class name while the existing `getType` is solely responsible to generate a type representation for the property. Only the latter has generic information. Closes gh-7236
1 parent bd2956c commit 5863e6f

File tree

6 files changed

+190
-19
lines changed

6 files changed

+190
-19
lines changed

spring-boot-docs/src/main/asciidoc/appendix-configuration-metadata.adoc

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,13 @@ The JSON object contained in the `properties` array can contain the following at
164164

165165
|`type`
166166
| String
167-
| The class name of the data type of the property. For example, `java.lang.String`. This
168-
attribute can be used to guide the user as to the types of values that they can enter.
169-
For consistency, the type of a primitive is specified using its wrapper counterpart,
170-
i.e. `boolean` becomes `java.lang.Boolean`. Note that this class may be a complex type
171-
that gets converted from a String as values are bound. May be omitted if the type is
172-
not known.
167+
| The full signature of the data type of the property. For example, `java.lang.String`
168+
but also a full generic type such as `java.util.Map<java.util.String,acme.MyEnum>`.
169+
This attribute can be used to guide the user as to the types of values that they can
170+
enter. For consistency, the type of a primitive is specified using its wrapper
171+
counterpart, i.e. `boolean` becomes `java.lang.Boolean`. Note that this class may be
172+
a complex type that gets converted from a String as values are bound. May be omitted
173+
if the type is not known.
173174

174175
|`description`
175176
| String

spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ else if (element instanceof ExecutableElement) {
161161
}
162162

163163
private void processAnnotatedTypeElement(String prefix, TypeElement element) {
164-
String type = this.typeUtils.getType(element);
164+
String type = this.typeUtils.getQualifiedName(element);
165165
this.metadataCollector.add(ItemMetadata.newGroup(prefix, type, type, null));
166166
processTypeElement(prefix, element);
167167
}
@@ -173,8 +173,9 @@ private void processExecutableElement(String prefix, ExecutableElement element)
173173
.asElement(element.getReturnType());
174174
if (returns instanceof TypeElement) {
175175
this.metadataCollector.add(
176-
ItemMetadata.newGroup(prefix, this.typeUtils.getType(returns),
177-
this.typeUtils.getType(element.getEnclosingElement()),
176+
ItemMetadata.newGroup(prefix,
177+
this.typeUtils.getQualifiedName(returns),
178+
this.typeUtils.getQualifiedName(element.getEnclosingElement()),
178179
element.toString()));
179180
processTypeElement(prefix, (TypeElement) returns);
180181
}
@@ -215,7 +216,7 @@ private void processSimpleTypes(String prefix, TypeElement element,
215216
boolean isCollection = this.typeUtils.isCollectionOrMap(returnType);
216217
if (!isExcluded && !isNested && (setter != null || isCollection)) {
217218
String dataType = this.typeUtils.getType(returnType);
218-
String sourceType = this.typeUtils.getType(element);
219+
String sourceType = this.typeUtils.getQualifiedName(element);
219220
String description = this.typeUtils.getJavaDoc(field);
220221
Object defaultValue = fieldValues.get(name);
221222
boolean deprecated = isDeprecated(getter) || isDeprecated(setter)
@@ -258,7 +259,7 @@ private void processSimpleLombokTypes(String prefix, TypeElement element,
258259
boolean hasSetter = hasLombokSetter(field, element);
259260
if (!isExcluded && !isNested && (hasSetter || isCollection)) {
260261
String dataType = this.typeUtils.getType(returnType);
261-
String sourceType = this.typeUtils.getType(element);
262+
String sourceType = this.typeUtils.getQualifiedName(element);
262263
String description = this.typeUtils.getJavaDoc(field);
263264
Object defaultValue = fieldValues.get(name);
264265
boolean deprecated = isDeprecated(field) || isDeprecated(element);
@@ -315,8 +316,8 @@ private void processNestedType(String prefix, TypeElement element, String name,
315316
&& annotation == null && isNested) {
316317
String nestedPrefix = ConfigurationMetadata.nestedPrefix(prefix, name);
317318
this.metadataCollector.add(ItemMetadata.newGroup(nestedPrefix,
318-
this.typeUtils.getType(returnElement),
319-
this.typeUtils.getType(element),
319+
this.typeUtils.getQualifiedName(returnElement),
320+
this.typeUtils.getQualifiedName(element),
320321
(getter == null ? null : getter.toString())));
321322
processTypeElement(nestedPrefix, (TypeElement) returnElement);
322323
}

spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataCollector.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public void processing(RoundEnvironment roundEnv) {
6969

7070
private void markAsProcessed(Element element) {
7171
if (element instanceof TypeElement) {
72-
this.processedSourceTypes.add(this.typeUtils.getType(element));
72+
this.processedSourceTypes.add(this.typeUtils.getQualifiedName(element));
7373
}
7474
}
7575

spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,34 @@ private TypeMirror getDeclaredType(Types types, Class<?> typeClass,
9393
}
9494
}
9595

96-
public String getType(Element element) {
97-
return getType(element == null ? null : element.asType());
96+
/**
97+
* Return the qualified name of the specified element.
98+
* @param element the element to handle
99+
* @return the fully qualified name of the element, suitable for a call
100+
* to {@link Class#forName(String)}
101+
*/
102+
public String getQualifiedName(Element element) {
103+
if (element == null) {
104+
return null;
105+
}
106+
TypeElement enclosingElement = getEnclosingTypeElement(element.asType());
107+
if (enclosingElement != null) {
108+
return getQualifiedName(enclosingElement) + "$"
109+
+ ((DeclaredType) element.asType()).asElement().getSimpleName().toString();
110+
}
111+
if (element instanceof TypeElement) {
112+
return ((TypeElement) element).getQualifiedName().toString();
113+
}
114+
throw new IllegalStateException("Could not extract qualified name from "
115+
+ element);
98116
}
99117

118+
/**
119+
* Return the type of the specified {@link TypeMirror} including all its generic
120+
* information.
121+
* @param type the type to handle
122+
* @return a representation of the type including all its generic information
123+
*/
100124
public String getType(TypeMirror type) {
101125
if (type == null) {
102126
return null;
@@ -105,15 +129,23 @@ public String getType(TypeMirror type) {
105129
if (wrapper != null) {
106130
return wrapper.getName();
107131
}
132+
TypeElement enclosingElement = getEnclosingTypeElement(type);
133+
if (enclosingElement != null) {
134+
return getQualifiedName(enclosingElement) + "$"
135+
+ ((DeclaredType) type).asElement().getSimpleName().toString();
136+
}
137+
return type.toString();
138+
}
139+
140+
private TypeElement getEnclosingTypeElement(TypeMirror type) {
108141
if (type instanceof DeclaredType) {
109142
DeclaredType declaredType = (DeclaredType) type;
110143
Element enclosingElement = declaredType.asElement().getEnclosingElement();
111144
if (enclosingElement != null && enclosingElement instanceof TypeElement) {
112-
return getType(enclosingElement) + "$"
113-
+ declaredType.asElement().getSimpleName().toString();
145+
return (TypeElement) enclosingElement;
114146
}
115147
}
116-
return type.toString();
148+
return null;
117149
}
118150

119151
public boolean isCollectionOrMap(TypeMirror type) {

spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import org.springframework.boot.configurationsample.specific.BuilderPojo;
5858
import org.springframework.boot.configurationsample.specific.DeprecatedUnrelatedMethodPojo;
5959
import org.springframework.boot.configurationsample.specific.ExcludedTypesPojo;
60+
import org.springframework.boot.configurationsample.specific.GenericConfig;
6061
import org.springframework.boot.configurationsample.specific.InnerClassAnnotatedGetterConfig;
6162
import org.springframework.boot.configurationsample.specific.InnerClassProperties;
6263
import org.springframework.boot.configurationsample.specific.InnerClassRootConfig;
@@ -339,6 +340,35 @@ public void invalidAccessor() throws IOException {
339340
assertThat(metadata.getItems(), hasSize(1));
340341
}
341342

343+
@Test
344+
public void genericTypes() throws IOException {
345+
ConfigurationMetadata metadata = compile(GenericConfig.class);
346+
assertThat(metadata, containsGroup("generic").ofType(
347+
"org.springframework.boot.configurationsample.specific.GenericConfig"));
348+
assertThat(metadata, containsGroup("generic.foo").ofType(
349+
"org.springframework.boot.configurationsample.specific.GenericConfig$Foo"));
350+
assertThat(metadata, containsGroup("generic.foo.bar").ofType(
351+
"org.springframework.boot.configurationsample.specific.GenericConfig$Bar"));
352+
assertThat(metadata, containsGroup("generic.foo.bar.biz").ofType(
353+
"org.springframework.boot.configurationsample.specific.GenericConfig$Bar$Biz"));
354+
assertThat(metadata, containsProperty("generic.foo.name")
355+
.ofType(String.class)
356+
.fromSource(GenericConfig.Foo.class));
357+
assertThat(metadata, containsProperty("generic.foo.string-to-bar")
358+
.ofType("java.util.Map<java.lang.String,org.springframework.boot.configurationsample.specific.GenericConfig.Bar<java.lang.Integer>>")
359+
.fromSource(GenericConfig.Foo.class));
360+
assertThat(metadata, containsProperty("generic.foo.string-to-integer")
361+
.ofType("java.util.Map<java.lang.String,java.lang.Integer>")
362+
.fromSource(GenericConfig.Foo.class));
363+
assertThat(metadata, containsProperty("generic.foo.bar.name")
364+
.ofType("java.lang.String")
365+
.fromSource(GenericConfig.Bar.class));
366+
assertThat(metadata, containsProperty("generic.foo.bar.biz.name")
367+
.ofType("java.lang.String")
368+
.fromSource(GenericConfig.Bar.Biz.class));
369+
assertThat(metadata.getItems(), hasSize(9));
370+
}
371+
342372
@Test
343373
public void lombokDataProperties() throws Exception {
344374
ConfigurationMetadata metadata = compile(LombokSimpleDataProperties.class);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2012-2016 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.configurationsample.specific;
18+
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
import org.springframework.boot.configurationsample.ConfigurationProperties;
23+
import org.springframework.boot.configurationsample.NestedConfigurationProperty;
24+
25+
/**
26+
* Demonstrate that only relevant generics are stored in the metadata.
27+
*
28+
* @author Stephane Nicoll
29+
*/
30+
@ConfigurationProperties("generic")
31+
public class GenericConfig<T> {
32+
33+
private final Foo foo = new Foo();
34+
35+
public Foo getFoo() {
36+
return this.foo;
37+
}
38+
39+
public static class Foo {
40+
41+
private String name;
42+
43+
@NestedConfigurationProperty
44+
private final Bar<String> bar = new Bar<String>();
45+
46+
private final Map<String, Bar<Integer>> stringToBar =
47+
new HashMap<String, Bar<Integer>>();
48+
49+
private final Map<String, Integer> stringToInteger =
50+
new HashMap<String, Integer>();
51+
52+
public String getName() {
53+
return this.name;
54+
}
55+
56+
public void setName(String name) {
57+
this.name = name;
58+
}
59+
60+
public Bar<String> getBar() {
61+
return this.bar;
62+
}
63+
64+
public Map<String, Bar<Integer>> getStringToBar() {
65+
return this.stringToBar;
66+
}
67+
68+
public Map<String, Integer> getStringToInteger() {
69+
return this.stringToInteger;
70+
}
71+
}
72+
73+
public static class Bar<U> {
74+
75+
private String name;
76+
77+
@NestedConfigurationProperty
78+
private final Biz<String> biz = new Biz<String>();
79+
80+
public String getName() {
81+
return this.name;
82+
}
83+
84+
public void setName(String name) {
85+
this.name = name;
86+
}
87+
88+
public Biz<String> getBiz() {
89+
return this.biz;
90+
}
91+
92+
public static class Biz<V> {
93+
94+
private String name;
95+
96+
public String getName() {
97+
return this.name;
98+
}
99+
100+
public void setName(String name) {
101+
this.name = name;
102+
}
103+
}
104+
105+
}
106+
107+
}

0 commit comments

Comments
 (0)