ConfigurationMetadataBuilder.java
/*
* Copyright 2017-2020 original 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 io.micronaut.inject.configuration;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.javadoc.Javadoc;
import com.github.javaparser.javadoc.JavadocBlockTag;
import com.github.javaparser.javadoc.description.JavadocDescription;
import com.github.javaparser.javadoc.description.JavadocDescriptionElement;
import com.github.javaparser.javadoc.description.JavadocInlineTag;
import com.github.javaparser.javadoc.description.JavadocSnippet;
import io.micronaut.context.annotation.ConfigurationReader;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.Element;
import io.micronaut.inject.ast.MethodElement;
import io.micronaut.inject.ast.PropertyElement;
import io.micronaut.inject.writer.OriginatingElements;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static io.micronaut.inject.configuration.ConfigurationUtils.buildPropertyPath;
import static io.micronaut.inject.configuration.ConfigurationUtils.getRequiredTypePath;
/**
* <p>A builder for producing metadata for the available {@link io.micronaut.context.annotation.ConfigurationProperties}.</p>
*
* <p>This data can then be subsequently written to a format readable by IDEs
* (like spring-configuration-metadata.json for example).</p>
*
* @author Graeme Rocher
* @author Denis Stepanov
* @since 1.0
*/
public class ConfigurationMetadataBuilder {
@SuppressWarnings({"StaticVariableName", "VisibilityModifier"})
public static final ConfigurationMetadataBuilder INSTANCE = new ConfigurationMetadataBuilder();
private final OriginatingElements originatingElements = OriginatingElements.of();
private final List<PropertyMetadata> properties = new ArrayList<>();
private final List<ConfigurationMetadata> configurations = new ArrayList<>();
/**
* @return The originating elements for the builder.
*/
public @NonNull Element[] getOriginatingElements() {
return originatingElements.getOriginatingElements();
}
/**
* @return The properties
*/
public List<PropertyMetadata> getProperties() {
return Collections.unmodifiableList(properties);
}
/**
* @return The configurations
*/
public List<ConfigurationMetadata> getConfigurations() {
return Collections.unmodifiableList(configurations);
}
/**
* @return Whether any metadata is present
*/
public boolean hasMetadata() {
return !properties.isEmpty() || !configurations.isEmpty();
}
/**
* Visit a {@link io.micronaut.context.annotation.ConfigurationProperties} class.
*
* @param classElement The type of the {@link io.micronaut.context.annotation.ConfigurationProperties}
* @return This {@link ConfigurationMetadata}
*/
public ConfigurationMetadata visitProperties(ClassElement classElement) {
originatingElements.addOriginatingElement(classElement);
String path = getRequiredTypePath(classElement);
ConfigurationMetadata configurationMetadata = new ConfigurationMetadata();
configurationMetadata.name = NameUtils.hyphenate(path, true);
configurationMetadata.type = classElement.getType().getName();
configurationMetadata.description = resolveJavadocDescription(classElement);
configurationMetadata.includes = CollectionUtils.setOf(classElement.stringValues(ConfigurationReader.class, "includes"));
configurationMetadata.excludes = CollectionUtils.setOf(classElement.stringValues(ConfigurationReader.class, "excludes"));
this.configurations.add(configurationMetadata);
return configurationMetadata;
}
/**
* Resolves the javadoc description for the given element.
* @param element The element
* @return The javadoc description.
*/
@Nullable
public static String resolveJavadocDescription(@NonNull Element element) {
String resolvedDocs = null;
String javadoc = element.getDocumentation().orElse(null);
if (javadoc == null && element instanceof PropertyElement propertyElement) {
javadoc = propertyElement.getWriteMethod().flatMap(Element::getDocumentation).orElse(null);
}
if (javadoc != null) {
try {
Javadoc jd = StaticJavaParser.parseJavadoc(javadoc);
JavadocDescription description = jd.getDescription();
StringBuilder builder = new StringBuilder();
List<JavadocDescriptionElement> elements = description.getElements();
if (!elements.isEmpty()) {
for (JavadocDescriptionElement jde : elements) {
if (jde instanceof JavadocSnippet snippet) {
builder.append(snippet.toText());
} else if (jde instanceof JavadocInlineTag tag) {
builder.append(tag.toText());
}
}
} else if (element instanceof MethodElement) {
jd.getBlockTags()
.stream().filter(bt -> bt.getType() == JavadocBlockTag.Type.RETURN)
.findFirst().ifPresent(returnTag -> builder.append(returnTag.getContent().toText()));
} else if (element instanceof PropertyElement) {
jd.getBlockTags()
.stream().filter(bt -> bt.getType() == JavadocBlockTag.Type.PARAM)
.findFirst().ifPresent(returnTag -> builder.append(returnTag.getContent().toText()));
}
resolvedDocs = builder.toString();
} catch (Exception e) {
// ignore
}
}
return resolvedDocs;
}
/**
* Visit a configuration property.
*
* @param owningType The type that owns the property
* @param declaringType The declaring type of the property
* @param propertyType The property type
* @param name The property name
* @param description A description for the property
* @param defaultValue The default value of the property (only used for constant values such as strings, numbers,
* enums etc.)
* @return This property metadata
*/
public PropertyMetadata visitProperty(ClassElement owningType,
ClassElement declaringType,
ClassElement propertyType,
String name,
@Nullable String description,
@Nullable String defaultValue) {
originatingElements.addOriginatingElement(owningType);
originatingElements.addOriginatingElement(declaringType);
PropertyMetadata metadata = new PropertyMetadata();
metadata.declaringType = declaringType.getName();
metadata.name = name;
metadata.path = propertyType.stringValue(ConfigurationReader.class, ConfigurationReader.PREFIX)
.orElseGet(() -> NameUtils.hyphenate(buildPropertyPath(owningType, declaringType, name), true));
if (propertyType.hasStereotype(ConfigurationReader.class)) {
metadata.path = ConfigurationUtils.getRequiredTypePath(propertyType);
} else {
metadata.path = NameUtils.hyphenate(buildPropertyPath(owningType, declaringType, name), true);
}
metadata.type = propertyType.getType().getName();
metadata.description = description;
metadata.defaultValue = defaultValue;
properties.add(metadata);
return metadata;
}
/**
* Quote a string.
*
* @param string The string to quote
* @return The quoted string
*/
@SuppressWarnings("MagicNumber")
static String quote(String string) {
if (string == null || string.isEmpty()) {
return "\"\"";
}
char c = 0;
int i;
int len = string.length();
StringBuilder sb = new StringBuilder(len + 4);
String t;
sb.append('"');
for (i = 0; i < len; i += 1) {
c = string.charAt(i);
switch (c) {
case '\\', '"', '/':
sb.append('\\');
sb.append(c);
break;
case '\b':
sb.append("\\b");
break;
case '\t':
sb.append("\\t");
break;
case '\n':
sb.append("\\n");
break;
case '\f':
sb.append("\\f");
break;
case '\r':
sb.append("\\r");
break;
default:
if (c < ' ') {
t = "000" + Integer.toHexString(c);
sb.append("\\u").append(t.substring(t.length() - 4));
} else {
sb.append(c);
}
}
}
sb.append('"');
return sb.toString();
}
/**
* Write a quoted attribute with a value to a writer.
*
* @param out The out writer
* @param name The name of the attribute
* @param value The value
* @throws IOException If an error occurred writing output
*/
static void writeAttribute(Writer out, String name, String value) throws IOException {
out.write('"');
out.write(name);
out.write("\":");
out.write(quote(value));
}
/**
* Reset the state.
*/
@Internal
public static void reset() {
INSTANCE.properties.clear();
INSTANCE.configurations.clear();
}
}