ConfigurationReaderBeanElementCreator.java
/*
* Copyright 2017-2022 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.processing;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.javadoc.Javadoc;
import com.github.javaparser.javadoc.JavadocBlockTag;
import io.micronaut.context.annotation.ConfigurationBuilder;
import io.micronaut.context.annotation.ConfigurationInject;
import io.micronaut.context.annotation.ConfigurationReader;
import io.micronaut.context.annotation.Executable;
import io.micronaut.context.annotation.Parameter;
import io.micronaut.context.annotation.Property;
import io.micronaut.context.annotation.Value;
import io.micronaut.context.visitor.ConfigurationReaderVisitor;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.AnnotationUtil;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.inject.annotation.AnnotationMetadataHierarchy;
import io.micronaut.inject.annotation.MutableAnnotationMetadata;
import io.micronaut.inject.ast.PropertyElementQuery;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.FieldElement;
import io.micronaut.inject.ast.MemberElement;
import io.micronaut.inject.ast.MethodElement;
import io.micronaut.inject.ast.ParameterElement;
import io.micronaut.inject.ast.PropertyElement;
import io.micronaut.inject.configuration.ConfigurationMetadataBuilder;
import io.micronaut.inject.configuration.PropertyMetadata;
import io.micronaut.inject.visitor.VisitorContext;
import io.micronaut.inject.writer.BeanDefinitionVisitor;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* Configuration reader bean builder.
*
* @author Denis Stepanov
* @since 4.0.0
*/
@Internal
final class ConfigurationReaderBeanElementCreator extends DeclaredBeanElementCreator {
private static final List<String> CONSTRUCTOR_PARAMETERS_INJECTION_ANN =
Arrays.asList(Property.class.getName(), Value.class.getName(), Parameter.class.getName(), AnnotationUtil.QUALIFIER, AnnotationUtil.INJECT);
private final ConfigurationMetadataBuilder metadataBuilder = ConfigurationMetadataBuilder.INSTANCE;
ConfigurationReaderBeanElementCreator(ClassElement classElement, VisitorContext visitorContext) {
super(classElement, visitorContext, false);
}
@Override
protected void applyConfigurationInjectionIfNecessary(BeanDefinitionVisitor visitor,
MethodElement constructor) {
if (!classElement.isRecord() && !constructor.hasAnnotation(ConfigurationInject.class)) {
return;
}
if (classElement.isRecord()) {
final List<PropertyElement> beanProperties = constructor
.getDeclaringType()
.getBeanProperties();
final ParameterElement[] parameters = constructor.getParameters();
if (beanProperties.size() == parameters.length) {
Javadoc javadoc = classElement.getDocumentation().map(StaticJavaParser::parseJavadoc).orElse(null);
for (int i = 0; i < parameters.length; i++) {
ParameterElement parameter = parameters[i];
final PropertyElement bp = beanProperties.get(i);
if (CONSTRUCTOR_PARAMETERS_INJECTION_ANN.stream().noneMatch(bp::hasStereotype)) {
String paramDoc = findParameterDoc(javadoc, parameter);
processConfigurationConstructorParameter(parameter, paramDoc);
}
}
if (constructor.hasStereotype(ANN_REQUIRES_VALIDATION)) {
visitor.setValidated(true);
}
return;
}
}
processConfigurationInjectionPoint(visitor, constructor);
}
@Nullable
private static String findParameterDoc(Javadoc javadoc, ParameterElement parameter) {
String paramDoc = null;
if (javadoc != null) {
JavadocBlockTag bt = javadoc.getBlockTags()
.stream().filter(t -> t.getType() == JavadocBlockTag.Type.PARAM && t.getName().map(n -> n.equals(parameter.getName())).orElse(false))
.findFirst().orElse(null);
if (bt != null) {
paramDoc = bt.getContent().toText();
}
}
return paramDoc;
}
private void processConfigurationInjectionPoint(BeanDefinitionVisitor visitor,
MethodElement constructor) {
Javadoc javadoc = constructor.getDocumentation().map(StaticJavaParser::parseJavadoc).orElse(null);
for (ParameterElement parameter : constructor.getParameters()) {
if (CONSTRUCTOR_PARAMETERS_INJECTION_ANN.stream().noneMatch(parameter::hasStereotype)) {
String paramDoc = findParameterDoc(javadoc, parameter);
processConfigurationConstructorParameter(parameter, paramDoc);
}
}
if (constructor.hasStereotype(ANN_REQUIRES_VALIDATION)) {
visitor.setValidated(true);
}
}
private void processConfigurationConstructorParameter(ParameterElement parameter, @Nullable String paramDoc) {
if (ConfigurationReaderVisitor.isPropertyParameter(parameter, visitorContext)) {
final PropertyMetadata pm = metadataBuilder.visitProperty(
parameter.getMethodElement().getOwningType(),
parameter.getMethodElement().getDeclaringType(),
parameter.getGenericType(),
parameter.getName(), paramDoc,
parameter.stringValue(Bindable.class, "defaultValue").orElse(null)
);
parameter.annotate(Property.class, (builder) -> builder.member("name", pm.getPath()));
}
}
public static boolean isConfigurationProperties(ClassElement classElement) {
return classElement.hasStereotype(ConfigurationReader.class);
}
@Override
protected void makeInterceptedForValidationIfNeeded(MethodElement element) {
// Configuration beans are validated by the introspection
}
@Override
protected boolean processAsProperties() {
return true;
}
@Override
protected boolean visitProperty(BeanDefinitionVisitor visitor, PropertyElement propertyElement) {
Optional<MethodElement> readMethod = propertyElement.getReadMethod();
Optional<FieldElement> field = propertyElement.getField();
if (propertyElement.hasStereotype(ConfigurationBuilder.class)) {
// Exclude / ignore shouldn't affect builders
if (readMethod.isPresent()) {
MethodElement methodElement = readMethod.get();
ClassElement builderType = methodElement.getReturnType();
visitor.visitConfigBuilderMethod(
builderType,
methodElement.getName(),
propertyElement.getAnnotationMetadata(),
null,
builderType.isInterface()
);
visitConfigurationBuilder(visitor, propertyElement, builderType);
return true;
}
if (field.isPresent()) {
FieldElement fieldElement = field.get();
if (fieldElement.isAccessible(classElement)) {
ClassElement builderType = fieldElement.getType();
visitor.visitConfigBuilderField(
builderType,
fieldElement.getName(),
fieldElement.getAnnotationMetadata(),
metadataBuilder,
builderType.isInterface()
);
visitConfigurationBuilder(visitor, propertyElement, builderType);
return true;
}
throw new ProcessingException(fieldElement, "ConfigurationBuilder applied to a non accessible (private or package-private/protected in a different package) field must have a corresponding non-private getter method.");
}
} else if (!propertyElement.isExcluded()) {
boolean claimed = false;
Optional<MethodElement> writeMethod = propertyElement.getWriteMethod();
if (propertyElement.getWriteAccessKind() == PropertyElement.AccessKind.METHOD && writeMethod.isPresent()) {
visitor.setValidated(visitor.isValidated() || propertyElement.hasAnnotation(ANN_REQUIRES_VALIDATION));
MethodElement methodElement = writeMethod.get();
ParameterElement parameter = methodElement.getParameters()[0];
AnnotationMetadata annotationMetadata = new AnnotationMetadataHierarchy(
propertyElement,
parameter
).merge();
annotationMetadata = calculatePath(propertyElement, methodElement, annotationMetadata);
AnnotationMetadata finalAnnotationMetadata = annotationMetadata;
methodElement = methodElement
.withAnnotationMetadata(annotationMetadata)
.withParameters(
Arrays.stream(methodElement.getParameters())
.map(p -> p == parameter ? parameter.withAnnotationMetadata(finalAnnotationMetadata) : p)
.toArray(ParameterElement[]::new)
);
visitor.visitSetterValue(methodElement.getDeclaringType(), methodElement, annotationMetadata, methodElement.isReflectionRequired(classElement), true);
claimed = true;
} else if (propertyElement.getWriteAccessKind() == PropertyElement.AccessKind.FIELD && field.isPresent()) {
visitor.setValidated(visitor.isValidated() || propertyElement.hasAnnotation(ANN_REQUIRES_VALIDATION));
FieldElement fieldElement = field.get();
AnnotationMetadata annotationMetadata = MutableAnnotationMetadata.of(propertyElement.getAnnotationMetadata());
annotationMetadata = calculatePath(propertyElement, fieldElement, annotationMetadata);
visitor.visitFieldValue(fieldElement.getDeclaringType(), fieldElement.withAnnotationMetadata(annotationMetadata), fieldElement.isReflectionRequired(classElement), true);
claimed = true;
}
if (readMethod.isPresent()) {
MethodElement methodElement = readMethod.get();
if (methodElement.hasStereotype(Executable.class)) {
claimed |= visitExecutableMethod(visitor, methodElement);
}
}
return claimed;
}
return false;
}
@Override
protected boolean visitField(BeanDefinitionVisitor visitor, FieldElement fieldElement) {
if (fieldElement.hasStereotype(ConfigurationBuilder.class)) {
if (fieldElement.isAccessible(classElement)) {
ClassElement builderType = fieldElement.getType();
visitor.visitConfigBuilderField(
builderType,
fieldElement.getName(),
fieldElement.getAnnotationMetadata(),
metadataBuilder,
builderType.isInterface()
);
visitConfigurationBuilder(visitor, fieldElement, builderType);
return true;
}
throw new ProcessingException(fieldElement, "ConfigurationBuilder applied to a non accessible (private or package-private/protected in a different package) field must have a corresponding non-private getter method.");
}
return super.visitField(visitor, fieldElement);
}
private AnnotationMetadata calculatePath(PropertyElement propertyElement, MemberElement writeMember, AnnotationMetadata annotationMetadata) {
String path = metadataBuilder.visitProperty(
writeMember.getOwningType(),
writeMember.getDeclaringType(),
propertyElement.getGenericType(),
propertyElement.getName(),
ConfigurationMetadataBuilder.resolveJavadocDescription(propertyElement),
null
).getPath();
return visitorContext.getAnnotationMetadataBuilder().annotate(annotationMetadata, AnnotationValue.builder(Property.class).member("name", path).build());
}
@Override
protected boolean isInjectPointMethod(MemberElement memberElement) {
return super.isInjectPointMethod(memberElement) || memberElement.hasDeclaredStereotype(ConfigurationInject.class);
}
private void visitConfigurationBuilder(BeanDefinitionVisitor visitor,
MemberElement builderElement,
ClassElement builderType) {
try {
String configurationPrefix = builderElement.stringValue(ConfigurationBuilder.class).map(v -> v + ".").orElse("");
builderType.getBeanProperties(PropertyElementQuery.of(builderElement))
.stream()
.filter(propertyElement -> {
if (propertyElement.isExcluded()) {
return false;
}
Optional<MethodElement> writeMethod = propertyElement.getWriteMethod();
if (writeMethod.isEmpty()) {
return false;
}
MethodElement methodElement = writeMethod.get();
if (methodElement.hasStereotype(Deprecated.class) || !methodElement.isPublic()) {
return false;
}
return methodElement.getParameters().length <= 2;
}).forEach(propertyElement -> {
MethodElement methodElement = propertyElement.getWriteMethod().get();
String propertyName = propertyElement.getName();
ParameterElement[] params = methodElement.getParameters();
int paramCount = params.length;
if (paramCount < 2) {
ParameterElement parameterElement = paramCount == 1 ? params[0] : null;
ClassElement parameterElementType = parameterElement == null ? null : parameterElement.getGenericType();
PropertyMetadata metadata = metadataBuilder.visitProperty(
classElement,
builderElement.getDeclaringType(),
propertyElement.getType(),
configurationPrefix + propertyName,
null,
null
);
visitor.visitConfigBuilderMethod(
propertyName,
methodElement.getReturnType(),
methodElement.getSimpleName(),
parameterElementType,
parameterElementType != null ? parameterElementType.getTypeArguments() : null,
metadata.getPath()
);
} else if (paramCount == 2) {
// check the params are a long and a TimeUnit
ParameterElement first = params[0];
ParameterElement second = params[1];
ClassElement firstParamType = first.getType();
ClassElement secondParamType = second.getType();
if (firstParamType.getSimpleName().equals("long") && secondParamType.isAssignable(TimeUnit.class)) {
PropertyMetadata metadata = metadataBuilder.visitProperty(
classElement,
methodElement.getDeclaringType(),
visitorContext.getClassElement(Duration.class.getName()).get(),
configurationPrefix + propertyName,
null,
null
);
visitor.visitConfigBuilderDurationMethod(
propertyName,
methodElement.getReturnType(),
methodElement.getSimpleName(),
metadata.getPath()
);
}
}
});
} finally {
visitor.visitConfigBuilderEnd();
}
}
}