BeanDefinitionDelegate.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;

import io.micronaut.context.annotation.EachBean;
import io.micronaut.context.annotation.Parameter;
import io.micronaut.context.annotation.Primary;
import io.micronaut.context.env.ConfigurationPath;
import io.micronaut.context.exceptions.BeanInstantiationException;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.naming.NameResolver;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ObjectUtils;
import io.micronaut.inject.BeanDefinition;
import io.micronaut.inject.DelegatingBeanDefinition;
import io.micronaut.inject.DisposableBeanDefinition;
import io.micronaut.inject.InitializingBeanDefinition;
import io.micronaut.inject.InjectableBeanDefinition;
import io.micronaut.inject.InjectionPoint;
import io.micronaut.inject.InstantiatableBeanDefinition;
import io.micronaut.inject.ParametrizedInstantiatableBeanDefinition;
import io.micronaut.inject.ValidatedBeanDefinition;
import io.micronaut.inject.qualifiers.PrimaryQualifier;
import io.micronaut.inject.qualifiers.Qualifiers;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
 * A delegate bean definition.
 *
 * @param <T> The bean type
 * @author Graeme Rocher
 * @since 1.0
 */
@Internal
sealed class BeanDefinitionDelegate<T> extends AbstractBeanContextConditional
    implements DelegatingBeanDefinition<T>, InstantiatableBeanDefinition<T>,
    InjectableBeanDefinition<T>, NameResolver {

    protected final BeanDefinition<T> definition;
    @Nullable
    protected final Qualifier<T> qualifier;

    @Nullable
    private final ConfigurationPath configurationPath;

    private final Map<String, List<Argument<?>>> typeArgumentsMap;

    private BeanDefinitionDelegate(BeanDefinition<T> definition, @Nullable Qualifier<T> qualifier,
                                   @Nullable ConfigurationPath configurationPath,
                                   @NonNull Map<String, List<Argument<?>>> typeArgumentsMap) {
        this.definition = definition;
        this.qualifier = qualifier;
        this.configurationPath = configurationPath;
        this.typeArgumentsMap = typeArgumentsMap;
    }

    public Optional<ConfigurationPath> getConfigurationPath() {
        return Optional.ofNullable(configurationPath);
    }

    @Override
    public List<Argument<?>> getTypeArguments(String type) {
        List<Argument<?>> arguments = typeArgumentsMap.get(type);
        return arguments == null ? getTarget().getTypeArguments(type) : arguments;
    }

    @Override
    public Qualifier<T> getDeclaredQualifier() {
        return qualifier;
    }

    /**
     * @return the qualifier
     */
    @Nullable
    public Qualifier<T> getQualifier() {
        return qualifier;
    }

    @Nullable
    @Override
    public Qualifier<T> resolveDynamicQualifier() {
        return qualifier;
    }

    /**
     * @return The bean definition type
     */
    BeanDefinition<T> getDelegate() {
        return definition;
    }

    @Override
    public boolean isProxy() {
        return definition.isProxy();
    }

    @Override
    public boolean isIterable() {
        return definition.isIterable();
    }

    @Override
    public boolean isPrimary() {
        return isQualifiedAsPrimary(qualifier) || definition.isPrimary() || isPrimaryThroughAttribute();
    }

    private boolean isQualifiedAsPrimary(Qualifier<?> q) {
        return q != null && (q == PrimaryQualifier.INSTANCE || q.contains(PrimaryQualifier.INSTANCE));
    }

    private boolean isPrimaryThroughAttribute() {
        if (configurationPath != null) {
            return configurationPath.isPrimary();
        }
        return false;
    }

    @Override
    public T inject(BeanContext context, T bean) {
        if (definition instanceof InjectableBeanDefinition<T> injectableBeanDefinition) {
            return injectableBeanDefinition.inject(context, bean);
        }
        return bean;
    }

    @Override
    public T inject(BeanResolutionContext resolutionContext, BeanContext context, T bean) {
        if (definition instanceof InjectableBeanDefinition<T> injectableBeanDefinition) {
            return injectableBeanDefinition.inject(resolutionContext, context, bean);
        }
        return bean;
    }

    @Override
    public T instantiate(BeanResolutionContext resolutionContext, BeanContext context) throws BeanInstantiationException {
        ConfigurationPath oldPath = null;
        if (configurationPath != null) {
            oldPath = resolutionContext.getConfigurationPath();
            resolutionContext.setConfigurationPath(configurationPath);
        }
        try {
            if (this.definition instanceof ParametrizedInstantiatableBeanDefinition<T> parametrizedInstantiatableBeanDefinition) {
                Argument<Object>[] requiredArguments = parametrizedInstantiatableBeanDefinition.getRequiredArguments();
                Map<String, Object> fulfilled = getParametersValues(resolutionContext, (DefaultBeanContext) context, definition, requiredArguments);
                return parametrizedInstantiatableBeanDefinition.instantiate(resolutionContext, context, fulfilled);
            }
            if (this.definition instanceof InstantiatableBeanDefinition<T> instantiatableBeanDefinition) {
                return instantiatableBeanDefinition.instantiate(resolutionContext, context);
            }
            throw new IllegalStateException("Cannot construct a dynamically registered singleton");
        } finally {
            resolutionContext.setConfigurationPath(oldPath);
        }
    }

    @Nullable
    private Map<String, Object> getParametersValues(BeanResolutionContext resolutionContext,
                                                    DefaultBeanContext context,
                                                    BeanDefinition<T> definition,
                                                    Argument<Object>[] requiredArguments) {
        if (requiredArguments.length == 0) {
            return Collections.emptyMap();
        }
        Map<String, Object> fulfilled = new LinkedHashMap<>(requiredArguments.length, 1);
        ConfigurationPath configurationPath = resolutionContext.getConfigurationPath();
        for (Argument<Object> argument : requiredArguments) {
            String argumentName = argument.getName();
            if (argument.isAnnotationPresent(Parameter.class)) {
                Class<?> type = argument.getWrapperType();
                boolean isEnum = Enum.class.isAssignableFrom(type);
                if (CharSequence.class.isAssignableFrom(type) || isEnum) {
                    String simpleName = configurationPath.simpleName();
                    if (simpleName != null) {
                        Object value = isEnum ? context.getConversionService().convertRequired(simpleName, type) : simpleName;
                        fulfilled.put(argumentName, value);
                    } else {
                        String name = findName(resolutionContext.getCurrentQualifier());
                        if (name != null) {
                            Object value = isEnum ? context.getConversionService().convertRequired(name, type) : name;
                            fulfilled.put(argumentName, value);
                        }
                    }
                } else if (Number.class.isAssignableFrom(type)) {
                    fulfilled.put(argumentName, context.getConversionService().convertRequired(configurationPath.index(), argument));
                } else if (qualifier != null && hasDeclaredAnnotation(EachBean.class) && String.class.equals(type) && "name".equals(argumentName)) {
                    String name = findName(qualifier);
                    if (name != null) {
                        fulfilled.put(argumentName, name);
                    }
                } else {
                    if (argument.isProvider()) {
                        Argument<?> pt = argument.getFirstTypeVariable().orElse(null);
                        if (pt != null) {
                            type = pt.getType();
                        }
                    }

                    try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushConstructorResolve(definition, argument)) {
                        if (type.equals(configurationPath.configurationType())) {
                            Object bean = context.findBean(resolutionContext, argument, configurationPath.beanQualifier()).orElse(null);
                            fulfilled.put(argumentName, bean);
                        } else {
                            ConfigurationPath old = resolutionContext.setConfigurationPath(null);// reset
                            try {
                                Qualifier<Object> q = qualifier != null ? (Qualifier<Object>) qualifier : configurationPath.beanQualifier();
                                Object bean = context.findBean(resolutionContext, argument, q).orElse(null);
                                fulfilled.put(argumentName, bean);
                            } finally {
                                resolutionContext.setConfigurationPath(old);
                            }
                        }
                    }
                }
            }
        }
        return fulfilled;
    }

    @Nullable
    private String findName(@Nullable Qualifier<?> q) {
        if (q == null) {
            return null;
        }
        String name = Qualifiers.findName(q);
        if (name != null) {
            return name;
        }
        if (isQualifiedAsPrimary(q)) {
            return Primary.SIMPLE_NAME;
        }
        return null;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        BeanDefinitionDelegate<?> that = (BeanDefinitionDelegate<?>) o;
        return Objects.equals(definition, that.definition) &&
            Objects.equals(qualifier, that.qualifier);
    }

    @Override
    public int hashCode() {
        return ObjectUtils.hash(definition, qualifier);
    }

    /**
     * @return The bean definition type
     */
    @Override
    public BeanDefinition<T> getTarget() {
        return definition;
    }

    @Override
    public Optional<String> resolveName() {
        return Optional.ofNullable(findName(qualifier));
    }

    @Override
    public String toString() {
        return definition.toString();
    }

    /**
     * @param definition The bean definition type
     * @param <T>        The type
     * @return The new bean definition
     */
    static <T> BeanDefinitionDelegate<T> create(BeanDefinition<T> definition) {
        return create(definition, null);
    }

    /**
     * @param definition The bean definition type
     * @param qualifier The bean qualifier
     * @param <T>        The type
     * @return The new bean definition
     */
    static <T> BeanDefinitionDelegate<T> create(BeanDefinition<T> definition, Qualifier<T> qualifier) {
        return create(definition, qualifier, null, Map.of());
    }

    /**
     * @param definition       The bean definition type
     * @param qualifier        The bean qualifier
     * @param typeArgumentsMap The type arguments
     * @param <T>              The type
     * @return The new bean definition
     * @since 4.6
     */
    static <T> BeanDefinitionDelegate<T> create(BeanDefinition<T> definition,
                                                Qualifier<T> qualifier,
                                                @NonNull Map<String, List<Argument<?>>> typeArgumentsMap) {
        return create(definition, qualifier, null, typeArgumentsMap);
    }

    /**
     * @param definition       The bean definition type
     * @param qualifier        The bean qualifier
     * @param path             The configuration path.
     * @param typeArgumentsMap The type arguments
     * @param <T>              The type
     * @return The new bean definition
     */
    static <T> BeanDefinitionDelegate<T> create(BeanDefinition<T> definition,
                                                Qualifier<T> qualifier,
                                                ConfigurationPath path,
                                                @NonNull Map<String, List<Argument<?>>> typeArgumentsMap) {
        if (definition instanceof InitializingBeanDefinition || definition instanceof DisposableBeanDefinition) {
            if (definition instanceof ValidatedBeanDefinition) {
                return new LifeCycleValidatingDelegate<>(definition, qualifier, path, typeArgumentsMap);
            } else {
                return new LifeCycleDelegate<>(definition, qualifier, path, typeArgumentsMap);
            }
        } else if (definition instanceof ValidatedBeanDefinition) {
            return new ValidatingDelegate<>(definition, qualifier, path, typeArgumentsMap);
        }
        return new BeanDefinitionDelegate<>(definition, qualifier, path, typeArgumentsMap);
    }

    /**
     * @param definition The bean definition type
     * @param qualifier The bean qualifier
     * @param path The configuration path.
     * @param <T>        The type
     * @return The new bean definition
     */
    static <T> BeanDefinitionDelegate<T> create(BeanDefinition<T> definition,
                                                Qualifier<T> qualifier,
                                                ConfigurationPath path) {
        return create(definition, qualifier, path, Map.of());
    }

    @Override
    @NonNull
    public String getName() {
        return definition.getName();
    }

    /**
     * @param <T> The bean definition type
     */
    sealed interface ProxyInitializingBeanDefinition<T> extends DelegatingBeanDefinition<T>, InitializingBeanDefinition<T> {
        @Override
        default T initialize(BeanResolutionContext resolutionContext, BeanContext context, T bean) {
            BeanDefinition<T> definition = getTarget();
            if (definition instanceof InitializingBeanDefinition) {
                return ((InitializingBeanDefinition<T>) definition).initialize(resolutionContext, context, bean);
            }
            return bean;
        }
    }

    /**
     * @param <T> The bean definition type
     */
    sealed interface ProxyDisposableBeanDefinition<T> extends DelegatingBeanDefinition<T>, DisposableBeanDefinition<T> {
        @Override
        default T dispose(BeanResolutionContext resolutionContext, BeanContext context, T bean) {
            BeanDefinition<T> definition = getTarget();
            if (definition instanceof DisposableBeanDefinition) {
                return ((DisposableBeanDefinition<T>) definition).dispose(resolutionContext, context, bean);
            }
            return bean;
        }
    }

    /**
     * @param <T> The bean definition type
     */
    sealed interface ProxyValidatingBeanDefinition<T> extends DelegatingBeanDefinition<T>, ValidatedBeanDefinition<T> {
        @Override
        default T validate(BeanResolutionContext resolutionContext, T instance) {
            BeanDefinition<T> definition = getTarget();
            if (definition instanceof ValidatedBeanDefinition) {
                return ((ValidatedBeanDefinition<T>) definition).validate(resolutionContext, instance);
            }
            return instance;
        }

        @Override
        default <V> void validateBeanArgument(@NonNull BeanResolutionContext resolutionContext, @NonNull InjectionPoint injectionPoint, @NonNull Argument<V> argument, int index, @Nullable V value) {
            BeanDefinition<T> definition = getTarget();
            if (definition instanceof ValidatedBeanDefinition) {
                ((ValidatedBeanDefinition<T>) definition).validateBeanArgument(
                        resolutionContext,
                        injectionPoint,
                        argument,
                        index,
                        value
                );
            }
        }
    }

    /**
     * @param <T> The bean definition type
     */
    private static final class LifeCycleDelegate<T> extends BeanDefinitionDelegate<T> implements ProxyInitializingBeanDefinition<T>, ProxyDisposableBeanDefinition<T> {
        private LifeCycleDelegate(BeanDefinition<T> definition, Qualifier qualifier, ConfigurationPath path, @NonNull Map<String, List<Argument<?>>> typeArgumentsMap) {
            super(definition, qualifier, path, typeArgumentsMap);
        }
    }

    /**
     * @param <T> The bean definition type
     */
    private static final class ValidatingDelegate<T> extends BeanDefinitionDelegate<T> implements ProxyValidatingBeanDefinition<T> {
        private ValidatingDelegate(BeanDefinition<T> definition, Qualifier qualifier, ConfigurationPath path, @NonNull Map<String, List<Argument<?>>> typeArgumentsMap) {
            super(definition, qualifier, path, typeArgumentsMap);
        }
    }

    /**
     * @param <T> The bean definition type
     */
    private static final class LifeCycleValidatingDelegate<T> extends BeanDefinitionDelegate<T> implements ProxyValidatingBeanDefinition<T>, ProxyInitializingBeanDefinition<T>, ProxyDisposableBeanDefinition<T> {
        private LifeCycleValidatingDelegate(BeanDefinition<T> definition, Qualifier qualifier, ConfigurationPath path, @NonNull Map<String, List<Argument<?>>> typeArgumentsMap) {
            super(definition, qualifier, path, typeArgumentsMap);
        }
    }
}