ConfigurationReaderVisitor.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.context.visitor;

import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.ConfigurationReader;
import io.micronaut.context.annotation.EachProperty;
import io.micronaut.context.annotation.Property;
import io.micronaut.core.annotation.AccessorsStyle;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.AnnotationUtil;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.type.DefaultArgument;
import io.micronaut.inject.ast.ParameterElement;
import io.micronaut.inject.validation.RequiresValidation;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.MethodElement;
import io.micronaut.inject.configuration.ConfigurationMetadata;
import io.micronaut.inject.configuration.ConfigurationMetadataBuilder;
import io.micronaut.inject.visitor.TypeElementVisitor;
import io.micronaut.inject.visitor.VisitorContext;

import java.util.Map;

/**
 * The visitor adds Validated annotation if one of the parameters is a constraint or @Valid.
 *
 * @author Denis Stepanov
 * @since 3.7.0
 */
@Internal
public class ConfigurationReaderVisitor implements TypeElementVisitor<ConfigurationReader, Object> {

    private static final String ANN_CONFIGURATION_ADVICE = "io.micronaut.runtime.context.env.ConfigurationAdvice";

    private final ConfigurationMetadataBuilder metadataBuilder = ConfigurationMetadataBuilder.INSTANCE;
    private String[] readPrefixes;

    @NonNull
    @Override
    public VisitorKind getVisitorKind() {
        return VisitorKind.ISOLATING;
    }

    @Override
    public void finish(VisitorContext visitorContext) {
        reset();
    }

    @Override
    public void visitClass(ClassElement classElement, VisitorContext context) {
        reset();

        if (!classElement.hasStereotype(ConfigurationReader.class)) {
            return;
        }

        ConfigurationMetadata configurationMetadata = metadataBuilder.visitProperties(classElement);
        if (configurationMetadata != null) {
            classElement.annotate(ConfigurationReader.class, builder -> builder.member(ConfigurationReader.PREFIX, configurationMetadata.getName()));
        }

        if (classElement.isInterface()) {
            classElement.annotate(ANN_CONFIGURATION_ADVICE);
        }
        if (classElement.hasStereotype(RequiresValidation.class)) {
            classElement.annotate(Introspected.class);
        }

        AnnotationMetadata annotationMetadata = classElement.getAnnotationMetadata();
        readPrefixes = annotationMetadata.getValue(AccessorsStyle.class, "readPrefixes", String[].class)
            .orElse(new String[]{AccessorsStyle.DEFAULT_READ_PREFIX});
    }

    private void reset() {
        readPrefixes = null;
    }

    @Override
    public void visitMethod(MethodElement method, VisitorContext context) {
        if (method.isAbstract()) {
            visitAbstractMethod(method, context);
        }
    }

    public static boolean isPropertyParameter(ParameterElement parameter, VisitorContext visitorContext) {
        ClassElement genericType = parameter.getGenericType();
        return isPropertyParameter(genericType, visitorContext);
    }

    private static boolean isPropertyParameter(ClassElement genericType, VisitorContext visitorContext) {
        if (genericType.isOptional() || genericType.isContainerType() || isProvider(genericType)) {
            ClassElement finalParameterType = genericType;
            genericType = genericType.getOptionalValueType().or(finalParameterType::getFirstTypeArgument).orElse(genericType);
            // Get the class with type annotations
            genericType = visitorContext.getClassElement(genericType.getCanonicalName()).orElse(genericType);
        } else if (genericType.isAssignable(Map.class)) {
            ClassElement t = genericType.getTypeArguments().get("V");
            if (t != null) {
                genericType = t;
            }
        }
        return !genericType.hasStereotype(AnnotationUtil.SCOPE) && !genericType.hasStereotype(Bean.class);
    }

    private static boolean isProvider(ClassElement genericType) {
        String name = genericType.getName();
        for (String type : DefaultArgument.PROVIDER_TYPES) {
            if (name.equals(type)) {
                return true;
            }
        }
        return false;
    }

    private void visitAbstractMethod(MethodElement method, VisitorContext context) {
        String methodName = method.getName();
        if (!isGetter(methodName)) {
            context.fail("Only getter methods are allowed on @ConfigurationProperties interfaces: " + method + ". You can change the accessors using @AccessorsStyle annotation", method.getOwningType());
            return;
        }
        if (method.hasParameters()) {
            context.fail("Only zero argument getter methods are allowed on @ConfigurationProperties interfaces: " + method, method);
            return;
        }
        if (method.getReturnType().isVoid()) {
            context.fail("Getter methods must return a value @ConfigurationProperties interfaces: " + method, method);
            return;
        }

        boolean isPropertyParameter = isPropertyParameter(method.getGenericReturnType(), context);
        if (isPropertyParameter) {
            final String propertyName = getPropertyNameForGetter(methodName);
            String path = metadataBuilder.visitProperty(
                method.getOwningType(),
                method.getOwningType(), // interface methods don't inherit the prefix
                method.getReturnType(),
                propertyName,
                ConfigurationMetadataBuilder.resolveJavadocDescription(method),
                method.getAnnotationMetadata().stringValue(Bindable.class, "defaultValue").orElse(null)
            ).getPath();

            method.annotate(Property.class, builder -> builder.member("name", path));
        }

        method.annotate(ANN_CONFIGURATION_ADVICE, annBuilder -> {

            if (!isPropertyParameter) {
                annBuilder.member("bean", true);
            }
            if (method.hasStereotype(EachProperty.class)) {
                annBuilder.member("iterable", true);
            }
        });
    }

    private String getPropertyNameForGetter(String methodName) {
        return NameUtils.getPropertyNameForGetter(methodName, readPrefixes);
    }

    private boolean isGetter(String methodName) {
        return NameUtils.isReaderName(methodName, readPrefixes);
    }

}