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

io.micronaut.context.AbstractBeanResolutionContext Maven / Gradle / Ivy

/*
 * 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.Factory;
import io.micronaut.context.env.CachedEnvironment;
import io.micronaut.context.annotation.InjectScope;
import io.micronaut.context.env.ConfigurationPath;
import io.micronaut.context.exceptions.CircularDependencyException;
import io.micronaut.context.exceptions.DependencyInjectionException;
import io.micronaut.context.scope.CustomScope;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionContext;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.naming.Named;
import io.micronaut.core.type.Argument;
import io.micronaut.core.type.ArgumentCoercible;
import io.micronaut.core.type.TypeInformation;
import io.micronaut.core.type.TypeInformation.TypeFormat;
import io.micronaut.core.util.AnsiColour;
import io.micronaut.core.util.ObjectUtils;
import io.micronaut.inject.*;

import io.micronaut.inject.proxy.InterceptedBean;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Default implementation of the {@link BeanResolutionContext} interface.
 *
 * @author Graeme Rocher
 * @since 1.2.3
 */
@Internal
public abstract class AbstractBeanResolutionContext implements BeanResolutionContext {

    private static final String CONSTRUCTOR_METHOD_NAME = "";
    protected final DefaultBeanContext context;
    protected final BeanDefinition rootDefinition;
    protected final Path path;
    private final @NonNull BeanResolutionTraceMode traceMode;
    private final boolean traceEnabled;
    private Map attributes;
    private Qualifier qualifier;
    private List> dependentBeans;
    private BeanRegistration dependentFactory;

    private ConfigurationPath configurationPath;

    /**
     * @param context        The bean context
     * @param rootDefinition The bean root definition
     */
    @Internal
    protected AbstractBeanResolutionContext(DefaultBeanContext context, BeanDefinition rootDefinition) {
        this.context = context;
        this.rootDefinition = rootDefinition;
        this.path = new DefaultPath();
        this.traceMode = context.traceMode;
        this.traceEnabled = rootDefinition != null && isTraceEnabled(rootDefinition.getBeanType().getTypeName(), context.tracePatterns);
    }

    @Override
    public ConfigurationPath getConfigurationPath() {
        if (configurationPath != null) {
            return configurationPath;
        } else {
            this.configurationPath = ConfigurationPath.newPath();
            return configurationPath;
        }
    }

    @Override
    public ConfigurationPath setConfigurationPath(ConfigurationPath configurationPath) {
        ConfigurationPath old = this.configurationPath;
        this.configurationPath = configurationPath;
        return old;
    }

    @Override
    public void valueResolved(Argument argument, Qualifier qualifier, String property, Object value) {
        if (traceEnabled) {
            traceMode.getTracer().ifPresent(tracer ->
                tracer.traceValueResolved(
                    this,
                    (Argument) argument,
                    property,
                    value
                )
            );
        }
    }

    private boolean isTraceEnabled(@NonNull String typeName, @NonNull Set tracePatterns) {
        return traceMode != BeanResolutionTraceMode.NONE &&
            (tracePatterns.isEmpty() || tracePatterns.stream().anyMatch(typeName::matches));
    }

    @Override
    public Object resolvePropertyValue(Argument argument, String stringValue, String cliProperty, boolean isPlaceholder) {
        ApplicationContext applicationContext = (ApplicationContext) context;

        Argument argumentType = argument;
        Class wrapperType = null;
        Class type = argument.getType();
        if (type == Optional.class) {
            wrapperType = Optional.class;
            argumentType = argument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT);
        } else if (type == OptionalInt.class) {
            wrapperType = OptionalInt.class;
            argumentType = Argument.INT;
        } else if (type == OptionalLong.class) {
            wrapperType = OptionalLong.class;
            argumentType = Argument.LONG;
        } else if (type == OptionalDouble.class) {
            wrapperType = OptionalDouble.class;
            argumentType = Argument.DOUBLE;
        }

        ArgumentConversionContext conversionContext = wrapperType != null ? ConversionContext.of(argumentType) : ConversionContext.of(argument);

        Optional value;
        if (isPlaceholder) {
            value = applicationContext.resolvePlaceholders(stringValue).flatMap(v -> applicationContext.getConversionService().convert(v, conversionContext));
        } else {
            stringValue = substituteWildCards(stringValue);
            value = applicationContext.getProperty(stringValue, conversionContext);
            if (value.isEmpty() && cliProperty != null) {
                value = applicationContext.getProperty(cliProperty, conversionContext);
            }
        }

        if (traceEnabled) {
            String propertyName = stringValue;
            Object propertyValue = value.orElse(null);
            traceMode.getTracer().ifPresent(tracer ->
                tracer.traceValueResolved(
                    this,
                    (Argument) argument,
                    propertyName,
                    propertyValue
                ));
        }

