Skip to content

Commit 3baf552

Browse files
committed
Preserve property descriptions during Gradle incremental compilation
Cache descriptions in a location outside CLASS_OUTPUT so that Gradle's incremental compilation does not lose javadoc-based descriptions for types passed as .class files. See gh-28075 Signed-off-by: Agustin Bereciartua <bereciartua.agustin@gmail.com>
1 parent 23bcc68 commit 3baf552

6 files changed

Lines changed: 465 additions & 3 deletions

File tree

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.time.Duration;
2222
import java.util.ArrayDeque;
2323
import java.util.Deque;
24+
import java.util.HashSet;
2425
import java.util.LinkedHashMap;
2526
import java.util.List;
2627
import java.util.Locale;
@@ -77,6 +78,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
7778

7879
static final String ADDITIONAL_METADATA_LOCATIONS_OPTION = "org.springframework.boot.configurationprocessor.additionalMetadataLocations";
7980

81+
static final String DESCRIPTION_CACHE_LOCATION_OPTION = "org.springframework.boot.configurationprocessor.descriptionCacheLocation";
82+
8083
static final String CONFIGURATION_PROPERTIES_ANNOTATION = "org.springframework.boot.context.properties.ConfigurationProperties";
8184

8285
static final String CONFIGURATION_PROPERTIES_SOURCE_ANNOTATION = "org.springframework.boot.context.properties.ConfigurationPropertiesSource";
@@ -113,7 +116,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
113116

114117
static final String ENDPOINT_ACCESS_ENUM = "org.springframework.boot.actuate.endpoint.Access";
115118

116-
private static final Set<String> SUPPORTED_OPTIONS = Set.of(ADDITIONAL_METADATA_LOCATIONS_OPTION);
119+
private static final Set<String> SUPPORTED_OPTIONS = Set.of(ADDITIONAL_METADATA_LOCATIONS_OPTION,
120+
DESCRIPTION_CACHE_LOCATION_OPTION);
117121

118122
private MetadataStore metadataStore;
119123

@@ -123,6 +127,10 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
123127

124128
private MetadataGenerationEnvironment metadataEnv;
125129

130+
private DescriptionCache descriptionCache;
131+
132+
private final Set<String> classFileBackedTypes = new HashSet<>();
133+
126134
protected String configurationPropertiesAnnotation() {
127135
return CONFIGURATION_PROPERTIES_ANNOTATION;
128136
}
@@ -189,6 +197,10 @@ public synchronized void init(ProcessingEnvironment env) {
189197
configurationPropertiesSourceAnnotation(), nestedConfigurationPropertyAnnotation(),
190198
deprecatedConfigurationPropertyAnnotation(), constructorBindingAnnotation(), autowiredAnnotation(),
191199
defaultValueAnnotation(), endpointAnnotations(), readOperationAnnotation(), nameAnnotation());
200+
String cacheLocation = env.getOptions().get(DESCRIPTION_CACHE_LOCATION_OPTION);
201+
if (cacheLocation != null) {
202+
this.descriptionCache = new DescriptionCache(cacheLocation);
203+
}
192204
}
193205

