BindingSelectionTest.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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
 *
 *   http://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 org.apache.maven.di.impl;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.api.di.Inject;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Priority;
import org.apache.maven.api.di.Provides;
import org.apache.maven.di.Injector;
import org.apache.maven.di.Key;
import org.junit.jupiter.api.Test;

import static java.util.stream.Collectors.toSet;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

class BindingSelectionTest {
    private static final String NAMED_QUALIFIER = "other";
    private static final String ALTERNATE_QUALIFIER = "another";
    private static final int PREFERRED_PRIORITY = 10;

    @Test
    void defaultProviderWinsOverNamedProvider() {
        InjectorImpl injector = namedFirst(Injector.create().bindImplicit(DefaultAndNamed.class));

        assertInstanceOf(DefaultService.class, injector.getInstance(Service.class));
        assertInstanceOf(NamedService.class, injector.getInstance(Key.of(Service.class, NAMED_QUALIFIER)));
    }

    @Test
    void emptyNamedProviderWinsOverNamedProvider() {
        InjectorImpl injector = namedFirst(Injector.create().bindImplicit(EmptyAndNamed.class));

        assertInstanceOf(DefaultService.class, injector.getInstance(Service.class));
    }

    @Test
    void defaultClassWinsOverNamedClass() {
        InjectorImpl injector =
                namedFirst(Injector.create().bindImplicit(DefaultService.class).bindImplicit(NamedService.class));

        assertInstanceOf(DefaultService.class, injector.getInstance(Service.class));
    }

    @Test
    void anInjectFactoryPreservesTheClassQualifier() {
        InjectorImpl injector = namedFirst(
                Injector.create().bindImplicit(DefaultService.class).bindImplicit(NamedFactoryService.class));

        assertInstanceOf(DefaultService.class, injector.getInstance(Service.class));
        Map<String, Service> services = injector.getInstance(new Key<Map<String, Service>>() {});
        assertInstanceOf(NamedFactoryService.class, services.get(NAMED_QUALIFIER));
    }

    @Test
    void repeatedImplicitRegistrationDoesNotCreateAnotherProvider() {
        Injector injector =
                Injector.create().bindImplicit(DefaultAndNamed.class).bindImplicit(DefaultAndNamed.class);

        assertInstanceOf(DefaultService.class, injector.getInstance(Service.class));
        assertEquals(2, injector.getInstance(new Key<List<Service>>() {}).size());
    }

    @Test
    void inheritedStaticProvidersAreRegisteredOnce() {
        Injector injector = Injector.create().bindImplicit(ParentProvider.class).bindImplicit(ChildProvider.class);

        assertEquals("parent", injector.getInstance(String.class));
        assertEquals(List.of("parent"), injector.getInstance(new Key<List<String>>() {}));
    }

    @Test
    void rebindingTheSameInstanceDoesNotDuplicateItsProviders() {
        InstanceProvider provider = new InstanceProvider();
        Injector injector = Injector.create()
                .bindInstance(InstanceProvider.class, provider)
                .bindInstance(InstanceProvider.class, provider);

        assertEquals("instance", injector.getInstance(String.class));
        assertEquals(provider, injector.getInstance(InstanceProvider.class));
        assertEquals(List.of("instance"), injector.getInstance(new Key<List<String>>() {}));
    }

    @Test
    void rebindingTheSameSupplierDoesNotDuplicateItsProviders() {
        Supplier<InstanceProvider> supplier = InstanceProvider::new;
        Injector injector = Injector.create()
                .bindSupplier(InstanceProvider.class, supplier)
                .bindSupplier(InstanceProvider.class, supplier);

        assertEquals("instance", injector.getInstance(String.class));
        assertEquals(List.of("instance"), injector.getInstance(new Key<List<String>>() {}));
    }

    @Test
    void differentProgrammaticFactoriesRemainAmbiguous() {
        InjectorImpl injector = new InjectorImpl();
        Key<Service> key = Key.of(Service.class);
        injector.bind(key, Binding.to(key, args -> new DefaultService(), new Dependency<?>[0]));
        injector.bind(key, Binding.to(key, args -> new EmptyNamedService(), new Dependency<?>[0]));

        assertThrows(DIException.class, () -> injector.getInstance(Service.class));
    }

