ConfigurationIntroductionAdvice.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.runtime.context.env;

import io.micronaut.aop.InterceptorBean;
import io.micronaut.aop.MethodInterceptor;
import io.micronaut.aop.MethodInvocationContext;
import io.micronaut.context.BeanContext;
import io.micronaut.context.BeanResolutionContext;
import io.micronaut.context.DefaultBeanContext;
import io.micronaut.context.DefaultBeanResolutionContext;
import io.micronaut.context.Qualifier;
import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.context.annotation.Property;
import io.micronaut.context.annotation.Prototype;
import io.micronaut.context.env.ConfigurationPath;
import io.micronaut.context.env.Environment;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.core.type.Argument;
import io.micronaut.core.type.ReturnType;
import io.micronaut.core.value.PropertyNotFoundException;
import io.micronaut.inject.BeanDefinition;

import java.util.Collections;
import java.util.Optional;

/**
 * Internal introduction advice used to allow {@link io.micronaut.context.annotation.ConfigurationProperties} on interfaces. Considered internal and not for direct use.
 *
 * @author graemerocher
 * @see ConfigurationAdvice
 * @see io.micronaut.context.annotation.ConfigurationProperties
 * @since 1.3.0
 */
@Prototype
@Internal
@BootstrapContextCompatible
@InterceptorBean(ConfigurationAdvice.class)
public class ConfigurationIntroductionAdvice implements MethodInterceptor<Object, Object> {

    private static final String MEMBER_BEAN = "bean";
    private static final String MEMBER_NAME = "name";
    private final Environment environment;
    private final BeanContext beanContext;
    private final ConfigurationPath configurationPath;
    private final BeanDefinition<?> beanDefinition;

    /**
     * Default constructor.
     *
     * @param resolutionContext The resolution context
     * @param environment       The environment
     * @param beanContext       The bean locator
     */
    ConfigurationIntroductionAdvice(
        BeanResolutionContext resolutionContext,
        Environment environment,
        BeanContext beanContext) {
        this.beanDefinition = resolutionContext.getRootDefinition();
        this.environment = environment;
        this.beanContext = beanContext;
        this.configurationPath = resolutionContext.getConfigurationPath().copy();
    }

    @Nullable
    @Override
    public Object intercept(MethodInvocationContext<Object, Object> context) {
        final ReturnType<Object> rt = context.getReturnType();
        final Class<Object> returnType = rt.getType();
        final Argument<Object> argument = rt.asArgument();
        if (context.isTrue(ConfigurationAdvice.class, MEMBER_BEAN)) {
            return resolveBean(context, returnType, argument);
        } else {
            return resolveProperty(context, rt, argument);
        }
    }

    private Object resolveProperty(MethodInvocationContext<Object, Object> context, ReturnType<Object> rt, Argument<Object> argument) {
        String property = context.stringValue(Property.class, MEMBER_NAME).orElse(null);
        if (property == null) {
            throw new IllegalStateException("No property name available to resolve");
        }
        if (configurationPath.hasDynamicSegments()) {
            property = configurationPath.resolveValue(property);
        }
        final String defaultValue = context.stringValue(Bindable.class, "defaultValue").orElse(null);

        final Optional<Object> value = environment.getProperty(
            property,
            argument
        );

        if (defaultValue != null) {
            Object result = value.orElse(null);
            if (result == null) {
                return environment.convertRequired(defaultValue, argument);
            }
            return result;
        } else if (rt.isOptional()) {
            return value.orElse(Optional.empty());
        } else if (context.isNullable()) {
            return value.orElse(null);
        } else {
            String finalProperty = property;
            return value.orElseThrow(() -> new PropertyNotFoundException(finalProperty, argument.getType()));
        }
    }

    private Object resolveBean(MethodInvocationContext<Object, Object> context, Class<Object> returnType, Argument<Object> argument) {
        final Qualifier<Object> qualifier = configurationPath.beanQualifier();
        if (Iterable.class.isAssignableFrom(returnType)) {
            @SuppressWarnings("unchecked")
            Argument<Object> firstArg = (Argument<Object>) argument.getFirstTypeVariable().orElse(null);
            if (firstArg != null) {
                return environment.convertRequired(beanContext.getBeansOfType(firstArg, qualifier), argument);
            } else {
                return environment.convertRequired(Collections.emptyMap(), argument);
            }
        } else if (context.isNullable()) {
            final Object v = beanContext.findBean(argument, qualifier).orElse(null);
            if (v != null) {
                return environment.convertRequired(v, returnType);
            } else {
                return v;
            }
        } else {
            try (BeanResolutionContext rc = new DefaultBeanResolutionContext(beanContext, beanDefinition)) {
                rc.setConfigurationPath(configurationPath);
                return environment.convertRequired(
                    ((DefaultBeanContext) beanContext).getBean(rc, argument, qualifier),
                    returnType
                );
            }
        }
    }
}