194206
@Override
@@ -289,6 +301,9 @@ private void processTypeElement(String prefix, TypeElement element, ExecutableEl
289301
Deque<TypeElement> seen) {
290302
if (!seen.contains(element)) {
291303
seen.push(element);
304+
if (this.descriptionCache != null && !this.metadataEnv.hasSourceTree(element)) {
305+
this.classFileBackedTypes.add(this.metadataEnv.getTypeUtils().getQualifiedName(element));
306+
}
292307
new PropertyDescriptorResolver(this.metadataEnv).resolve(element, source).forEach((descriptor) -> {
293308
this.metadataCollector.add(descriptor.resolveItemMetadata(prefix, this.metadataEnv));
294309
ItemHint itemHint = descriptor.resolveItemHint(prefix, this.metadataEnv);
@@ -405,14 +420,38 @@ protected void writeSourceMetadata() throws Exception {
405420
protected ConfigurationMetadata writeMetadata() throws Exception {
406421
ConfigurationMetadata metadata = this.metadataCollector.getMetadata();
407422
metadata = mergeAdditionalMetadata(metadata, () -> this.metadataStore.readAdditionalMetadata());
423+
fillCachedDescriptions(metadata);
408424
removeIgnored(metadata);
409425
if (!metadata.getItems().isEmpty()) {
410426
this.metadataStore.writeMetadata(metadata);
427+
updateDescriptionCache(metadata);
411428
return metadata;
412429
}
413430
return null;
414431
}
415432

433+
private void fillCachedDescriptions(ConfigurationMetadata metadata) {
434+
if (this.descriptionCache == null) {
435+
return;
436+
}
437+
for (ItemMetadata item : metadata.getItems()) {
438+
if (item.isOfItemType(ItemMetadata.ItemType.PROPERTY) && item.getDescription() == null
439+
&& item.getSourceType() != null && this.classFileBackedTypes.contains(item.getSourceType())) {
440+
String cached = this.descriptionCache.getDescription(item.getName());
441+
if (cached != null) {
442+
item.setDescription(cached);
443+
}
444+
}
445+
}
446+
}
447+
448+
private void updateDescriptionCache(ConfigurationMetadata metadata) {
449+
if (this.descriptionCache == null) {
450+
return;
451+
}
452+
this.descriptionCache.update(metadata);
453+
}
454+
416455
private void removeIgnored(ConfigurationMetadata metadata) {
417456
for (ItemIgnore itemIgnore : metadata.getIgnored()) {
418457
metadata.removeMetadata(itemIgnore.getType(), itemIgnore.getName());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2012-present 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+
* https://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.configurationprocessor;
18+
19+
import java.io.File;
20+
import java.io.FileInputStream;
21+
import java.io.FileOutputStream;
22+
import java.io.IOException;
23+
24+
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
25+
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
26+
import org.springframework.boot.configurationprocessor.metadata.JsonMarshaller;
27+
28+
/**
29+
* Cache for property descriptions that persists across incremental builds. Stores
30+
* metadata in a location outside of {@code CLASS_OUTPUT} so that Gradle's incremental
31+
* compilation does not delete it.
32+
*
33+
* @author Agustin Palazzo
34+
* @see ConfigurationMetadataAnnotationProcessor
35+
*/
36+
class DescriptionCache {
37+
38+
private final File cacheFile;
39+
40+
private ConfigurationMetadata cachedMetadata;
41+
42+
DescriptionCache(String cacheFilePath) {
43+
this.cacheFile = new File(cacheFilePath);
44+
this.cachedMetadata = read();
45+
}
46+
47+
/**
48+
* Look up a cached description for the given property name.
49+
* @param propertyName the fully qualified property name
50+
* @return the cached description or {@code null}
51+
*/
52+
String getDescription(String propertyName) {
53+
if (this.cachedMetadata == null) {
54+
return null;
55+
}
56+
for (ItemMetadata item : this.cachedMetadata.getItems()) {
57+
if (item.isOfItemType(ItemMetadata.ItemType.PROPERTY) && propertyName.equals(item.getName())) {
58+
return item.getDescription();
59+
}
60+
}
61+
return null;
62+
}
63+
64+
/**
65+
* Replace the cache with the given metadata. After
66+
* {@link ConfigurationMetadataAnnotationProcessor#fillCachedDescriptions} has
67+
* restored descriptions from the cache, the metadata is the complete picture of the
68+
* current build. Replacing (instead of merging) ensures that entries for deleted or
69+
* de-annotated types are automatically pruned.
70+
* @param metadata the current build's metadata with descriptions already filled
71+
*/
72+
void update(ConfigurationMetadata metadata) {
73+
write(metadata);
74+
this.cachedMetadata = new ConfigurationMetadata(metadata);
75+
}
76+
77+
private ConfigurationMetadata read() {
78+
if (!this.cacheFile.exists()) {
79+
return null;
80+
}
81+
try (FileInputStream in = new FileInputStream(this.cacheFile)) {
82+
return new JsonMarshaller().read(in);
83+
}
84+
catch (Exception ex) {
85+
return null;
86+
}
87+
}
88+
89+
private void write(ConfigurationMetadata metadata) {
90+
this.cacheFile.getParentFile().mkdirs();
91+
try (FileOutputStream out = new FileOutputStream(this.cacheFile)) {
92+
new JsonMarshaller().write(metadata, out);
93+
}
94+
catch (IOException ex) {
95+
// Cache write failure is non-fatal
96+
}
97+
}
98+
99+
}

configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ TypeUtils getTypeUtils() {
137137
return this.typeUtils;
138138
}
139139

140+
boolean hasSourceTree(TypeElement element) {
141+
return this.fieldValuesParser.hasSourceTree(element);
142+
}
143+
140144
Messager getMessager() {
141145
return this.messager;
142146
}

configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/FieldValuesParser.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,19 @@
3030
* @since 1.1.2
3131
* @see JavaCompilerFieldValuesParser
3232
*/
33-
@FunctionalInterface
3433
public interface FieldValuesParser {
3534

3635
/**
3736
* Implementation of {@link FieldValuesParser} that always returns an empty result.
3837
*/
39-
FieldValuesParser NONE = (element) -> Collections.emptyMap();
38+
FieldValuesParser NONE = new FieldValuesParser() {
39+
40+
@Override
41+
public Map<String, Object> getFieldValues(TypeElement element) {
42+
return Collections.emptyMap();
43+
}
44+
45+
};
4046

4147
/**
4248
* Return the field values for the given element.
@@ -46,4 +52,16 @@ public interface FieldValuesParser {
4652
*/
4753
Map<String, Object> getFieldValues(TypeElement element) throws Exception;
4854

55+
/**
56+
* Return whether the given element has a source tree available. Elements loaded from
57+
* {@code .class} files during incremental compilation will not have a source tree,
58+
* meaning javadoc comments are unavailable.
59+
* @param element the element to check
60+
* @return {@code true} if the element has source, {@code false} if backed by
61+
* {@code .class}
62+
*/
63+
default boolean hasSourceTree(TypeElement element) {
64+
return true;
65+
}
66+
4967
}

configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesParser.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ public Map<String, Object> getFieldValues(TypeElement element) throws Exception
5757
return Collections.emptyMap();
5858
}
5959

60+
@Override
61+
public boolean hasSourceTree(TypeElement element) {
62+
try {
63+
return this.trees.getTree(element) != null;
64+
}
65+
catch (Exception ex) {
66+
return true;
67+
}
68+
}
69+
6070
/**
6171
* {@link TreeVisitor} to collect fields.
6272
*/

0 commit comments

Comments
 (0)