    @Test
    void registeringTheSameProgrammaticBindingTwiceDoesNotDuplicateIt() {
        InjectorImpl injector = new InjectorImpl();
        Key<Service> key = Key.of(Service.class);
        Binding<Service> binding = Binding.to(key, args -> new DefaultService(), new Dependency<?>[0]);
        injector.bind(key, binding);
        injector.bind(key, binding);

        assertInstanceOf(DefaultService.class, injector.getInstance(Service.class));
        assertEquals(1, injector.getInstance(new Key<List<Service>>() {}).size());
    }

    @Test
    void differentCustomInitializersRemainDistinct() throws NoSuchMethodException {
        InjectorImpl injector = new InjectorImpl();
        Key<InitializedService> key = Key.of(InitializedService.class);
        Binding<InitializedService> base =
                ReflectionUtils.bindingFromConstructor(key, InitializedService.class.getDeclaredConstructor());
        injector.bind(key, base.initializeWith(tagInitializer("first")));
        injector.bind(key, base.initializeWith(tagInitializer("second")));

        List<InitializedService> services = injector.getInstance(new Key<List<InitializedService>>() {});
        assertEquals(
                Set.of("first", "second"), services.stream().map(s -> s.tag).collect(toSet()));
        assertThrows(DIException.class, () -> injector.getInstance(InitializedService.class));
    }

    @Test
    void aCustomInitializerIsDistinctFromItsRawBinding() throws NoSuchMethodException {
        InjectorImpl injector = new InjectorImpl();
        Key<InitializedService> key = Key.of(InitializedService.class);
        Binding<InitializedService> base =
                ReflectionUtils.bindingFromConstructor(key, InitializedService.class.getDeclaredConstructor());
        injector.bind(key, base);
        injector.bind(key, base.initializeWith(tagInitializer("custom")));

        List<InitializedService> services = injector.getInstance(new Key<List<InitializedService>>() {});
        assertEquals(Set.of("raw", "custom"), services.stream().map(s -> s.tag).collect(toSet()));
        assertThrows(DIException.class, () -> injector.getInstance(InitializedService.class));
    }

    @Test
    void generatedInjectionIsDistinctFromTheRawConstructor() throws NoSuchMethodException {
        InjectorImpl injector = new InjectorImpl();
        injector.bindInstance(String.class, "injected");
        Key<InitializedService> key = Key.of(InitializedService.class);
        Binding<InitializedService> rawBinding =
                ReflectionUtils.bindingFromConstructor(key, InitializedService.class.getDeclaredConstructor());
        Binding<InitializedService> initializedBinding = ReflectionUtils.generateImplicitBinding(key);
        injector.bind(key, rawBinding);
        injector.bind(key, initializedBinding);

        List<InitializedService> services = injector.getInstance(new Key<List<InitializedService>>() {});
        assertEquals(
                Set.of("raw", "injected"), services.stream().map(s -> s.tag).collect(toSet()));
        DIException exception = assertThrows(DIException.class, () -> injector.getInstance(InitializedService.class));
        assertNotEquals(rawBinding.toString(), initializedBinding.toString());
        assertTrue(exception.getMessage().contains(initializedBinding.toString()), exception.getMessage());
    }

    private static BindingInitializer<InitializedService> tagInitializer(String tag) {
        return new BindingInitializer<>(Set.of()) {
            @Override
            public Consumer<InitializedService> compile(Function<Dependency<?>, Supplier<?>> compiler) {
                return service -> service.tag = tag;
            }
        };
    }

    @Test
    void ambiguityDiagnosticsDoNotInvokeInstanceToString() {
        Injector injector = Injector.create()
                .bindInstance(Service.class, new ExplosiveService())
                .bindInstance(Service.class, new DefaultService());

        DIException exception = assertThrows(DIException.class, () -> injector.getInstance(Service.class));
        assertTrue(exception.getMessage().contains("ExplosiveService"));
        assertTrue(exception.getMessage().contains("DefaultService"));
    }

    @Test
    void ambiguityDiagnosticsDoNotInvokeSupplierToString() {
        Injector injector = Injector.create()
                .bindSupplier(Service.class, new ExplosiveSupplier())
                .bindSupplier(Service.class, DefaultService::new);

        DIException exception = assertThrows(DIException.class, () -> injector.getInstance(Service.class));
        assertTrue(exception.getMessage().contains("ExplosiveSupplier"));
    }