        if (argument.isOptional()) {
            if (value.isEmpty()) {
                return value;
            } else {
                Object convertedOptional = value.get();
                if (convertedOptional instanceof Optional) {
                    return convertedOptional;
                } else {
                    return value;
                }
            }
        } else {
            if (wrapperType != null) {
                final Object v = value.orElse(null);
                if (OptionalInt.class == wrapperType) {
                    return v instanceof Integer i ? OptionalInt.of(i) : OptionalInt.empty();
                } else if (OptionalLong.class == wrapperType) {
                    return v instanceof Long l ? OptionalLong.of(l) : OptionalLong.empty();
                } else if (OptionalDouble.class == wrapperType) {
                    return v instanceof Double d ? OptionalDouble.of(d) : OptionalDouble.empty();
                }
            }
            if (value.isPresent()) {
                return value.get();
            } else {
                if (argument.isDeclaredNullable()) {
                    return null;
                }
                String finalStringValue = stringValue;
                return argument.getAnnotationMetadata().getValue(Bindable.class, "defaultValue", argument)
                    .orElseThrow(() -> DependencyInjectionException.missingProperty(this, conversionContext, finalStringValue));
            }
        }
    }

    private String substituteWildCards(String valString) {
        ConfigurationPath configurationPath = getConfigurationPath();
        if (configurationPath.isNotEmpty()) {
            return configurationPath.resolveValue(valString);
        }
        return valString;
    }

    @NonNull
    @Override
    public  T getBean(@NonNull Argument beanType, @Nullable Qualifier qualifier) {
        T bean = context.getBean(this, beanType, qualifier);
        if (traceEnabled) {
            traceMode.getTracer().ifPresent(tracer -> {
                tracer.traceBeanResolved(
                    this,
                    beanType,
                    qualifier,
                    bean
                );
                String disabledBeanMessage = context.resolveDisabledBeanMessage(
                    this,
                    beanType,
                    qualifier
                );
                if (disabledBeanMessage != null) {
                    tracer.traceBeanDisabled(
                        AbstractBeanResolutionContext.this,
                        beanType,
                        qualifier,
                        disabledBeanMessage
                    );
                }
            });
        }
        return bean;
    }

    @NonNull
    @Override
    public  Collection getBeansOfType(@NonNull Argument beanType, @Nullable Qualifier qualifier) {
        Collection beans = context.getBeansOfType(this, beanType, qualifier);
        if (traceEnabled) {
            traceBeanCollection(beanType, qualifier, beans);
        }
        return beans;
    }

    private  void traceBeanCollection(Argument beanType, Qualifier qualifier, Collection beans) {
        traceMode.getTracer().ifPresent(tracer -> {
            for (T bean : beans) {
                tracer.traceBeanResolved(this, beanType, qualifier, bean);
            }
            String disabledBeanMessage = context.resolveDisabledBeanMessage(
                this,
                beanType,
                qualifier
            );
            if (disabledBeanMessage != null) {
                tracer.traceBeanDisabled(
                    AbstractBeanResolutionContext.this,
                    beanType,
                    qualifier,
                    disabledBeanMessage
                );
            }
        });
    }

    @NonNull
    @Override
    public  Stream streamOfType(@NonNull Argument beanType, @Nullable Qualifier qualifier) {
        return context.streamOfType(this, beanType, qualifier);
    }

    @Override
    public  Map mapOfType(Argument beanType, Qualifier qualifier) {
        Map beanMap = context.mapOfType(this, beanType, qualifier);
        if (traceEnabled) {
            traceBeanCollection(beanType, qualifier, beanMap.values());
        }
        return beanMap;
    }

    @NonNull
    @Override
    public  Optional findBean(@NonNull Argument beanType, @Nullable Qualifier qualifier) {
        Optional resolved = context.findBean(this, beanType, qualifier);
        if (traceEnabled) {
            traceMode.getTracer().ifPresent(tracer -> {
                tracer.traceBeanResolved(
                    this,
                    beanType,
                    qualifier,
                    resolved.orElse(null)
                );
                String disabledBeanMessage = context.resolveDisabledBeanMessage(
                    this,
                    beanType,
                    qualifier
                );
                if (disabledBeanMessage != null) {
                    tracer.traceBeanDisabled(
                        AbstractBeanResolutionContext.this,
                        beanType,
                        qualifier,
                        disabledBeanMessage
                    );
                }
            });
        }
        return resolved;
    }

    @NonNull
    @Override
    public  Collection> getBeanRegistrations(@NonNull Argument beanType, @Nullable Qualifier qualifier) {
        Collection> registrations = context.getBeanRegistrations(this, beanType, qualifier);
        if (traceEnabled) {
            traceBeanCollection(
                beanType,
                qualifier,
                registrations.stream().map(BeanRegistration::getBean).collect(Collectors.toList())
            );
        }
        return registrations;
    }

    /**
     * Copy the state from a previous resolution context.
     *
     * @param context The previous context
     */
    public void copyStateFrom(@NonNull AbstractBeanResolutionContext context) {
        path.addAll(context.path);
        qualifier = context.qualifier;
        if (context.attributes != null) {
            getAttributesOrCreate().putAll(context.attributes);
        }
    }

    @Override
    public  void addDependentBean(BeanRegistration beanRegistration) {
        if (beanRegistration.getBeanDefinition() == rootDefinition) {
            // Don't add self
            return;
        }
        if (dependentBeans == null) {
            dependentBeans = new ArrayList<>(3);
        }
        dependentBeans.add(beanRegistration);
    }

    @Override
    public void destroyInjectScopedBeans() {
        final CustomScope injectScope = context.getCustomScopeRegistry()
            .findScope(InjectScope.class.getName())
            .orElse(null);
        if (injectScope instanceof LifeCycle cycle) {
            cycle.stop();
        }
    }

    @NonNull
    @Override
    public List> getAndResetDependentBeans() {
        if (dependentBeans == null) {
            return Collections.emptyList();
        }
        final List> registrations = Collections.unmodifiableList(dependentBeans);
        dependentBeans = null;
        return registrations;
    }

    @Override
    public void markDependentAsFactory() {
        if (dependentBeans != null) {
            if (dependentBeans.isEmpty()) {
                return;
            }
            if (dependentBeans.size() != 1) {
                throw new IllegalStateException("Expected only one bean dependent!");
            }
            dependentFactory = dependentBeans.remove(0);
        }
    }

    @Override
    public BeanRegistration getAndResetDependentFactoryBean() {
        BeanRegistration result = this.dependentFactory;
        this.dependentFactory = null;
        return result;
    }

    @Override
    public List> popDependentBeans() {
        List> result = this.dependentBeans;
        this.dependentBeans = null;
        return result;
    }

    @Override
    public void pushDependentBeans(List> dependentBeans) {
        if (this.dependentBeans != null && !this.dependentBeans.isEmpty()) {
            throw new IllegalStateException("Found existing dependent beans!");
        }
        this.dependentBeans = dependentBeans;
    }

    @Override
    public final BeanContext getContext() {
        return context;
    }

    @Override
    public final BeanDefinition getRootDefinition() {
        return rootDefinition;
    }

    @Override
    public final Path getPath() {
        return path;
    }

    @Override
    public final Object setAttribute(CharSequence key, Object value) {
        return getAttributesOrCreate().put(key, value);
    }

    /**
     * @param key The key
     * @return The attribute value
     */
    @Override
    public final Object getAttribute(CharSequence key) {
        if (attributes == null) {
            return null;
        }
        return attributes.get(key);
    }

    @Override
    public final Object removeAttribute(CharSequence key) {
        if (attributes != null && key != null) {
            return attributes.remove(key);
        }
        return null;
    }

    @Override
    public Map getAttributes() {
        return attributes;
    }

    @Override
    public void setAttributes(Map attributes) {
        this.attributes = attributes;
    }

    @Nullable
    @Override
    public Qualifier getCurrentQualifier() {
        return qualifier;
    }

    @Override
    public void setCurrentQualifier(@Nullable Qualifier qualifier) {
        this.qualifier = qualifier;
    }

    @Override
    public  Optional get(CharSequence name, ArgumentConversionContext conversionContext) {
        if (attributes == null) {
            return Optional.empty();
        }
        Object value = attributes.get(name);
        if (value != null && conversionContext.getArgument().getType().isInstance(value)) {
            return Optional.of((T) value);
        }
        return Optional.empty();
    }

    @Override
    public  Optional get(CharSequence name, Class requiredType) {
        if (attributes == null) {
            return Optional.empty();
        }
        Object value = attributes.get(name);
        if (requiredType.isInstance(value)) {
            return Optional.of((T) value);
        }
        return Optional.empty();
    }

    protected void onNewSegment(Segment segment) {
        //no-op
    }

    @NonNull
    private Map getAttributesOrCreate() {
        if (attributes == null) {
            attributes = new LinkedHashMap<>(2);
        }
        return attributes;
    }

    /**
     * Class that represents a default path.
     */
    class DefaultPath extends LinkedList> implements Path {

        public static final String RIGHT_ARROW = "\\---> ";
        public static final String RIGHT_ARROW_EMOJI = " ↪️  ";
        private static final String CIRCULAR_ERROR_MSG = "Circular dependency detected";

        DefaultPath() {
        }

        @Override
        public String toConsoleString(boolean ansiSupported) {
            Iterator> i = descendingIterator();
            String ls = CachedEnvironment.getProperty("line.separator");
            StringBuilder pathString = new StringBuilder().append(ls);

            String spaces = "";
            while (i.hasNext()) {
                pathString.append(i.next().toString());
                if (i.hasNext()) {
                    pathString
                        .append(ls)
                        .append(spaces)
                        .append(ansiSupported ? RIGHT_ARROW_EMOJI : RIGHT_ARROW);
                    spaces += "      ";
                }
            }
            return pathString.toString();
        }

        @Override
        public String toString() {
            return toConsoleString(false);
        }

        @Override
        public String toCircularString() {
            return toConsoleCircularString(false);
        }

        @SuppressWarnings("MagicNumber")
        @Override
        public String toConsoleCircularString(boolean ansiSupported) {
            Iterator> i = descendingIterator();
            StringBuilder pathString = new StringBuilder();
            String ls = CachedEnvironment.getProperty("line.separator");

            // Try finding an actual cycle, cycleI is index where the cycle starts
            int cycleIndex = lastIndexOf(iterator().next());
            if (cycleIndex > 0) {
                cycleIndex = size() - cycleIndex;
            } else {
                cycleIndex = 0;
            }

            String spaces = "";
            int index = 0;
            // The last element ends the cycle and is repeated in the path, so we skip it
            // and point to an already present element instead
            while (i.hasNext() && index < size() - 1) {
                String segmentString = i.next().toString();
                if (index == cycleIndex) {
                    pathString.append(ls).append(spaces).append("^").append("  ")
                        .append(ansiSupported ? RIGHT_ARROW_EMOJI : RIGHT_ARROW);
                    spaces = spaces + "|  ";
                } else if (index != 0) {
                    pathString
                        .append(ls)
                        .append(spaces)
                        .append(ansiSupported ? RIGHT_ARROW_EMOJI : RIGHT_ARROW);
                }
                pathString.append(segmentString);
                spaces = spaces + "      ";
                ++index;
            }

            String dashes = String.join("", Collections.nCopies(spaces.length() - spaces.indexOf("|") - 1, "-"));
            pathString
                .append(ls).append(spaces).append("|")
                .append(ls).append(spaces, 0, spaces.indexOf("|"))
                .append("+").append(dashes).append("+");

            return pathString.toString();
        }

        @Override
        public Optional> currentSegment() {
            return Optional.ofNullable(peek());
        }

        @Override
        public Path pushConstructorResolve(BeanDefinition declaringType, Argument argument) {
            ConstructorInjectionPoint constructor = declaringType.getConstructor();
            if (constructor instanceof MethodInjectionPoint methodInjectionPoint) {
                return pushConstructorResolve(declaringType, methodInjectionPoint.getName(), argument, constructor.getArguments());
            }
            return pushConstructorResolve(declaringType, CONSTRUCTOR_METHOD_NAME, argument, constructor.getArguments());
        }

        @Override
        public Path pushConstructorResolve(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments) {
            try {
                if (CONSTRUCTOR_METHOD_NAME.equals(methodName)) {
                    ConstructorSegment constructorSegment = new ConstructorArgumentSegment(declaringType, (Qualifier) getCurrentQualifier(), methodName, argument, arguments);
                    detectCircularDependency(declaringType, argument, constructorSegment);
                } else {
                    Segment previous = peek();
                    MethodSegment methodSegment = new MethodArgumentSegment(declaringType, (Qualifier) getCurrentQualifier(), methodName, argument, arguments, previous instanceof MethodSegment ms ? ms : null);
                    if (contains(methodSegment)) {
                        push(methodSegment);
                        throw new CircularDependencyException(AbstractBeanResolutionContext.this, argument, CIRCULAR_ERROR_MSG);
                    } else {
                        push(methodSegment);
                    }
                }
            } finally {
                traceResolution();
            }
            return this;
        }

        @Override
        public Path pushBeanCreate(BeanDefinition declaringType, Argument beanType) {
            if (traceEnabled) {
                traceMode.getTracer().ifPresent(tracer -> {
                    tracer.traceBeanCreation(
                        AbstractBeanResolutionContext.this,
                        declaringType,
                        beanType
                    );
                });
            }
            return pushConstructorResolve(declaringType, beanType);
        }

        private void traceResolution() {
            if (traceEnabled) {
                getPath().currentSegment().ifPresent(segment -> traceMode.getTracer().ifPresent(tracer -> {
                    tracer.traceInjectBean(
                        AbstractBeanResolutionContext.this,
                        segment
                    );
                }));
            }
        }

        @Override
        public Path pushMethodArgumentResolve(BeanDefinition declaringType, MethodInjectionPoint methodInjectionPoint, Argument argument) {
            try {
                Segment previous = peek();
                MethodSegment methodSegment = new MethodArgumentSegment(declaringType, (Qualifier) getCurrentQualifier(), methodInjectionPoint.getName(), argument,
                    methodInjectionPoint.getArguments(), previous instanceof MethodSegment ms ? ms : null);
                if (contains(methodSegment)) {
                    push(methodSegment);
                    throw new CircularDependencyException(AbstractBeanResolutionContext.this, methodInjectionPoint, argument, CIRCULAR_ERROR_MSG);
                } else {
                    push(methodSegment);
                }
            } finally {
                traceResolution();
            }

            return this;
        }

        @Override
        public Path pushMethodArgumentResolve(BeanDefinition declaringType, String methodName, Argument argument, Argument[] arguments) {
            try {
                Segment previous = peek();
                MethodSegment methodSegment = new MethodArgumentSegment(declaringType, (Qualifier) getCurrentQualifier(), methodName, argument, arguments, previous instanceof MethodSegment ms ? ms : null);
                if (contains(methodSegment)) {
                    push(methodSegment);
                    throw new CircularDependencyException(AbstractBeanResolutionContext.this, declaringType, methodName, argument, CIRCULAR_ERROR_MSG);
                } else {
                    push(methodSegment);
                }
            } finally {
                traceResolution();
            }

            return this;
        }

        @Override
        public Path pushEventListenerResolve(BeanDefinition declaringType, Argument eventType) {
            try {
                EventListenerSegment segment = new EventListenerSegment<>(
                    declaringType,
                    eventType
                );
                if (contains(segment)) {
                    push(segment);
                    throw new CircularDependencyException(
                        AbstractBeanResolutionContext.this,
                        eventType,
                        CIRCULAR_ERROR_MSG
                    );
                } else {
                    push(segment);
                }
            } finally {
                traceResolution();
            }

            return this;
        }

        @Override
        public Path pushFieldResolve(BeanDefinition declaringType, FieldInjectionPoint fieldInjectionPoint) {
            try {
                FieldSegment fieldSegment = new FieldSegment<>(declaringType, getCurrentQualifier(), fieldInjectionPoint.asArgument());
                if (contains(fieldSegment)) {
                    push(fieldSegment);
                    throw new CircularDependencyException(AbstractBeanResolutionContext.this, fieldInjectionPoint, CIRCULAR_ERROR_MSG);
                } else {
                    push(fieldSegment);
                }
            } finally {
                traceResolution();
            }
            return this;
        }

        @Override
        public Path pushFieldResolve(BeanDefinition declaringType, Argument fieldAsArgument) {
            try {
                FieldSegment fieldSegment = new FieldSegment<>(declaringType, getCurrentQualifier(), fieldAsArgument);
                if (contains(fieldSegment)) {
                    push(fieldSegment);
                    throw new CircularDependencyException(AbstractBeanResolutionContext.this, declaringType, fieldAsArgument.getName(), CIRCULAR_ERROR_MSG);
                } else {
                    push(fieldSegment);
                }
            } finally {
                traceResolution();
            }
            return this;
        }

        @Override
        public Path pushAnnotationResolve(BeanDefinition beanDefinition, Argument annotationMemberBeanAsArgument) {
            try {
                AnnotationSegment annotationSegment = new AnnotationSegment(beanDefinition, getCurrentQualifier(), annotationMemberBeanAsArgument);
                if (contains(annotationSegment)) {
                    push(annotationSegment);
                    throw new CircularDependencyException(AbstractBeanResolutionContext.this, beanDefinition, annotationMemberBeanAsArgument.getName(), CIRCULAR_ERROR_MSG);
                } else {
                    push(annotationSegment);
                }
            } finally {
                traceResolution();
            }
            return this;
        }

        private void detectCircularDependency(BeanDefinition declaringType, Argument argument, Segment constructorSegment) {
            if (contains(constructorSegment)) {
                Segment last = peek();
                if (last != null) {

                    BeanDefinition declaringBean = last.getDeclaringType();
                    // if the currently injected segment is a constructor argument and the type to be constructed is the
                    // same as the candidate, then filter out the candidate to avoid a circular injection problem
                    if (!declaringBean.equals(declaringType)) {
                        if (declaringType instanceof ProxyBeanDefinition proxyBeanDefinition) {
                            // take into account proxies
                            if (!proxyBeanDefinition.getTargetDefinitionType().equals(declaringBean.getClass())) {
                                push(constructorSegment);
                                throw new CircularDependencyException(AbstractBeanResolutionContext.this, argument, CIRCULAR_ERROR_MSG);
                            } else {
                                push(constructorSegment);
                            }
                        } else if (declaringBean instanceof ProxyBeanDefinition proxyBeanDefinition) {
                            // take into account proxies
                            if (!proxyBeanDefinition.getTargetDefinitionType().equals(declaringType.getClass())) {
                                push(constructorSegment);
                                throw new CircularDependencyException(AbstractBeanResolutionContext.this, argument, CIRCULAR_ERROR_MSG);
                            } else {
                                push(constructorSegment);
                            }
                        } else {
                            push(constructorSegment);
                            throw new CircularDependencyException(AbstractBeanResolutionContext.this, argument, CIRCULAR_ERROR_MSG);
                        }
                    } else {
                        push(constructorSegment);
                    }
                } else {
                    push(constructorSegment);
                    throw new CircularDependencyException(AbstractBeanResolutionContext.this, argument, CIRCULAR_ERROR_MSG);
                }
            } else {
                push(constructorSegment);
            }
        }

        @Override
        public void push(Segment segment) {
            super.push(segment);
            AbstractBeanResolutionContext.this.onNewSegment(segment);
        }

        @Override
        public void close() {
            if (traceEnabled) {
                traceMode.getTracer().ifPresent(tracer -> {
                    if (isEmpty()) {
                        tracer.traceBeanCreated(
                            AbstractBeanResolutionContext.this,
                            rootDefinition
                        );
                    } else {
                        Segment segment = peek();
                        if (segment != null) {
                            tracer.traceInjectComplete(
                                AbstractBeanResolutionContext.this,
                                segment
                            );
                        }
                    }
                });
            }
            Path.super.close();
        }
    }

    /**
     * A segment that represents a method argument.
     */
    public static final class ConstructorArgumentSegment extends ConstructorSegment implements ArgumentInjectionPoint {
        public ConstructorArgumentSegment(BeanDefinition declaringType, Qualifier qualifier, String methodName, Argument argument, Argument[] arguments) {
            super(declaringType, qualifier, methodName, argument, arguments);
        }

        @Override
        public BeanDefinition getDeclaringBean() {
            return getDeclaringType();
        }

        @Override
        public Qualifier getDeclaringBeanQualifier() {
            return getDeclaringTypeQualifier();
        }

    }

    /**
     * A segment that represents a constructor.
     */
    public static class ConstructorSegment extends AbstractSegment implements ArgumentInjectionPoint {

        private static final String ANN_ADAPTER = "io.micronaut.aop.Adapter";
        private final String methodName;
        private final Argument[] arguments;

        /**
         * @param declaringBeanDefinition The declaring class
         * @param qualifier               The qualifier
         * @param methodName              The methodName
         * @param argument                The argument
         * @param arguments               The arguments
         */
        ConstructorSegment(BeanDefinition declaringBeanDefinition, Qualifier qualifier, String methodName, Argument argument, Argument[] arguments) {
            super(declaringBeanDefinition, qualifier, declaringBeanDefinition.getBeanType().getName(), argument);
            this.methodName = methodName;
            this.arguments = arguments;
        }

        @Override
        public String toString() {
            return toConsoleString(false);
        }

        @NonNull
        public String toConsoleString(boolean ansiSupported) {
            StringBuilder baseString;
            BeanDefinition declaringType = getDeclaringType();
            TypeInformation typeInformation = declaringType.getTypeInformation();
            if (declaringType.hasDeclaredStereotype(ANN_ADAPTER)) {
                ExecutableMethod method = declaringType.getExecutableMethods().iterator().next();
                // Not great, but to produce accurate debug output we have to reach into AOP internals
                Class beanType = method.classValue(ANN_ADAPTER, "adaptedBean").orElse(declaringType.getBeanType());
                String beanMethod = method.stringValue(ANN_ADAPTER, "adaptedMethod").orElse("unknown");
                baseString = new StringBuilder(TypeFormat.getBeanTypeString(
                    ansiSupported ? TypeFormat.ANSI_SHORTENED : TypeFormat.SHORTENED,
                    beanType,
                    declaringType.asArgument().getTypeVariables(),
                    declaringType.getAnnotationMetadata()
                )).append(MEMBER_SEPARATOR);
                baseString.append(beanMethod);
            } else if (CONSTRUCTOR_METHOD_NAME.equals(methodName)) {
                baseString = new StringBuilder(
                    ansiSupported ? AnsiColour.magentaBold("new ") : "new "
                );
                baseString.append(typeInformation.getBeanTypeString(
                    ansiSupported ? TypeFormat.ANSI_SHORTENED : TypeFormat.SHORTENED
                ));
            } else {
                baseString = new StringBuilder(typeInformation.getBeanTypeString(
                    ansiSupported ? TypeFormat.ANSI_SHORTENED : TypeFormat.SHORTENED
                )).append(MEMBER_SEPARATOR);
                baseString.append(methodName);
            }
            outputArguments(baseString, arguments, ansiSupported);
            return baseString.toString();
        }

        @Override
        public InjectionPoint getInjectionPoint() {
            return this;
        }

        @NonNull
        @Override
        public CallableInjectionPoint getOuterInjectionPoint() {
            return getDeclaringType().getConstructor();
        }

        @Override
        public BeanDefinition getDeclaringBean() {
            return ConstructorSegment.this.getDeclaringType();
        }

        @Override
        public Qualifier getDeclaringBeanQualifier() {
            return ConstructorSegment.this.getDeclaringTypeQualifier();
        }

        @Override
        public AnnotationMetadata getAnnotationMetadata() {
            return getArgument().getAnnotationMetadata();
        }

    }

    /**
     * A segment that represents a method argument.
     */
    public static final class MethodArgumentSegment extends MethodSegment implements ArgumentInjectionPoint {
        private final MethodSegment outer;

        public MethodArgumentSegment(BeanDefinition declaringType,
                                     Qualifier qualifier,
                                     String methodName,
                                     Argument argument,
                                     Argument[] arguments,
                                     MethodSegment outer) {
            super(declaringType, qualifier, methodName, argument, arguments);
            this.outer = outer;
        }

        @Override
        public CallableInjectionPoint getOuterInjectionPoint() {
            if (outer == null) {
                throw new IllegalStateException("Outer argument inaccessible");
            }
            return outer;
        }

        @Override
        public String toString() {
            return toConsoleString(false);
        }

        @Override
        public String toConsoleString(boolean ansiSupported) {
            BeanDefinition declaringBean = getDeclaringBean();
            if (declaringBean.hasAnnotation(Factory.class)) {
                String beanDescription = declaringBean.getBeanDescription(
                    ansiSupported ? TypeFormat.ANSI_SHORTENED : TypeFormat.SHORTENED,
                    false
                );

                var baseString = new StringBuilder(beanDescription);
                String methodName = getName();
                if (!CONSTRUCTOR_METHOD_NAME.equals(methodName)) {
                    String memberSeparator = ansiSupported ? AnsiColour.CYAN_BOLD + MEMBER_SEPARATOR + AnsiColour.RESET : MEMBER_SEPARATOR;
                    baseString.append(memberSeparator);
                    baseString.append(methodName);
                }

                outputArguments(baseString, getArguments(), ansiSupported);
                return baseString.toString();
            } else {
                return super.toConsoleString(ansiSupported);
            }
        }
    }

    /**
     * Represents a segment that is an event listener.
     *
     * @param  The bean type
     * @param  The event type
     */
    public static class EventListenerSegment extends AbstractSegment implements CallableInjectionPoint {
        /**
         * @param declaringClass The declaring class
         * @param eventType      The argument
         */
        EventListenerSegment(
            BeanDefinition declaringClass,
            Argument eventType) {
            super(declaringClass, null, eventType.getName(), eventType);
        }

        @Override
        public String toConsoleString(boolean ansiSupported) {
            if (ansiSupported) {
                String event = getArgument().getTypeString(TypeFormat.ANSI_SIMPLE);
                return event + " ➡️  " +
                    getDeclaringBean().getBeanDescription(TypeFormat.ANSI_SHORTENED);
            } else {
                String event = getArgument().getTypeString(TypeFormat.SIMPLE);
                return event + " -> " +
                    getDeclaringBean().getBeanDescription(TypeFormat.SHORTENED);
            }
        }

        @Override
        public InjectionPoint getInjectionPoint() {
            return this;
        }

        @Override
        public Argument[] getArguments() {
            return new Argument[]{getArgument()};
        }

        @Override
        public BeanDefinition getDeclaringBean() {
            return getDeclaringType();
        }
    }

    /**
     * A segment that represents a method.
     */
    public static class MethodSegment extends AbstractSegment implements CallableInjectionPoint {

        private final Argument[] arguments;

        /**
         * @param declaringType The declaring type
         * @param qualifier     The qualifier
         * @param methodName    The method name
         * @param argument      The argument
         * @param arguments     The arguments
         */
        MethodSegment(BeanDefinition declaringType, Qualifier qualifier, String methodName, Argument argument, Argument[] arguments) {
            super(declaringType, qualifier, methodName, argument);
            this.arguments = arguments;
        }

        @Override
        public String toString() {
            return toConsoleString(false);
        }

        @Override
        public String toConsoleString(boolean ansiSupported) {
            StringBuilder baseString = new StringBuilder(
                getDeclaringType().getTypeInformation().getBeanTypeString(
                    ansiSupported ? TypeFormat.ANSI_SHORTENED : TypeFormat.SHORTENED
                )
            );
            String memberSeparator = ansiSupported ? AnsiColour.CYAN_BOLD + MEMBER_SEPARATOR + AnsiColour.RESET : MEMBER_SEPARATOR;
            baseString.append(memberSeparator);
            baseString.append(getName());
            outputArguments(baseString, arguments, ansiSupported);
            return baseString.toString();
        }

        @Override
        public InjectionPoint getInjectionPoint() {
            return this;
        }

        @Override
        public BeanDefinition getDeclaringBean() {
            return getDeclaringType();
        }

        @Override
        public Argument[] getArguments() {
            return arguments;
        }

        @Override
        public AnnotationMetadata getAnnotationMetadata() {
            return getArgument().getAnnotationMetadata();
        }

        @Override
        public Qualifier getDeclaringBeanQualifier() {
            return getDeclaringTypeQualifier();
        }
    }

    /**
     * A segment that represents a field.
     */
    public static final class FieldSegment extends AbstractSegment implements InjectionPoint, ArgumentCoercible, ArgumentInjectionPoint {

        /**
         * @param declaringClass The declaring class
         * @param qualifier      The qualifier
         * @param argument       The argument
         */
        FieldSegment(BeanDefinition declaringClass, Qualifier qualifier, Argument argument) {
            super(declaringClass, qualifier, argument.getName(), argument);
        }

        @Override
        public String toString() {
            return toConsoleString(false);
        }

        @Override
        public String toConsoleString(boolean ansiSupported) {
            String beanDescription = getDeclaringType().getBeanDescription(
                ansiSupported ? TypeFormat.ANSI_SHORTENED : TypeFormat.SHORTENED,
                false
            );
            StringBuilder baseString = new StringBuilder(beanDescription);
            String memberSeparator = ansiSupported ? AnsiColour.CYAN_BOLD + MEMBER_SEPARATOR + AnsiColour.RESET : MEMBER_SEPARATOR;
            baseString.append(memberSeparator);
            baseString.append(getName());

            return baseString.toString();
        }

        @Override
        public InjectionPoint getInjectionPoint() {
            return this;
        }

        @Override
        public BeanDefinition getDeclaringBean() {
            return getDeclaringType();
        }

        @Override
        public CallableInjectionPoint getOuterInjectionPoint() {
            throw new UnsupportedOperationException("Outer injection point not retrievable from here");
        }

        @Override
        public Argument asArgument() {
            return getArgument();
        }

        @Override
        public AnnotationMetadata getAnnotationMetadata() {
            return getArgument().getAnnotationMetadata();
        }

        @Override
        public Qualifier getDeclaringBeanQualifier() {
            return getDeclaringTypeQualifier();
        }
    }

    /**
     * A segment that represents annotation.
     *
     * @since 3.3.0
     */
    public static final class AnnotationSegment extends AbstractSegment implements InjectionPoint {

        /**
         * @param beanDefinition The bean definition
         * @param qualifier      The qualifier
         * @param argument       The argument
         */
        AnnotationSegment(BeanDefinition beanDefinition, Qualifier qualifier, Argument argument) {
            super(beanDefinition, qualifier, argument.getName(), argument);
        }

        @Override
        public String toString() {
            return getName();
        }

        @Override
        public InjectionPoint getInjectionPoint() {
            return this;
        }

        @Override
        public BeanDefinition getDeclaringBean() {
            return getDeclaringType();
        }

        @Override
        public AnnotationMetadata getAnnotationMetadata() {
            return getArgument().getAnnotationMetadata();
        }

        @Override
        public Qualifier getDeclaringBeanQualifier() {
            return getDeclaringTypeQualifier();
        }
    }

    /**
     * Abstract class for a Segment.
     */
    protected abstract static class AbstractSegment implements Segment, Named {

        /**
         * The separator between a type and its member when printing to user.
         */
        protected static final String MEMBER_SEPARATOR = "#";

        private final BeanDefinition declaringComponent;
        @Nullable
        private final Qualifier qualifier;
        private final String name;
        private final Argument argument;

        /**
         * @param declaringClass The declaring class
         * @param qualifier      The qualifier
         * @param name           The name
         * @param argument       The argument
         */
        AbstractSegment(BeanDefinition declaringClass, Qualifier qualifier, String name, Argument argument) {
            this.declaringComponent = declaringClass;
            this.qualifier = qualifier;
            this.name = name;
            this.argument = argument;
        }

        /**
         * A common method for retrieving a name for type. The default behavior is to use the shortened type name.
         *
         * @param type The type
         * @return The name to be shown to user
         */
        protected String getTypeName(Class type) {
            if (InterceptedBean.class.isAssignableFrom(type)) {
                Class[] interfaces = type.getInterfaces();
                Set interfaceNames = Arrays.stream(interfaces)
                    .map(Class::getName)
                    .collect(Collectors.toSet());
                if (type.isInterface() && interfaceNames.contains("io.micronaut.aop.Introduced")) {
                    return NameUtils.getShortenedName(
                        interfaces[0].getTypeName()
                    );
                } else {
                    return NameUtils.getShortenedName(
                        type.getSuperclass().getTypeName()
                    );
                }
            } else {
                return NameUtils.getShortenedName(type.getTypeName());
            }
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public BeanDefinition getDeclaringType() {
            return declaringComponent;
        }

        @Override
        public Qualifier getDeclaringTypeQualifier() {
            return qualifier == null ? declaringComponent.getDeclaredQualifier() : qualifier;
        }

        @Override
        public Argument getArgument() {
            return argument;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            AbstractSegment that = (AbstractSegment) o;

            return declaringComponent.equals(that.declaringComponent) && name.equals(that.name) && argument.equals(that.argument);
        }

        @Override
        public int hashCode() {
            return ObjectUtils.hash(declaringComponent, name, argument);
        }

        /**
         * @param baseString    The base string
         * @param arguments     The arguments
         * @param ansiSupported Whether ANSI colour is supported
         */
        void outputArguments(StringBuilder baseString, Argument[] arguments, boolean ansiSupported) {
            baseString.append(ansiSupported ? AnsiColour.brightCyan("(") : "(");
            for (int i = 0; i < arguments.length; i++) {
                Argument argument = arguments[i];
                boolean isInjectedArgument = argument.equals(getArgument());
                if (isInjectedArgument) {
                    if (ansiSupported) {
                        baseString.append(AnsiColour.BLUE_UNDERLINED);
                    }
                    baseString.append('[');
                }
                String beanTypeString = argument.getBeanTypeString(
                    ansiSupported && !isInjectedArgument ? TypeFormat.ANSI_SIMPLE : TypeFormat.SIMPLE
                );
                baseString.append(beanTypeString)
                    .append(' ')
                    .append(ansiSupported && !isInjectedArgument ? AnsiColour.brightBlue(argument.getName()) : argument.getName());
                if (isInjectedArgument) {
                    baseString.append(']');
                    if (ansiSupported) {
                        baseString.append(AnsiColour.RESET);
                    }
                }

                if (i != arguments.length - 1) {
                    Argument next = arguments[i + 1];
                    if (getDeclaringType().getBeanType().isSynthetic() &&
                        next.getName().startsWith("$")) {
                        // skip synthetic arguments
                        break;
                    }
                    baseString.append(", ");
                }
            }
            baseString.append(ansiSupported ? AnsiColour.brightCyan(")") : ")");

        }
    }
}