All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.micronaut.guice.GuiceModuleBinder Maven / Gradle / Ivy

There is a newer version: 1.1.0
Show newest version
/*
 * Copyright 2017-2024 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.guice;

import com.google.inject.Binder;
import com.google.inject.Binding;
import com.google.inject.BindingAnnotation;
import com.google.inject.CreationException;
import com.google.inject.ImplementedBy;
import com.google.inject.Key;
import com.google.inject.MembersInjector;
import com.google.inject.Module;
import com.google.inject.PrivateBinder;
import com.google.inject.ProvidedBy;
import com.google.inject.Provider;
import com.google.inject.Scope;
import com.google.inject.Scopes;
import com.google.inject.Singleton;
import com.google.inject.Stage;
import com.google.inject.TypeLiteral;
import com.google.inject.binder.AnnotatedBindingBuilder;
import com.google.inject.binder.AnnotatedConstantBindingBuilder;
import com.google.inject.binder.ConstantBindingBuilder;
import com.google.inject.binder.LinkedBindingBuilder;
import com.google.inject.binder.ScopedBindingBuilder;
import com.google.inject.matcher.Matcher;
import com.google.inject.name.Named;
import com.google.inject.spi.Dependency;
import com.google.inject.spi.Message;
import com.google.inject.spi.ModuleAnnotatedMethodScanner;
import com.google.inject.spi.ProvisionListener;
import com.google.inject.spi.TypeConverter;
import com.google.inject.spi.TypeListener;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.BeanProvider;
import io.micronaut.context.RuntimeBeanDefinition;
import io.micronaut.context.annotation.Context;
import io.micronaut.context.env.Environment;
import io.micronaut.context.event.StartupEvent;
import io.micronaut.context.exceptions.BeanInstantiationException;
import io.micronaut.context.exceptions.ConfigurationException;
import io.micronaut.context.exceptions.NoSuchBeanException;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Order;
import io.micronaut.core.beans.BeanIntrospection;
import io.micronaut.core.beans.BeanIntrospector;
import io.micronaut.core.order.Ordered;
import io.micronaut.core.reflect.InstantiationUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.StringUtils;
import io.micronaut.inject.BeanDefinition;
import io.micronaut.inject.annotation.MutableAnnotationMetadata;
import io.micronaut.inject.qualifiers.PrimaryQualifier;
import io.micronaut.inject.qualifiers.Qualifiers;
import io.micronaut.runtime.event.annotation.EventListener;
import jakarta.inject.Qualifier;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import org.aopalliance.intercept.MethodInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Context
@Order(Ordered.HIGHEST_PRECEDENCE)
@Internal
class GuiceModuleBinder implements Binder {
    private static final Logger LOG = LoggerFactory.getLogger(GuiceModuleBinder.class);
    private final ApplicationContext applicationContext;
    private final List> linkedBindingBuilders = new ArrayList<>();
    private final List constantBuilders = new ArrayList<>();
    private final List errors = new ArrayList<>();
    private final List toInject = new ArrayList<>();
    private Object currentSource;

    GuiceModuleBinder(
        ApplicationContext applicationContext,
        List modules) {
        this.applicationContext = applicationContext;
        for (Module module : modules) {
            withSource(module);
            module.configure(this);
        }
        try {
            if (!this.errors.isEmpty()) {
                for (Message error : errors) {
                    Throwable cause = error.getCause();
                    if (cause != null) {
                        LOG.error("Guice Module Error: " + error.getMessage(), cause);
                    } else {
                        LOG.error("Guice Module Error: {}", error.getMessage());
                    }
                }
                throw new ConfigurationException("Failed to import modules due to prior errors");
            }
            for (LinkedBindingBuilderImpl builder : linkedBindingBuilders) {
                RuntimeBeanDefinition beanDefinition = builder.build();
                if (beanDefinition != null) {
                    applicationContext.registerBeanDefinition(beanDefinition);
                }
            }

            for (AnnotatedConstantBindingBuilderImpl constantBuilder : constantBuilders) {
                RuntimeBeanDefinition beanDefinition = constantBuilder.build();
                applicationContext.registerBeanDefinition(beanDefinition);
            }
        } finally {
            linkedBindingBuilders.clear();
            constantBuilders.clear();
        }
    }

    @EventListener
    void onStartup(StartupEvent startupEvent) {
        // run more injections
        try {
            for (Object o : toInject) {
                applicationContext.inject(o);
            }
        } finally {
            toInject.clear();
        }
    }

    @Override
    public void bindInterceptor(Matcher> classMatcher, Matcher methodMatcher, MethodInterceptor... interceptors) {
        throw new UnsupportedOperationException("Guice interceptors are not supported");
    }

    @Override
    public void bindScope(Class annotationType, Scope scope) {
        if (LinkedBindingBuilderImpl.isCustomScope(scope)) {
            throw new UnsupportedOperationException("Guice custom scopes are not supported");
        }
    }

    @Override
    public  LinkedBindingBuilder bind(Key key) {
        @SuppressWarnings("unchecked")
        Argument argument = ((Argument) Argument.of(key.getTypeLiteral().getType()));
        return bind(argument);
    }

    @Override
    public  AnnotatedBindingBuilder bind(TypeLiteral typeLiteral) {
        @SuppressWarnings("unchecked")
        Argument argument = (Argument) Argument.of(typeLiteral.getType());
        return bind(argument);
    }

    @Override
    public  AnnotatedBindingBuilder bind(Class type) {
        return bind(Argument.of(type));
    }

    private  AnnotatedBindingBuilder bind(Argument argument) {
        LinkedBindingBuilderImpl builder = new LinkedBindingBuilderImpl<>(argument);
        linkedBindingBuilders.add(builder);
        return builder;
    }

    @Override
    public AnnotatedConstantBindingBuilder bindConstant() {
        AnnotatedConstantBindingBuilderImpl builder = new AnnotatedConstantBindingBuilderImpl();
        constantBuilders.add(builder);
        return builder;
    }

    @Override
    public  void requestInjection(TypeLiteral type, T instance) {
        requestInjection(instance);
    }

    @Override
    public void requestInjection(Object instance) {
        if (!toInject.contains(instance)) {
            toInject.add(instance);
        }

    }

    @Override
    public void requestStaticInjection(Class... types) {
        throw new UnsupportedOperationException("Static injection is not supported");
    }

    @Override
    public void install(Module module) {
        module.configure(this);
    }

    @Override
    public Stage currentStage() {
        Set activeNames = applicationContext.getEnvironment().getActiveNames();
        if (activeNames.contains(Environment.DEVELOPMENT) || activeNames.contains(Environment.TEST)) {
            return Stage.DEVELOPMENT;
        }
        return Stage.PRODUCTION;
    }

    @Override
    public void addError(String message, Object... arguments) {
        Objects.requireNonNull(message, "Message cannot be null");
        String msg = String.format(message, arguments);
        addError(new Message(msg));
    }

    @Override
    public void addError(Throwable t) {
        Objects.requireNonNull(t, "Throwable cannot be null");
        addError(new Message(t.getMessage(), t));
    }

    @Override
    public void addError(Message message) {
        Objects.requireNonNull(message, "Message cannot be null");
        errors.add(message);
    }

    @Override
    public  Provider getProvider(Key key) {
        Objects.requireNonNull(key, "Key cannot be null");
        @SuppressWarnings("unchecked")
        Argument argument = (Argument) Argument.of(key.getTypeLiteral().getType());
        @SuppressWarnings("unchecked")
        BeanProvider provider = applicationContext.getBean(Argument.of(BeanProvider.class, argument));
        return provider::get;
    }

    @Override
    public  Provider getProvider(Dependency dependency) {
        Objects.requireNonNull(dependency, "Dependency cannot be null");
        return getProvider(dependency.getKey());
    }

    @Override
    public  Provider getProvider(Class type) {
        Objects.requireNonNull(type, "Type cannot be null");
        @SuppressWarnings("unchecked")
        BeanProvider provider = applicationContext.getBean(Argument.of(BeanProvider.class, type));
        return provider::get;
    }

    @Override
    public  MembersInjector getMembersInjector(TypeLiteral typeLiteral) {
        return instance -> {
            if (!applicationContext.isRunning()) {
                throw new IllegalStateException("Injector not started");
            }
            applicationContext.inject(instance);
        };
    }

    @Override
    public  MembersInjector getMembersInjector(Class type) {
        return instance -> {
            if (!applicationContext.isRunning()) {
                throw new IllegalStateException("Injector not started");
            }
            applicationContext.inject(instance);
        };
    }

    @Override
    public void convertToTypes(Matcher> typeMatcher, TypeConverter converter) {
        throw new UnsupportedOperationException("Method convertToTypes is not supported");
    }

    @Override
    public void bindListener(Matcher> typeMatcher, TypeListener listener) {
        throw new UnsupportedOperationException("Method bindListener is not supported");
    }

    @Override
    public void bindListener(Matcher> bindingMatcher, ProvisionListener... listeners) {
        throw new UnsupportedOperationException("Method bindListener is not supported");
    }

    @Override
    public Binder withSource(Object source) {
        this.currentSource = source;
        return this;
    }

    @Override
    public Binder skipSources(Class... classesToSkip) {
        throw new UnsupportedOperationException("Method skipSources is not supported");
    }

    @Override
    public PrivateBinder newPrivateBinder() {
        throw new UnsupportedOperationException("Private bindings are not supported");
    }

    @Override
    public void requireExplicitBindings() {
        // no-op
    }

    @Override
    public void disableCircularProxies() {
        // no-op
    }

    @Override
    public void requireAtInjectOnConstructors() {
        // no-op
    }

    @Override
    public void requireExactBindingAnnotations() {
        // no-op
    }

    @Override
    public void scanModulesForAnnotatedMethods(ModuleAnnotatedMethodScanner scanner) {
        // no-op
    }

    private static  void bindQualifier(RuntimeBeanDefinition.Builder builder, String beanName, Class beanQualifier) {
        if (StringUtils.isNotEmpty(beanName)) {
            builder.named(beanName);
        } else {
            if (beanQualifier != null) {
                MutableAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata();
                annotationMetadata.addAnnotation(beanQualifier.getName(), Map.of());
                builder.annotationMetadata(annotationMetadata);
                builder.qualifier(Qualifiers.byAnnotation(annotationMetadata, beanQualifier));
            } else {
                builder.qualifier(PrimaryQualifier.INSTANCE);
            }
        }
    }

    private static void validateBindingAnnotation(Class annotationType) {
        Objects.requireNonNull(annotationType, "Annotation type cannot be null");
        if (annotationType.getAnnotation(BindingAnnotation.class) == null && annotationType.getAnnotation(Qualifier.class) == null) {
            throw new IllegalArgumentException("Annotation type must be annotated itself with either @BindingAnnotation or jakarta.inject.Qualifier");
        }
    }

    private static class AnnotatedConstantBindingBuilderImpl implements AnnotatedConstantBindingBuilder, ConstantBindingBuilder {
        private Object value;
        private Class annotationType;
        private String name;

        @Override
        public ConstantBindingBuilder annotatedWith(Class annotationType) {
            Objects.requireNonNull(annotationType, "Annotation type cannot be null");
            this.annotationType = annotationType;
            return this;
        }

        @Override
        public ConstantBindingBuilder annotatedWith(Annotation annotation) {
            Objects.requireNonNull(annotation, "Annotation cannot be null");
            if (annotation instanceof Named named) {
                this.name = named.value();
            } else {
                this.annotationType = annotation.annotationType();
            }
            return this;
        }

        @Override
        public void to(String value) {
            this.value = value;
        }

        @Override
        public void to(int value) {
            this.value = value;
        }

        @Override
        public void to(long value) {
            this.value = value;
        }

        @Override
        public void to(boolean value) {
            this.value = value;
        }

        @Override
        public void to(double value) {
            this.value = value;
        }

        @Override
        public void to(float value) {
            this.value = value;
        }

        @Override
        public void to(short value) {
            this.value = value;
        }

        @Override
        public void to(char value) {
            this.value = value;
        }

        @Override
        public void to(byte value) {
            this.value = value;
        }

        @Override
        public void to(Class value) {
            this.value = value;
        }

        @Override
        public > void to(E value) {
            this.value = value;
        }

        @SuppressWarnings("java:S1452")
        public RuntimeBeanDefinition build() {
            Objects.requireNonNull(value, "Binding constant cannot be null, call one of the to(..) methods on the Guice binding");
            RuntimeBeanDefinition.Builder builder = RuntimeBeanDefinition.builder(value);
            bindQualifier(builder, name, annotationType);
            return builder.build();
        }
    }

    private class LinkedBindingBuilderImpl implements LinkedBindingBuilder, AnnotatedBindingBuilder {
        private final Argument beanType;

        private static final List> SINGLETON_CLASSES = List.of(
                Singleton.class,
                jakarta.inject.Singleton.class
        );

        private boolean isSingleton;
        private Class scope;

        private Supplier supplier;
        private Class annotationType;
        private String name;

        public LinkedBindingBuilderImpl(Argument argument) {
            this.beanType = argument;
        }

        @Override
        public ScopedBindingBuilder to(Class implementation) {
            if (applicationContext.containsBean(implementation)) {
                BeanProvider provider = applicationContext.getBean(Argument.of(BeanProvider.class, implementation));
                this.supplier = provider::get;
            } else {
                BeanIntrospection introspection = BeanIntrospector.SHARED.findIntrospection(implementation).orElse(null);
                if (introspection != null) {
                    this.supplier = introspection::instantiate;
                } else {
                    Message message = new Message(currentSource != null ? currentSource : implementation, "Cannot create  binding to type that is not itself declared a bean. " +
                        "Considering adding @Guice(classes=" + implementation.getSimpleName() + ".class) below your @Guice declaration.");
                    throw new com.google.inject.ConfigurationException(
                        List.of(message)
                    );
                }
            }
            return this;
        }

        @Override
        public ScopedBindingBuilder to(TypeLiteral implementation) {
            @SuppressWarnings("unchecked")
            Argument argument = (Argument) Argument.of(implementation.getType());
            return to(argument);
        }

        @Override
        public ScopedBindingBuilder to(Key targetKey) {
            @SuppressWarnings("unchecked")
            Argument argument = (Argument) Argument.of(targetKey.getTypeLiteral().getType());
            return to(argument);
        }

        private ScopedBindingBuilder to(Argument argument) {
            @SuppressWarnings("unchecked")
            BeanProvider provider = applicationContext.getBean(Argument.of(BeanProvider.class, argument));
            this.supplier = provider::get;
            return this;
        }

        @Override
        public void toInstance(T instance) {
            Objects.requireNonNull(instance, "Instance cannot be null");
            this.supplier = () -> instance;
        }

        @Override
        public ScopedBindingBuilder toProvider(Provider provider) {
            Objects.requireNonNull(provider, "Provider cannot be null");
            this.supplier = provider::get;
            return this;
        }

        @Override
        public ScopedBindingBuilder toProvider(jakarta.inject.Provider provider) {
            Objects.requireNonNull(provider, "Provider cannot be null");
            this.supplier = provider::get;
            return this;
        }

        @Override
        public ScopedBindingBuilder toProvider(Class> providerType) {
            Objects.requireNonNull(providerType, "Provider type cannot be null");
            @SuppressWarnings("unchecked")
            BeanProvider> provider = applicationContext.getBean(Argument.of(BeanProvider.class, providerType));
            this.supplier = () -> provider.get().get();
            return this;
        }

        @Override
        public ScopedBindingBuilder toProvider(TypeLiteral> providerType) {
            Objects.requireNonNull(providerType, "Provider type cannot be null");
            @SuppressWarnings("unchecked") Argument> argument =
                (Argument>) Argument.of(providerType.getType());
            @SuppressWarnings("unchecked")
            BeanProvider> provider = applicationContext.getBean(Argument.of(BeanProvider.class, argument));
            this.supplier = () -> provider.get().get();
            return this;
        }

        @Override
        public ScopedBindingBuilder toProvider(Key> providerKey) {
            Objects.requireNonNull(providerKey, "Provider type cannot be null");
            return toProvider(providerKey.getTypeLiteral());
        }

        @Override
        public  ScopedBindingBuilder toConstructor(Constructor constructor) {
            supplier = () -> InstantiationUtils.tryInstantiate(constructor)
                .orElseThrow(() -> new BeanInstantiationException("Unable to instance bean via constructor: " + constructor));
            return this;
        }

        @Override
        public  ScopedBindingBuilder toConstructor(Constructor constructor, TypeLiteral type) {
            supplier = () -> InstantiationUtils.tryInstantiate(constructor)
                .orElseThrow(() -> new BeanInstantiationException("Unable to instance bean via constructor: " + constructor));
            return this;
        }

        @Override
        public void in(Class scopeAnnotation) {
            if (SINGLETON_CLASSES.contains(scopeAnnotation)) {
                this.isSingleton = true;
            }
            this.scope = scopeAnnotation;
        }

        @Override
        public void in(Scope scope) {
            if (isCustomScope(scope)) {
                throw new IllegalArgumentException("Custom Guice scopes are not supported");
            }
            if (scope == Scopes.SINGLETON) {
                this.isSingleton = true;
            }
        }

        private static boolean isCustomScope(Scope scope) {
            return !(scope == Scopes.SINGLETON || scope == Scopes.NO_SCOPE);
        }

        @Override
        public void asEagerSingleton() {
            this.isSingleton = true;
            this.scope = Context.class;
        }

        public RuntimeBeanDefinition build() {
            Objects.requireNonNull(beanType, "Bean type cannot be null");
            if (supplier == null) {
                // untargetted binding
                Class javaType = beanType.getType();
                ImplementedBy implementedBy = javaType.getAnnotation(ImplementedBy.class);
                ProvidedBy providedBy = javaType.getAnnotation(ProvidedBy.class);
                if (implementedBy != null) {
                    if (!javaType.isAssignableFrom(implementedBy.value())) {
                        Message message = new Message(javaType, "@ImplementedBy annotation specifies a type that does not implement the declaring type");
                        throw new com.google.inject.ConfigurationException(
                            List.of(message)
                        );
                    }
                    to((Class) implementedBy.value());
                } else if (providedBy != null) {
                    toProvider((Class>) providedBy.value());
                } else {
                    if (!applicationContext.containsBean(javaType)) {
                        Message message = new Message(javaType, "Cannot create untargetted binding to type that is not itself declared a bean. " +
                            "Considering adding @Guice(classes=" + javaType.getSimpleName() + ".class) below your @Guice declaration.");
                        throw new com.google.inject.ConfigurationException(
                            List.of(message)
                        );
                    } else {
                        BeanDefinition beanDefinition = applicationContext.getBeanDefinition(javaType);
                        toProvider(() -> applicationContext.getBean(beanDefinition));
                    }
                }
            }
            Objects.requireNonNull(supplier, "Bean Provider cannot be null, call one of the binding methods like to(instance)");

            RuntimeBeanDefinition.Builder builder = RuntimeBeanDefinition
                .builder(beanType, () -> {
                    try {
                        return supplier.get();
                    } catch (NoSuchBeanException e) {
                        throw new CreationException(List.of(
                            new Message("Guice binding to bean [" + beanType.getTypeName() + "] cannot be resolved since no bean exists. " +
                                "Considering adding @Guice(classes=" + beanType.getSimpleName() + ".class) to the @Guice annotation definition."),
                            new Message(e.getMessage(), e)
                        ));
                    }
                });

            if (scope != null) {
                builder.scope(scope);
            }
            if (isSingleton) {
                builder.singleton(true);
            }
            builder.exposedTypes(beanType.getType());
            String beanName = name;
            Class beanQualifier = annotationType;
            bindQualifier(builder, beanName, beanQualifier);
            return builder
                .build();
        }

        @Override
        public LinkedBindingBuilder annotatedWith(Class annotationType) {
            validateBindingAnnotation(annotationType);
            this.annotationType = annotationType;
            return this;
        }

        @Override
        public LinkedBindingBuilder annotatedWith(Annotation annotation) {
            Objects.requireNonNull(annotation, "Annotation cannot be null");
            if (annotation instanceof Named named) {
                this.name = named.value();
                return this;
            } else {
                return annotatedWith(annotation.annotationType());
            }
        }
    }
}