    @Test
    void namedInstanceDoesNotBecomeDefault() {
        InjectorImpl injector = namedFirst(Injector.create()
                .bindInstance(Service.class, new DefaultService())
                .bindInstance(NamedService.class, new NamedService()));

        assertInstanceOf(DefaultService.class, injector.getInstance(Service.class));
        Map<String, Service> services = injector.getInstance(new Key<Map<String, Service>>() {});
        assertInstanceOf(NamedService.class, services.get(NAMED_QUALIFIER));
    }

    @Test
    void namedSupplierDoesNotBecomeDefault() {
        InjectorImpl injector = namedFirst(Injector.create()
                .bindSupplier(Service.class, DefaultService::new)
                .bindSupplier(NamedService.class, NamedService::new));

        assertInstanceOf(DefaultService.class, injector.getInstance(Service.class));
        Map<String, Service> services = injector.getInstance(new Key<Map<String, Service>>() {});
        assertInstanceOf(NamedService.class, services.get(NAMED_QUALIFIER));
    }

    @Test
    void higherPriorityNamedProviderWins() {
        Injector injector = Injector.create().bindImplicit(HigherPriorityNamed.class);

        assertInstanceOf(NamedService.class, injector.getInstance(Service.class));
    }

    @Test
    void aSingleNamedBindingCanSatisfyAnUnqualifiedLookup() {
        Injector injector = Injector.create().bindImplicit(NamedService.class);

        assertInstanceOf(NamedService.class, injector.getInstance(Service.class));
    }

    @Test
    void equalDefaultProvidersAreAmbiguous() {
        Injector injector = Injector.create().bindImplicit(EqualDefaults.class);

        DIException exception = assertThrows(DIException.class, () -> injector.getInstance(Service.class));
        String message = exception.getMessage();
        assertTrue(message.contains("Ambiguous bindings"), message);
        assertTrue(message.contains("Service"), message);
        assertTrue(message.contains("first()"), message);
        assertTrue(message.contains("second()"), message);
        assertTrue(message.contains("priority=0"), message);
    }

    @Test
    void anUnqualifiedClassAndAnEmptyNamedClassAreAmbiguous() {
        Injector injector = Injector.create().bindImplicit(DefaultService.class).bindImplicit(EmptyNamedService.class);

        DIException exception = assertThrows(DIException.class, () -> injector.getInstance(Service.class));
        assertTrue(exception.getMessage().contains("DefaultService"));
        assertTrue(exception.getMessage().contains("EmptyNamedService"));
    }

    @Test
    void equalNamedBindingsAreAmbiguousButRemainAvailableByNameAndInCollections() {
        Injector injector = Injector.create().bindImplicit(EqualNames.class);

        assertThrows(DIException.class, () -> injector.getInstance(Service.class));
        assertInstanceOf(NamedService.class, injector.getInstance(Key.of(Service.class, NAMED_QUALIFIER)));
        assertInstanceOf(DefaultService.class, injector.getInstance(Key.of(Service.class, ALTERNATE_QUALIFIER)));

        List<Service> list = injector.getInstance(new Key<List<Service>>() {});
        assertEquals(2, list.size());
        Map<String, Service> map = injector.getInstance(new Key<Map<String, Service>>() {});
        assertEquals(Set.of(NAMED_QUALIFIER, ALTERNATE_QUALIFIER), map.keySet());
        assertInstanceOf(NamedService.class, map.get(NAMED_QUALIFIER));
        assertInstanceOf(DefaultService.class, map.get(ALTERNATE_QUALIFIER));
    }

    @Test
    void equalBindingsForAnExplicitNameAreAmbiguous() {
        Injector injector = Injector.create().bindImplicit(EqualExplicitNames.class);

        DIException exception =
                assertThrows(DIException.class, () -> injector.getInstance(Key.of(Service.class, NAMED_QUALIFIER)));
        assertTrue(exception.getMessage().contains("@Named(\"other\")"));
        assertTrue(exception.getMessage().contains("first()"));
        assertTrue(exception.getMessage().contains("second()"));
    }

    @Test
    void equalDefaultBindingsRemainAvailableInAList() {
        Injector injector = Injector.create().bindImplicit(EqualDefaults.class);

        List<Service> services = injector.getInstance(new Key<List<Service>>() {});
        assertEquals(2, services.size());
        assertTrue(services.stream().anyMatch(DefaultService.class::isInstance));
        assertTrue(services.stream().anyMatch(EmptyNamedService.class::isInstance));
    }

