diff --git a/core/common/boot/src/main/java/org/eclipse/edc/boot/system/DependencyGraph.java b/core/common/boot/src/main/java/org/eclipse/edc/boot/system/DependencyGraph.java index eaaac2872a..5a7f98a979 100644 --- a/core/common/boot/src/main/java/org/eclipse/edc/boot/system/DependencyGraph.java +++ b/core/common/boot/src/main/java/org/eclipse/edc/boot/system/DependencyGraph.java @@ -14,7 +14,6 @@ package org.eclipse.edc.boot.system; -import org.eclipse.edc.boot.system.injection.EdcInjectionException; import org.eclipse.edc.boot.system.injection.InjectionContainer; import org.eclipse.edc.boot.system.injection.InjectionPoint; import org.eclipse.edc.boot.system.injection.InjectionPointScanner; @@ -25,9 +24,9 @@ import org.eclipse.edc.boot.util.TopologicalSort; import org.eclipse.edc.runtime.metamodel.annotation.Provides; import org.eclipse.edc.runtime.metamodel.annotation.Requires; -import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Arrays; @@ -37,7 +36,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.stream.Collectors; +import java.util.function.Function; import java.util.stream.Stream; import static java.util.stream.Collectors.toList; @@ -48,25 +47,42 @@ * which extension depends on which other extension. */ public class DependencyGraph { - private final InjectionPointScanner injectionPointScanner = new InjectionPointScanner(); - private final ServiceExtensionContext context; - public DependencyGraph(ServiceExtensionContext context) { - this.context = context; + private final List> injectionContainers; + /** + * contains all missing dependencies that were expressed as injection points + */ + private final HashMap, List> unsatisfiedInjectionPoints; + /** + * contains all missing dependencies that were expressed as @Require(...) annotations on the extension class + */ + private final ArrayList> unsatisfiedRequirements; + + private DependencyGraph(List> injectionContainers, HashMap, List> unsatisfiedInjectionPoints, ArrayList> unsatisfiedRequirements) { + + this.injectionContainers = injectionContainers; + this.unsatisfiedInjectionPoints = unsatisfiedInjectionPoints; + this.unsatisfiedRequirements = unsatisfiedRequirements; } /** - * Sorts all {@link ServiceExtension} implementors, that were found on the classpath, according to their dependencies. - * Depending Extensions (i.e. those who express a dependency) are sorted first, providing extensions (i.e. those - * who provide a dependency) are sorted last. + * Builds the DependencyGraph by evaluating all {@link ServiceExtension} implementors, that were found on the classpath, + * and sorting them topologically according to their dependencies. + * Dependent extensions (i.e. those who express a dependency) are sorted first, providing extensions (i.e. those + * who provide a dependency) are sorted last. + *

+ * This factory method does not throw any exception except a {@link CyclicDependencyException}, please check {@link DependencyGraph#isValid()} if the graph is valid. * + * @param context An instance of the (fully-initialized) {@link ServiceExtensionContext} which is used to resolve services and configuration. * @param extensions A list of {@link ServiceExtension} instances that were picked up by the {@link ServiceLocator} * @return A list of {@link InjectionContainer}s that are sorted topologically according to their dependencies. * @throws CyclicDependencyException when there is a dependency cycle * @see TopologicalSort * @see InjectionContainer */ - public List> of(List extensions) { + public static DependencyGraph of(ServiceExtensionContext context, List extensions) { + var injectionPointScanner = new InjectionPointScanner(); + Map, ServiceProvider> defaultServiceProviders = new HashMap<>(); Map, List>> dependencyMap = new HashMap<>(); var injectionContainers = extensions.stream() @@ -94,14 +110,14 @@ public List> of(List exte // check if all injected fields are satisfied, collect missing ones and throw exception otherwise var unsatisfiedInjectionPoints = new HashMap, List>(); - var unsatisfiedRequirements = new ArrayList(); + var unsatisfiedRequirements = new ArrayList>(); injectionContainers.forEach(container -> { //check that all the @Required features are there getRequiredFeatures(container.getInjectionTarget().getClass()).forEach(serviceClass -> { var dependencies = dependencyMap.get(serviceClass); if (dependencies == null) { - unsatisfiedRequirements.add(serviceClass.getName()); + unsatisfiedRequirements.add(serviceClass); } else { dependencies.forEach(dependency -> sort.addDependency(container, dependency)); } @@ -109,15 +125,16 @@ public List> of(List exte injectionPointScanner.getInjectionPoints(container.getInjectionTarget()) .peek(injectionPoint -> { - var providersResult = injectionPoint.getProviders(dependencyMap, context); - if (providersResult.succeeded()) { - List> providers = providersResult.getContent(); - providers.stream().filter(d -> !Objects.equals(d, container)).forEach(provider -> sort.addDependency(container, provider)); - } else { - if (injectionPoint.isRequired()) { - unsatisfiedInjectionPoints.computeIfAbsent(injectionPoint.getTargetInstance().getClass(), s -> new ArrayList<>()).add(new InjectionFailure(injectionPoint, providersResult.getFailureDetail())); - } - } + injectionPoint.getProviders(dependencyMap, context) + .onSuccess(providers -> providers.stream() + .filter(d -> !Objects.equals(d, container)) + .forEach(provider -> sort.addDependency(container, provider))) + .onFailure(f -> { + if (injectionPoint.isRequired()) { + unsatisfiedInjectionPoints.computeIfAbsent(injectionPoint.getTargetInstance().getClass(), s -> new ArrayList<>()) + .add(new InjectionFailure(injectionPoint.getTargetInstance(), injectionPoint, f.getFailureDetail())); + } + }); var defaultServiceProvider = defaultServiceProviders.get(injectionPoint.getType()); if (defaultServiceProvider != null) { @@ -127,24 +144,95 @@ public List> of(List exte .forEach(injectionPoint -> container.getInjectionPoints().add(injectionPoint)); }); - if (!unsatisfiedInjectionPoints.isEmpty()) { - var message = "The following injected fields or values were not provided or could not be resolved:\n"; - message += unsatisfiedInjectionPoints.entrySet().stream() - .map(entry -> String.format("%s is missing \n --> %s", entry.getKey(), String.join("\n --> ", entry.getValue().stream().map(Object::toString).toList()))).collect(Collectors.joining("\n")); - throw new EdcInjectionException(message); - } + sort.sort(injectionContainers); - if (!unsatisfiedRequirements.isEmpty()) { - var message = String.format("The following @Require'd features were not provided: [%s]", String.join(", ", unsatisfiedRequirements)); - throw new EdcException(message); - } + return new DependencyGraph(injectionContainers, unsatisfiedInjectionPoints, unsatisfiedRequirements); + } - sort.sort(injectionContainers); + /** + * Returns all {@link InjectionPoint}s for a particular extension class. These include all types of injection points. + * + * @param serviceClass The extension class + * @return A (potentially empty) list of injection points. + */ + public List> getDependenciesOf(Class serviceClass) { + return injectionContainers.stream().filter(ic -> ic.getInjectionTarget().getClass().equals(serviceClass)) + .flatMap(ic -> ic.getInjectionPoints().stream()) + .toList(); + } + /** + * Obtains all extension classes that declare a dependency on an object of the given point. For example, declaring a + * field {@code @Inject FooService fooService} in an extension would constitute such a dependency, and in that case + * {@code FooService.class} has to be passed into this method. + *

+ * This can also be invoked if the dependency graph is invalid, i.e. if dependency injection is not possible. + * + * @param dependencyType The type of the injection point, for example the type of service that is injected. + * @return a list of extension classes that declare a dependency onto the given injection point + */ + public List> getDependentExtensions(Class dependencyType) { + + return injectionContainers.stream() + .filter(ic -> ic.getInjectionPoints().stream().anyMatch(ip -> ip.getType().equals(dependencyType))) + .map(InjectionContainer::getInjectionTarget) + .map((Function>) ServiceExtension::getClass) // yes, it's nasty, but keeps the compiler happy + .toList(); + } + + /** + * Obtains a list of injection points, where the injection target is of the given type. For example, passing in {@code FooService.class} + * would return a list that contains injection points across all {@link ServiceExtension}s that declare a {@code @Inject FooService fooService} field. + * This is similar to {@link DependencyGraph#getDependenciesOf(Class)}, but the result values are {@link InjectionPoint}s rather than extension classes. + * + * @param dependencyType The type of the injection point, for example the type of service that is injected. + * @return a list of {@link InjectionPoint} instances that represent the concrete dependency + */ + public List> getDependenciesFor(Class dependencyType) { + return injectionContainers.stream() + .flatMap(ic -> ic.getInjectionPoints().stream().filter(ip -> ip.getType().equals(dependencyType))) + .toList(); + } + + public List> getInjectionContainers() { return injectionContainers; } - private Stream> getRequiredFeatures(Class clazz) { + /** + * Returns a list of extension instances that were found on the classpath + */ + public List getExtensions() { + return injectionContainers.stream().map(InjectionContainer::getInjectionTarget).toList(); + } + + /** + * Checks if the current dependency graph is valid, i.e. there are no cycles in it and all required injection + * dependencies are satisfied. + * + * @return true if the dependency graph is valid, and the DI container can be built, false otherwise. + */ + public boolean isValid() { + return unsatisfiedInjectionPoints.isEmpty() && unsatisfiedRequirements.isEmpty(); + } + + /** + * Returns a list of strings, each containing information about a missing dependency + * + * @return A list of errors describing one missing dependency each + */ + public List getProblems() { + var messages = unsatisfiedInjectionPoints.entrySet().stream() + .map(entry -> { + var dependent = entry.getKey(); + var dependencies = entry.getValue(); + var missingDependencyList = dependencies.stream().map(injectionFailure -> " --> " + injectionFailure.failureDetail()).toList(); + return "## %s is missing\n%s".formatted(dependent, String.join("\n", missingDependencyList)); + }); + + return messages.toList(); + } + + private static Stream> getRequiredFeatures(Class clazz) { var requiresAnnotation = clazz.getAnnotation(Requires.class); if (requiresAnnotation != null) { var features = requiresAnnotation.value(); @@ -156,7 +244,7 @@ private Stream> getRequiredFeatures(Class clazz) { /** * Obtains all features a specific extension requires as strings */ - private Set> getProvidedFeatures(ServiceExtension ext) { + private static Set> getProvidedFeatures(ServiceExtension ext) { var allProvides = new HashSet>(); // check all @Provides @@ -168,10 +256,11 @@ private Set> getProvidedFeatures(ServiceExtension ext) { return allProvides; } - private record InjectionFailure(InjectionPoint injectionPoint, String failureDetail) { + private record InjectionFailure(ServiceExtension dependent, InjectionPoint dependency, + @Nullable String failureDetail) { @Override public String toString() { - return "%s %s".formatted(injectionPoint.getTypeString(), failureDetail); + return "%s %s".formatted(dependency.getTypeString(), failureDetail); } } } diff --git a/core/common/boot/src/main/java/org/eclipse/edc/boot/system/ExtensionLoader.java b/core/common/boot/src/main/java/org/eclipse/edc/boot/system/ExtensionLoader.java index 870d57e07d..b63df589c5 100644 --- a/core/common/boot/src/main/java/org/eclipse/edc/boot/system/ExtensionLoader.java +++ b/core/common/boot/src/main/java/org/eclipse/edc/boot/system/ExtensionLoader.java @@ -18,7 +18,6 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; import org.eclipse.edc.boot.monitor.MultiplexingMonitor; -import org.eclipse.edc.boot.system.injection.InjectionContainer; import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.monitor.ConsoleMonitor; import org.eclipse.edc.spi.monitor.Monitor; @@ -83,9 +82,9 @@ public Monitor loadMonitor(String... programArgs) { /** * Loads and orders the service extensions. */ - public List> loadServiceExtensions(ServiceExtensionContext context) { + public DependencyGraph buildDependencyGraph(ServiceExtensionContext context) { var serviceExtensions = loadExtensions(ServiceExtension.class, true); - return new DependencyGraph(context).of(serviceExtensions); + return DependencyGraph.of(context, serviceExtensions); } /** diff --git a/core/common/boot/src/main/java/org/eclipse/edc/boot/system/injection/ConfigurationInjectionPoint.java b/core/common/boot/src/main/java/org/eclipse/edc/boot/system/injection/ConfigurationInjectionPoint.java index f929e07342..12a2053872 100644 --- a/core/common/boot/src/main/java/org/eclipse/edc/boot/system/injection/ConfigurationInjectionPoint.java +++ b/core/common/boot/src/main/java/org/eclipse/edc/boot/system/injection/ConfigurationInjectionPoint.java @@ -159,18 +159,18 @@ public Result>> getProviders(Map, List %s".formatted(configurationObject.getName(), configurationObject.getType().getSimpleName(), violators)); + return violators.isEmpty() ? Result.success(List.of()) : Result.failure("%s, through nested settings %s".formatted(toString(), violators)); } @Override public String getTypeString() { - return "Config object"; + return "Configuration object"; } @Override public String toString() { - return "Configuration object '%s' of type '%s' in %s" - .formatted(configurationObject.getName(), configurationObject.getType(), targetInstance.getClass()); + return "Configuration object \"%s\" of type [%s]" + .formatted(configurationObject.getName(), configurationObject.getType()); } private Predicate> constructorFilter(List args) { diff --git a/core/common/boot/src/main/java/org/eclipse/edc/boot/system/injection/ServiceInjectionPoint.java b/core/common/boot/src/main/java/org/eclipse/edc/boot/system/injection/ServiceInjectionPoint.java index d22ca51624..0b98e112d2 100644 --- a/core/common/boot/src/main/java/org/eclipse/edc/boot/system/injection/ServiceInjectionPoint.java +++ b/core/common/boot/src/main/java/org/eclipse/edc/boot/system/injection/ServiceInjectionPoint.java @@ -106,7 +106,7 @@ public Result>> getProviders(Map, List>> getProviders(Map, List>> dependencyMap, ServiceExtensionContext context) { + public Result>> getProviders(Map, List>> ignoredMap, ServiceExtensionContext context) { if (!annotationValue.required()) { return Result.success(emptyProviderlist); // optional configs are always satisfied @@ -177,17 +177,17 @@ public Result>> getProviders(Map, List valueType) { diff --git a/core/common/boot/src/main/java/org/eclipse/edc/boot/system/runtime/BaseRuntime.java b/core/common/boot/src/main/java/org/eclipse/edc/boot/system/runtime/BaseRuntime.java index 8fa1d1e6c7..b7dd73793a 100644 --- a/core/common/boot/src/main/java/org/eclipse/edc/boot/system/runtime/BaseRuntime.java +++ b/core/common/boot/src/main/java/org/eclipse/edc/boot/system/runtime/BaseRuntime.java @@ -20,10 +20,11 @@ import org.eclipse.edc.boot.config.EnvironmentVariables; import org.eclipse.edc.boot.config.SystemProperties; import org.eclipse.edc.boot.system.DefaultServiceExtensionContext; +import org.eclipse.edc.boot.system.DependencyGraph; import org.eclipse.edc.boot.system.ExtensionLoader; import org.eclipse.edc.boot.system.ServiceLocator; import org.eclipse.edc.boot.system.ServiceLocatorImpl; -import org.eclipse.edc.boot.system.injection.InjectionContainer; +import org.eclipse.edc.boot.system.injection.EdcInjectionException; import org.eclipse.edc.boot.system.injection.lifecycle.ExtensionLifecycleManager; import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.monitor.Monitor; @@ -48,9 +49,9 @@ *

    *
  • {@link BaseRuntime#createMonitor()} : instantiates a new {@link Monitor}
  • *
  • {@link BaseRuntime#createContext(Monitor, Config)}: creates a new {@link DefaultServiceExtensionContext} and invokes its {@link DefaultServiceExtensionContext#initialize()} method
  • - *
  • {@link BaseRuntime#createExtensions(ServiceExtensionContext)}: creates a list of {@code ServiceExtension} objects. By default, these are created through {@link ExtensionLoader#loadServiceExtensions(ServiceExtensionContext)}
  • - *
  • {@link BaseRuntime#bootExtensions(ServiceExtensionContext, List)}: initializes the service extensions by putting them through their lifecycle. - * By default this calls {@link ExtensionLifecycleManager#bootServiceExtensions(List, ServiceExtensionContext)}
  • + *
  • {@link BaseRuntime#buildDependencyGraph(ServiceExtensionContext)}: creates a list of {@code ServiceExtension} objects. By default, these are created through {@link ExtensionLoader#buildDependencyGraph(ServiceExtensionContext)}
  • + *
  • {@link BaseRuntime#bootExtensions(ServiceExtensionContext, DependencyGraph)}: initializes the service extensions by putting them through their lifecycle. + * By default, this calls {@link ExtensionLifecycleManager#bootServiceExtensions(List, ServiceExtensionContext)}
  • *
  • {@link BaseRuntime#onError(Exception)}: receives any Exception that was raised during initialization
  • *
*/ @@ -59,7 +60,7 @@ public class BaseRuntime { private static String[] programArgs = new String[0]; private final ExtensionLoader extensionLoader; private final ConfigurationLoader configurationLoader; - private final List serviceExtensions = new ArrayList<>(); + private List serviceExtensions = new ArrayList<>(); protected Monitor monitor; protected ServiceExtensionContext context; @@ -91,10 +92,16 @@ public void boot(boolean addShutdownHook) { context = createServiceExtensionContext(config); try { - var newExtensions = createExtensions(context); - bootExtensions(context, newExtensions); + var graph = buildDependencyGraph(context); + + if (!graph.isValid()) { + onError(new EdcInjectionException("The following problems occurred during dependency injection:\n%s".formatted(String.join("\n", graph.getProblems())))); + } + + bootExtensions(context, graph); + + serviceExtensions = graph.getExtensions(); - newExtensions.stream().map(InjectionContainer::getInjectionTarget).forEach(serviceExtensions::add); if (addShutdownHook) { getRuntime().addShutdownHook(new Thread(this::shutdown)); } @@ -122,7 +129,6 @@ public void shutdown() { var extension = iter.previous(); extension.shutdown(); monitor.debug("Shutdown " + extension.name()); - iter.remove(); } monitor.info("Shutdown complete"); } @@ -149,11 +155,11 @@ protected void onError(Exception e) { /** * Starts all service extensions by invoking {@link ExtensionLifecycleManager#bootServiceExtensions(List, ServiceExtensionContext)} * - * @param context The {@code ServiceExtensionContext} that is used in this runtime. - * @param serviceExtensions a list of extensions + * @param context The {@code ServiceExtensionContext} that is used in this runtime. + * @param graph a list of extensions */ - protected void bootExtensions(ServiceExtensionContext context, List> serviceExtensions) { - ExtensionLifecycleManager.bootServiceExtensions(serviceExtensions, context); + protected void bootExtensions(ServiceExtensionContext context, DependencyGraph graph) { + ExtensionLifecycleManager.bootServiceExtensions(graph.getInjectionContainers(), context); } /** @@ -162,8 +168,8 @@ protected void bootExtensions(ServiceExtensionContext context, List> createExtensions(ServiceExtensionContext context) { - return extensionLoader.loadServiceExtensions(context); + protected DependencyGraph buildDependencyGraph(ServiceExtensionContext context) { + return extensionLoader.buildDependencyGraph(context); } /** @@ -171,7 +177,7 @@ protected List> createExtensions(ServiceExt * this would likely need to be overridden. * * @param monitor a Monitor - * @param config the cofiguratiohn + * @param config the configuration * @return a {@code ServiceExtensionContext} */ @NotNull @@ -180,7 +186,7 @@ protected ServiceExtensionContext createContext(Monitor monitor, Config config) } /** - * Hook point to instantiate a {@link Monitor}. By default, the runtime instantiates a {@code Monitor} using the + * Hook point to instantiate a {@link Monitor}. By default, the runtime instantiates a {@code Monitor} using the * Service Loader mechanism, i.e. by calling the {@link ExtensionLoader#loadMonitor(String...)} method. *

* Please consider using the extension mechanism (i.e. {@link MonitorExtension}) rather than supplying a custom monitor by overriding this method. diff --git a/core/common/boot/src/test/java/org/eclipse/edc/boot/system/DependencyGraphTest.java b/core/common/boot/src/test/java/org/eclipse/edc/boot/system/DependencyGraphTest.java index 11aa365ca3..2b16494948 100644 --- a/core/common/boot/src/test/java/org/eclipse/edc/boot/system/DependencyGraphTest.java +++ b/core/common/boot/src/test/java/org/eclipse/edc/boot/system/DependencyGraphTest.java @@ -14,58 +14,100 @@ package org.eclipse.edc.boot.system; -import org.eclipse.edc.boot.system.injection.EdcInjectionException; import org.eclipse.edc.boot.system.injection.InjectionContainer; +import org.eclipse.edc.boot.system.injection.ServiceInjectionPoint; +import org.eclipse.edc.boot.system.injection.ValueInjectionPoint; +import org.eclipse.edc.boot.system.testextensions.DependentExtension; +import org.eclipse.edc.boot.system.testextensions.RequiredDependentExtension; import org.junit.jupiter.api.Test; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.eclipse.edc.boot.system.TestFunctions.createDependentExtension; +import static org.eclipse.edc.boot.system.TestFunctions.createProviderExtension; import static org.eclipse.edc.boot.system.TestFunctions.mutableListOf; import static org.mockito.Mockito.mock; class DependencyGraphTest { - private final DependencyGraph graph = new DependencyGraph(mock()); @Test - void sortExtensions_withDefaultProvider() { - var providerExtension = TestFunctions.createProviderExtension(true); - var dependentExtension = TestFunctions.createDependentExtension(true); + void getExtensions_withDefaultProvider() { + var providerExtension = createProviderExtension(true); + var dependentExtension = createDependentExtension(true); - var list = graph.of(mutableListOf(dependentExtension, providerExtension)); + var list = DependencyGraph.of(mock(), mutableListOf(dependentExtension, providerExtension)).getInjectionContainers(); assertThat(list).extracting(InjectionContainer::getInjectionTarget) .containsExactly(providerExtension, dependentExtension); } @Test - void sortExtensions_withNoDefaultProvider() { - var defaultProvider = TestFunctions.createProviderExtension(false); - var provider = TestFunctions.createProviderExtension(true); - var dependentExtension = TestFunctions.createDependentExtension(true); + void getExtensions_withNoDefaultProvider() { + var defaultProvider = createProviderExtension(false); + var provider = createProviderExtension(true); + var dependentExtension = createDependentExtension(true); - var list = graph.of(mutableListOf(dependentExtension, provider, defaultProvider)); + var list = DependencyGraph.of(mock(), mutableListOf(dependentExtension, provider, defaultProvider)).getInjectionContainers(); assertThat(list).extracting(InjectionContainer::getInjectionTarget) .containsExactly(provider, defaultProvider, dependentExtension); } @Test - void sortExtensions_missingDependency() { - var dependentExtension = TestFunctions.createDependentExtension(true); + void getExtensions_missingDependency() { + var dependentExtension = createDependentExtension(true); - assertThatThrownBy(() -> graph.of(mutableListOf(dependentExtension))) - .isInstanceOf(EdcInjectionException.class); + assertThat(DependencyGraph.of(mock(), mutableListOf(dependentExtension)).isValid()).isFalse(); } @Test - void sortExtensions_missingOptionalDependency() { - var dependentExtension = TestFunctions.createDependentExtension(false); + void getExtensions_missingOptionalDependency() { + var dependentExtension = createDependentExtension(false); - var injectionContainers = graph.of(mutableListOf(dependentExtension)); + var dependencyGraph = DependencyGraph.of(mock(), mutableListOf(dependentExtension)); - assertThat(injectionContainers).hasSize(1) + assertThat(dependencyGraph.isValid()).isTrue(); + assertThat(dependencyGraph.getInjectionContainers()).hasSize(1) .extracting(InjectionContainer::getInjectionTarget) .containsExactly(dependentExtension); } + + @Test + void getDependenciesOf() { + var providerExtension = createProviderExtension(false); + var dependentExtension = createDependentExtension(true); + + var graph = DependencyGraph.of(mock(), List.of(providerExtension, dependentExtension)); + var dependencies = graph.getDependenciesOf(RequiredDependentExtension.class); + assertThat(dependencies).hasSize(2) + .anySatisfy(ip -> assertThat(ip).isInstanceOf(ServiceInjectionPoint.class)) + .anySatisfy(ip -> assertThat(ip).isInstanceOf(ValueInjectionPoint.class)); + } + + @Test + void getDependentExtensions() { + var providerExtension = createProviderExtension(true); + var ext1 = createDependentExtension(true); + var ext2 = createDependentExtension(false); + + var graph = DependencyGraph.of(mock(), List.of(providerExtension, ext1, ext2)); + var dependents = graph.getDependentExtensions(TestObject.class); + + assertThat(dependents).hasSize(2) + .containsExactlyInAnyOrder(RequiredDependentExtension.class, DependentExtension.class); + } + + @Test + void getDependenciesFor() { + var providerExtension = createProviderExtension(true); + var ext1 = createDependentExtension(true); + var ext2 = createDependentExtension(false); + + var graph = DependencyGraph.of(mock(), List.of(providerExtension, ext1, ext2)); + var deps = graph.getDependenciesFor(TestObject.class); + assertThat(deps).hasSize(2); + } + } diff --git a/core/common/boot/src/test/java/org/eclipse/edc/boot/system/ExtensionLoaderTest.java b/core/common/boot/src/test/java/org/eclipse/edc/boot/system/ExtensionLoaderTest.java index d83a6ae118..fbeaf6ac0c 100644 --- a/core/common/boot/src/test/java/org/eclipse/edc/boot/system/ExtensionLoaderTest.java +++ b/core/common/boot/src/test/java/org/eclipse/edc/boot/system/ExtensionLoaderTest.java @@ -18,7 +18,6 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; import org.eclipse.edc.boot.monitor.MultiplexingMonitor; -import org.eclipse.edc.boot.system.injection.EdcInjectionException; import org.eclipse.edc.boot.system.injection.InjectionContainer; import org.eclipse.edc.boot.util.CyclicDependencyException; import org.eclipse.edc.runtime.metamodel.annotation.Inject; @@ -150,13 +149,13 @@ void selectOpenTelemetryImpl_whenSeveralOpenTelemetry() { @Test @DisplayName("No dependencies between service extensions") - void loadServiceExtensions_noDependencies() { + void buildDependencyGraph_noDependencies() { var service1 = new ServiceExtension() { }; when(serviceLocator.loadImplementors(eq(ServiceExtension.class), anyBoolean())).thenReturn(mutableListOf(service1)); - var list = loader.loadServiceExtensions(context); + var list = loader.buildDependencyGraph(context).getInjectionContainers(); assertThat(list).hasSize(1).extracting(InjectionContainer::getInjectionTarget).containsExactly(service1); verify(serviceLocator).loadImplementors(eq(ServiceExtension.class), anyBoolean()); @@ -164,7 +163,7 @@ void loadServiceExtensions_noDependencies() { @Test @DisplayName("Locating two service extensions for the same service class ") - void loadServiceExtensions_whenMultipleServices() { + void buildDependencyGraph_whenMultipleServices() { var service1 = new ServiceExtension() { }; var service2 = new ServiceExtension() { @@ -172,7 +171,7 @@ void loadServiceExtensions_whenMultipleServices() { when(serviceLocator.loadImplementors(eq(ServiceExtension.class), anyBoolean())).thenReturn(mutableListOf(service1, service2)); - var list = loader.loadServiceExtensions(context); + var list = loader.buildDependencyGraph(context).getInjectionContainers(); assertThat(list).hasSize(2).extracting(InjectionContainer::getInjectionTarget).containsExactly(service1, service2); verify(serviceLocator).loadImplementors(eq(ServiceExtension.class), anyBoolean()); @@ -180,21 +179,21 @@ void loadServiceExtensions_whenMultipleServices() { @Test @DisplayName("A DEFAULT service extension depends on a PRIMORDIAL one") - void loadServiceExtensions_withBackwardsDependency() { + void buildDependencyGraph_withBackwardsDependency() { var depending = new DependingExtension(); var someExtension = new SomeExtension(); var providing = new ProvidingExtension(); when(serviceLocator.loadImplementors(eq(ServiceExtension.class), anyBoolean())).thenReturn(mutableListOf(providing, depending, someExtension)); - var services = loader.loadServiceExtensions(context); + var services = loader.buildDependencyGraph(context).getInjectionContainers(); assertThat(services).extracting(InjectionContainer::getInjectionTarget).containsExactly(providing, depending, someExtension); verify(serviceLocator).loadImplementors(eq(ServiceExtension.class), anyBoolean()); } @Test @DisplayName("A service extension has a dependency on another one of the same loading stage") - void loadServiceExtensions_withEqualDependency() { + void buildDependencyGraph_withEqualDependency() { var depending = new DependingExtension() { }; var coreService = new SomeExtension() { @@ -205,71 +204,70 @@ void loadServiceExtensions_withEqualDependency() { when(serviceLocator.loadImplementors(eq(ServiceExtension.class), anyBoolean())).thenReturn(mutableListOf(depending, thirdService, coreService)); - var services = loader.loadServiceExtensions(context); + var services = loader.buildDependencyGraph(context).getInjectionContainers(); assertThat(services).extracting(InjectionContainer::getInjectionTarget).containsExactlyInAnyOrder(coreService, depending, thirdService); verify(serviceLocator).loadImplementors(eq(ServiceExtension.class), anyBoolean()); } @Test @DisplayName("Two service extensions have a circular dependency") - void loadServiceExtensions_withCircularDependency() { + void buildDependencyGraph_withCircularDependency() { var s1 = new TestProvidingExtension2(); var s2 = new TestProvidingExtension(); when(serviceLocator.loadImplementors(eq(ServiceExtension.class), anyBoolean())).thenReturn(mutableListOf(s1, s2)); - assertThatThrownBy(() -> loader.loadServiceExtensions(context)).isInstanceOf(CyclicDependencyException.class); + assertThatThrownBy(() -> loader.buildDependencyGraph(context)).isInstanceOf(CyclicDependencyException.class); verify(serviceLocator).loadImplementors(eq(ServiceExtension.class), anyBoolean()); } @Test @DisplayName("A service extension has an unsatisfied dependency") - void loadServiceExtensions_dependencyNotSatisfied() { + void buildDependencyGraph_dependencyNotSatisfied() { var depending = new DependingExtension(); var someExtension = new SomeExtension(); when(serviceLocator.loadImplementors(eq(ServiceExtension.class), anyBoolean())).thenReturn(mutableListOf(depending, someExtension)); - assertThatThrownBy(() -> loader.loadServiceExtensions(context)) - .isInstanceOf(EdcException.class) - .hasMessageContaining("The following injected fields or values were not provided or could not be resolved") - .hasMessageContaining("Service someService of type class org.eclipse.edc.boot.system.ExtensionLoaderTest$SomeObject"); + var graph = loader.buildDependencyGraph(context); + assertThat(graph.isValid()).isFalse(); + assertThat(graph.getProblems()).hasSize(1); verify(serviceLocator).loadImplementors(eq(ServiceExtension.class), anyBoolean()); } @Test @DisplayName("Services extensions are sorted by dependency order") - void loadServiceExtensions_dependenciesAreSorted() { + void buildDependencyGraph_dependenciesAreSorted() { var depending = new DependingExtension(); var providingExtension = new ProvidingExtension(); when(serviceLocator.loadImplementors(eq(ServiceExtension.class), anyBoolean())).thenReturn(mutableListOf(depending, providingExtension)); - var services = loader.loadServiceExtensions(context); + var services = loader.buildDependencyGraph(context).getInjectionContainers(); assertThat(services).extracting(InjectionContainer::getInjectionTarget).containsExactly(providingExtension, depending); verify(serviceLocator).loadImplementors(eq(ServiceExtension.class), anyBoolean()); } @Test @DisplayName("Should throw exception when no core dependency found") - void loadServiceExtensions_noCoreDependencyShouldThrowException() { + void buildDependencyGraph_noCoreDependency_shouldBeInvalid() { var depending = new DependingExtension(); var coreService = new SomeExtension(); when(serviceLocator.loadImplementors(eq(ServiceExtension.class), anyBoolean())).thenReturn(mutableListOf(depending, coreService)); - assertThatThrownBy(() -> loader.loadServiceExtensions(context)).isInstanceOf(EdcException.class); + assertThat(loader.buildDependencyGraph(context).isValid()).isFalse(); } @Test @DisplayName("Requires annotation influences ordering") - void loadServiceExtensions_withAnnotation() { + void buildDependencyGraph_withAnnotation() { var depending = new DependingExtension(); var providingExtension = new ProvidingExtension(); var annotatedExtension = new AnnotatedExtension(); when(serviceLocator.loadImplementors(eq(ServiceExtension.class), anyBoolean())).thenReturn(mutableListOf(depending, annotatedExtension, providingExtension)); - var services = loader.loadServiceExtensions(context); + var services = loader.buildDependencyGraph(context).getInjectionContainers(); assertThat(services).extracting(InjectionContainer::getInjectionTarget).containsExactly(providingExtension, depending, annotatedExtension); verify(serviceLocator).loadImplementors(eq(ServiceExtension.class), anyBoolean()); @@ -277,38 +275,38 @@ void loadServiceExtensions_withAnnotation() { @Test @DisplayName("Requires annotation not satisfied") - void loadServiceExtensions_withAnnotation_notSatisfied() { + void buildDependencyGraph_withAnnotation_notSatisfied() { var annotatedExtension = new AnnotatedExtension(); when(serviceLocator.loadImplementors(eq(ServiceExtension.class), anyBoolean())).thenReturn(mutableListOf(annotatedExtension)); - assertThatThrownBy(() -> loader.loadServiceExtensions(context)).isNotInstanceOf(EdcInjectionException.class).isInstanceOf(EdcException.class); + assertThat(loader.buildDependencyGraph(context).isValid()).isFalse(); verify(serviceLocator).loadImplementors(eq(ServiceExtension.class), anyBoolean()); } @Test @DisplayName("Mixed requirement features work") - void loadServiceExtensions_withMixedInjectAndAnnotation() { + void buildDependencyGraph_withMixedInjectAndAnnotation() { var providingExtension = new ProvidingExtension(); // provides SomeObject var anotherProvidingExt = new AnotherProvidingExtension(); //provides AnotherObject var mixedAnnotation = new MixedAnnotation(); when(serviceLocator.loadImplementors(eq(ServiceExtension.class), anyBoolean())).thenReturn(mutableListOf(mixedAnnotation, providingExtension, anotherProvidingExt)); - var services = loader.loadServiceExtensions(context); + var services = loader.buildDependencyGraph(context).getInjectionContainers(); assertThat(services).extracting(InjectionContainer::getInjectionTarget).containsExactly(providingExtension, anotherProvidingExt, mixedAnnotation); verify(serviceLocator).loadImplementors(eq(ServiceExtension.class), anyBoolean()); } @Test @DisplayName("Mixed requirement features introducing circular dependency") - void loadServiceExtensions_withMixedInjectAndAnnotation_withCircDependency() { + void buildDependencyGraph_withMixedInjectAndAnnotation_withCircDependency() { var s1 = new TestProvidingExtension3(); var s2 = new TestProvidingExtension(); when(serviceLocator.loadImplementors(eq(ServiceExtension.class), anyBoolean())).thenReturn(mutableListOf(s1, s2)); - assertThatThrownBy(() -> loader.loadServiceExtensions(context)).isInstanceOf(CyclicDependencyException.class); + assertThatThrownBy(() -> loader.buildDependencyGraph(context)).isInstanceOf(CyclicDependencyException.class); verify(serviceLocator).loadImplementors(eq(ServiceExtension.class), anyBoolean()); } diff --git a/core/common/boot/src/test/java/org/eclipse/edc/boot/system/injection/ConfigurationInjectionPointTest.java b/core/common/boot/src/test/java/org/eclipse/edc/boot/system/injection/ConfigurationInjectionPointTest.java index 9cd7773752..0ff7cb0c1a 100644 --- a/core/common/boot/src/test/java/org/eclipse/edc/boot/system/injection/ConfigurationInjectionPointTest.java +++ b/core/common/boot/src/test/java/org/eclipse/edc/boot/system/injection/ConfigurationInjectionPointTest.java @@ -115,6 +115,7 @@ void getProviders_hasViolations() { var result = injectionPoint.getProviders(Map.of(), context); assertThat(result.succeeded()).isFalse(); - assertThat(result.getFailureDetail()).isEqualTo("configurationObject (ConfigurationObject) --> [requiredVal (property \"foo.bar.baz\")]"); + assertThat(result.getFailureDetail()).isEqualTo("Configuration object \"configurationObject\" of type [class org.eclipse.edc.boot.system.testextensions.ConfigurationObject], " + + "through nested settings [Configuration value \"requiredVal\" of type [class java.lang.String] (property 'foo.bar.baz')]"); } } \ No newline at end of file diff --git a/core/common/boot/src/test/java/org/eclipse/edc/boot/system/injection/lifecycle/ExtensionLifecycleManagerTest.java b/core/common/boot/src/test/java/org/eclipse/edc/boot/system/injection/lifecycle/ExtensionLifecycleManagerTest.java index 376f10f3cb..26a725c5b1 100644 --- a/core/common/boot/src/test/java/org/eclipse/edc/boot/system/injection/lifecycle/ExtensionLifecycleManagerTest.java +++ b/core/common/boot/src/test/java/org/eclipse/edc/boot/system/injection/lifecycle/ExtensionLifecycleManagerTest.java @@ -124,7 +124,7 @@ public void boot(ServiceExtension... serviceExtensions) { } public List> createInjectionContainers(List extensions) { - return new DependencyGraph(context).of(extensions); + return DependencyGraph.of(context, extensions).getInjectionContainers(); } } diff --git a/core/common/boot/src/test/java/org/eclipse/edc/boot/system/testextensions/RequiredDependentExtension.java b/core/common/boot/src/test/java/org/eclipse/edc/boot/system/testextensions/RequiredDependentExtension.java index 178caab800..5ced1dc3db 100644 --- a/core/common/boot/src/test/java/org/eclipse/edc/boot/system/testextensions/RequiredDependentExtension.java +++ b/core/common/boot/src/test/java/org/eclipse/edc/boot/system/testextensions/RequiredDependentExtension.java @@ -16,6 +16,7 @@ import org.eclipse.edc.boot.system.TestObject; import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; import org.eclipse.edc.spi.system.ServiceExtension; public class RequiredDependentExtension implements ServiceExtension { @@ -25,4 +26,7 @@ public TestObject getTestObject() { @Inject private TestObject testObject; + + @Setting(key = "foo.bar", required = false) + private String fooBar; } diff --git a/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-remote-client/src/main/java/org/eclipse/edc/iam/identitytrust/sts/remote/client/StsRemoteClientConfigurationExtension.java b/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-remote-client/src/main/java/org/eclipse/edc/iam/identitytrust/sts/remote/client/StsRemoteClientConfigurationExtension.java index 3797fdf851..2c84f9d4f5 100644 --- a/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-remote-client/src/main/java/org/eclipse/edc/iam/identitytrust/sts/remote/client/StsRemoteClientConfigurationExtension.java +++ b/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-remote-client/src/main/java/org/eclipse/edc/iam/identitytrust/sts/remote/client/StsRemoteClientConfigurationExtension.java @@ -48,7 +48,6 @@ public StsRemoteClientConfiguration clientConfiguration(ServiceExtensionContext return new StsRemoteClientConfiguration(clientConfig.tokenUrl(), clientConfig.clientId(), clientConfig.clientSecretAlias()); } - @Settings private record StsClientConfig( @Setting(key = "edc.iam.sts.oauth.token.url", description = "STS OAuth2 endpoint for requesting a token") String tokenUrl,