Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.time.Duration;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -77,6 +78,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor

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

static final String DESCRIPTION_CACHE_LOCATION_OPTION = "org.springframework.boot.configurationprocessor.descriptionCacheLocation";

static final String CONFIGURATION_PROPERTIES_ANNOTATION = "org.springframework.boot.context.properties.ConfigurationProperties";

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

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

private static final Set<String> SUPPORTED_OPTIONS = Set.of(ADDITIONAL_METADATA_LOCATIONS_OPTION);
private static final Set<String> SUPPORTED_OPTIONS = Set.of(ADDITIONAL_METADATA_LOCATIONS_OPTION,
DESCRIPTION_CACHE_LOCATION_OPTION);

private MetadataStore metadataStore;

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

private MetadataGenerationEnvironment metadataEnv;

private DescriptionCache descriptionCache;

private final Set<String> classFileBackedTypes = new HashSet<>();

protected String configurationPropertiesAnnotation() {
return CONFIGURATION_PROPERTIES_ANNOTATION;
}
Expand Down Expand Up @@ -189,6 +197,10 @@ public synchronized void init(ProcessingEnvironment env) {
configurationPropertiesSourceAnnotation(), nestedConfigurationPropertyAnnotation(),
deprecatedConfigurationPropertyAnnotation(), constructorBindingAnnotation(), autowiredAnnotation(),
defaultValueAnnotation(), endpointAnnotations(), readOperationAnnotation(), nameAnnotation());
String cacheLocation = env.getOptions().get(DESCRIPTION_CACHE_LOCATION_OPTION);
if (cacheLocation != null) {
this.descriptionCache = new DescriptionCache(cacheLocation);
}
}

@Override
Expand Down Expand Up @@ -289,6 +301,9 @@ private void processTypeElement(String prefix, TypeElement element, ExecutableEl
Deque<TypeElement> seen) {
if (!seen.contains(element)) {
seen.push(element);
if (this.descriptionCache != null && !this.metadataEnv.hasSourceTree(element)) {
this.classFileBackedTypes.add(this.metadataEnv.getTypeUtils().getQualifiedName(element));
}
new PropertyDescriptorResolver(this.metadataEnv).resolve(element, source).forEach((descriptor) -> {
this.metadataCollector.add(descriptor.resolveItemMetadata(prefix, this.metadataEnv));
ItemHint itemHint = descriptor.resolveItemHint(prefix, this.metadataEnv);
Expand Down Expand Up @@ -405,14 +420,38 @@ protected void writeSourceMetadata() throws Exception {
protected ConfigurationMetadata writeMetadata() throws Exception {
ConfigurationMetadata metadata = this.metadataCollector.getMetadata();
metadata = mergeAdditionalMetadata(metadata, () -> this.metadataStore.readAdditionalMetadata());
fillCachedDescriptions(metadata);
removeIgnored(metadata);
if (!metadata.getItems().isEmpty()) {
this.metadataStore.writeMetadata(metadata);
updateDescriptionCache(metadata);
return metadata;
}
return null;
}

private void fillCachedDescriptions(ConfigurationMetadata metadata) {
if (this.descriptionCache == null) {
return;
}
for (ItemMetadata item : metadata.getItems()) {
if (item.isOfItemType(ItemMetadata.ItemType.PROPERTY) && item.getDescription() == null
&& item.getSourceType() != null && this.classFileBackedTypes.contains(item.getSourceType())) {
String cached = this.descriptionCache.getDescription(item.getName());
if (cached != null) {
item.setDescription(cached);
}
}
}
}

private void updateDescriptionCache(ConfigurationMetadata metadata) {
if (this.descriptionCache == null) {
return;
}
this.descriptionCache.update(metadata);
}

private void removeIgnored(ConfigurationMetadata metadata) {
for (ItemIgnore itemIgnore : metadata.getIgnored()) {
metadata.removeMetadata(itemIgnore.getType(), itemIgnore.getName());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright 2012-present 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
*
* https://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.configurationprocessor;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
import org.springframework.boot.configurationprocessor.metadata.JsonMarshaller;

/**
* Cache for property descriptions that persists across incremental builds. Stores
* metadata in a location outside of {@code CLASS_OUTPUT} so that Gradle's incremental
* compilation does not delete it.
*
* @author Agustin Palazzo
* @see ConfigurationMetadataAnnotationProcessor
*/
class DescriptionCache {

private final File cacheFile;

private ConfigurationMetadata cachedMetadata;

DescriptionCache(String cacheFilePath) {
this.cacheFile = new File(cacheFilePath);
this.cachedMetadata = read();
}

/**
* Look up a cached description for the given property name.
* @param propertyName the fully qualified property name
* @return the cached description or {@code null}
*/
String getDescription(String propertyName) {
if (this.cachedMetadata == null) {
return null;
}
for (ItemMetadata item : this.cachedMetadata.getItems()) {
if (item.isOfItemType(ItemMetadata.ItemType.PROPERTY) && propertyName.equals(item.getName())) {
return item.getDescription();
}
}
return null;
}

/**
* Replace the cache with the given metadata. After
* {@link ConfigurationMetadataAnnotationProcessor#fillCachedDescriptions} has
* restored descriptions from the cache, the metadata is the complete picture of the
* current build. Replacing (instead of merging) ensures that entries for deleted or
* de-annotated types are automatically pruned.
* @param metadata the current build's metadata with descriptions already filled
*/
void update(ConfigurationMetadata metadata) {
write(metadata);
this.cachedMetadata = new ConfigurationMetadata(metadata);
}

private ConfigurationMetadata read() {
if (!this.cacheFile.exists()) {
return null;
}
try (FileInputStream in = new FileInputStream(this.cacheFile)) {
return new JsonMarshaller().read(in);
}
catch (Exception ex) {
return null;
}
}

private void write(ConfigurationMetadata metadata) {
this.cacheFile.getParentFile().mkdirs();
try (FileOutputStream out = new FileOutputStream(this.cacheFile)) {
new JsonMarshaller().write(metadata, out);
}
catch (IOException ex) {
// Cache write failure is non-fatal
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ TypeUtils getTypeUtils() {
return this.typeUtils;
}

boolean hasSourceTree(TypeElement element) {
return this.fieldValuesParser.hasSourceTree(element);
}

Messager getMessager() {
return this.messager;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,19 @@
* @since 1.1.2
* @see JavaCompilerFieldValuesParser
*/
@FunctionalInterface
public interface FieldValuesParser {

/**
* Implementation of {@link FieldValuesParser} that always returns an empty result.
*/
FieldValuesParser NONE = (element) -> Collections.emptyMap();
FieldValuesParser NONE = new FieldValuesParser() {

@Override
public Map<String, Object> getFieldValues(TypeElement element) {
return Collections.emptyMap();
}

};

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

/**
* Return whether the given element has a source tree available. Elements loaded from
* {@code .class} files during incremental compilation will not have a source tree,
* meaning javadoc comments are unavailable.
* @param element the element to check
* @return {@code true} if the element has source, {@code false} if backed by
* {@code .class}
*/
default boolean hasSourceTree(TypeElement element) {
return true;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ public Map<String, Object> getFieldValues(TypeElement element) throws Exception
return Collections.emptyMap();
}

@Override
public boolean hasSourceTree(TypeElement element) {
try {
return this.trees.getTree(element) != null;
}
catch (Exception ex) {
return true;
}
}

/**
* {@link TreeVisitor} to collect fields.
*/
Expand Down
Loading