    @Test
    void lowerPriorityTiesDoNotMakeThePreferredBindingAmbiguous() {
        Injector injector = Injector.create().bindImplicit(UniqueHighestPriority.class);

        assertInstanceOf(NamedService.class, injector.getInstance(Service.class));
    }

    @Test
    void nullableDependenciesDoNotHideAmbiguity() {
        Injector injector = Injector.create().bindImplicit(EqualDefaults.class).bindImplicit(OptionalConsumer.class);

        assertThrows(DIException.class, () -> injector.getInstance(OptionalConsumer.class));
    }

    private static InjectorImpl namedFirst(Injector injector) {
        InjectorImpl impl = (InjectorImpl) injector;
        Map<Key<?>, Set<Binding<?>>> bindings = impl.getBindings();
        // Make the regression independent of HashSet iteration order.
        Set<Binding<?>> ordered = new LinkedHashSet<>(bindings.get(Key.of(Service.class, NAMED_QUALIFIER)));
        ordered.addAll(bindings.get(Key.of(Service.class)));
        bindings.put(Key.of(Service.class), ordered);
        return impl;
    }

    interface Service {}

    static class DefaultService implements Service {}

    @Named
    static class EmptyNamedService implements Service {}

    @Named(NAMED_QUALIFIER)
    static class NamedService implements Service {}

    static class InitializedService {
        @Inject
        String tag = "raw";
    }

    @Named(NAMED_QUALIFIER)
    static class NamedFactoryService implements Service {
        @Inject
        static NamedFactoryService create() {
            return new NamedFactoryService();
        }
    }

    static class ParentProvider {
        @Provides
        static String value() {
            return "parent";
        }
    }

    static class ChildProvider extends ParentProvider {}

    static class InstanceProvider {
        @Provides
        String value() {
            return "instance";
        }
    }

    static class ExplosiveService implements Service {
        @Override
        public String toString() {
            throw new IllegalStateException("instance toString must not be called");
        }
    }

    static class ExplosiveSupplier implements Supplier<Service> {
        @Override
        public Service get() {
            return new DefaultService();
        }

        @Override
        public String toString() {
            throw new IllegalStateException("supplier toString must not be called");
        }
    }

    static class DefaultAndNamed {
        @Provides
        static Service defaultService() {
            return new DefaultService();
        }

        @Provides
        @Named(NAMED_QUALIFIER)
        static Service namedService() {
            return new NamedService();
        }
    }

    static class EmptyAndNamed {
        @Provides
        @Named
        static Service defaultService() {
            return new DefaultService();
        }

        @Provides
        @Named(NAMED_QUALIFIER)
        static Service namedService() {
            return new NamedService();
        }
    }

    static class HigherPriorityNamed {
        @Provides
        static Service defaultService() {
            return new DefaultService();
        }

        @Provides
        @Named(NAMED_QUALIFIER)
        @Priority(PREFERRED_PRIORITY)
        static Service namedService() {
            return new NamedService();
        }
    }

    static class EqualDefaults {
        @Provides
        static Service first() {
            return new DefaultService();
        }

        @Provides
        static Service second() {
            return new EmptyNamedService();
        }
    }

    static class EqualNames {
        @Provides
        @Named(NAMED_QUALIFIER)
        static Service first() {
            return new NamedService();
        }

        @Provides
        @Named(ALTERNATE_QUALIFIER)
        static Service second() {
            return new DefaultService();
        }
    }

    static class EqualExplicitNames {
        @Provides
        @Named(NAMED_QUALIFIER)
        static Service first() {
            return new NamedService();
        }

        @Provides
        @Named(NAMED_QUALIFIER)
        static Service second() {
            return new DefaultService();
        }
    }

    static class UniqueHighestPriority {
        @Provides
        static Service first() {
            return new DefaultService();
        }

        @Provides
        static Service second() {
            return new EmptyNamedService();
        }

        @Provides
        @Priority(PREFERRED_PRIORITY)
        static Service preferred() {
            return new NamedService();
        }
    }

    static class OptionalConsumer {
        @Inject
        @Nullable
        Service service;
    }
}