From f063f63b579f643ec57f6af61e0b5928549ea211 Mon Sep 17 00:00:00 2001 From: DasBabyPixel <43410952+DasBabyPixel@users.noreply.github.com> Date: Sun, 27 Aug 2023 04:22:05 +0200 Subject: [PATCH] module loading improvements --- .../driver/module/DefaultModuleProvider.java | 294 ++++++++++++++---- .../ModuleCyclicDependenciesException.java | 42 +++ .../ModuleDependencyOutdatedException.java | 27 +- .../driver/module/ModuleProvider.java | 26 +- .../module/util/ModuleDependencyUtil.java | 37 ++- .../module/ModuleDependencyUtilTest.java | 10 + 6 files changed, 358 insertions(+), 78 deletions(-) create mode 100644 driver/src/main/java/eu/cloudnetservice/driver/module/ModuleCyclicDependenciesException.java diff --git a/driver/src/main/java/eu/cloudnetservice/driver/module/DefaultModuleProvider.java b/driver/src/main/java/eu/cloudnetservice/driver/module/DefaultModuleProvider.java index a1ef87b9d0..f9a5bf8612 100644 --- a/driver/src/main/java/eu/cloudnetservice/driver/module/DefaultModuleProvider.java +++ b/driver/src/main/java/eu/cloudnetservice/driver/module/DefaultModuleProvider.java @@ -18,6 +18,7 @@ import com.google.common.base.Preconditions; import dev.derklaro.aerogel.Element; +import dev.derklaro.aerogel.SpecifiedInjector; import dev.derklaro.aerogel.auto.Provides; import dev.derklaro.aerogel.binding.BindingBuilder; import dev.derklaro.aerogel.util.Qualifiers; @@ -26,6 +27,7 @@ import eu.cloudnetservice.common.tuple.Tuple2; import eu.cloudnetservice.driver.document.DocumentFactory; import eu.cloudnetservice.driver.inject.InjectionLayer; +import eu.cloudnetservice.driver.module.util.ModuleDependencyUtil; import jakarta.inject.Singleton; import java.io.BufferedInputStream; import java.io.IOException; @@ -34,8 +36,11 @@ import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -45,6 +50,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; +import java.util.stream.Collectors; import lombok.NonNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -181,62 +187,16 @@ public void moduleDependencyLoader(@NonNull ModuleDependencyLoader moduleDepende @Override public @Nullable ModuleWrapper loadModule(@NonNull URL url) { try { - // check if there is any other module loaded from the same url - if (this.findModuleBySource(url).isPresent()) { - return null; - } - // check if we can load the module configuration from the file - var moduleConfiguration = this.findModuleConfiguration(url).orElse(null); + var moduleConfiguration = this.loadModuleConfigurationIfValid(url).orElse(null); if (moduleConfiguration == null) { - throw new ModuleConfigurationNotFoundException(url); - } - // check if the module can run on the current java version release. - if (!moduleConfiguration.canRunOn(JavaVersion.runtimeVersion())) { - LOGGER.warn( - "Unable to load module {}:{} because it only supports Java {}+", - moduleConfiguration.group(), - moduleConfiguration.name(), - moduleConfiguration.minJavaVersionId()); return null; } - // get the data directory of the module - var dataDirectory = moduleConfiguration.dataFolder(this.moduleDirectory); - - // create the injection layer for the module - var externalLayer = InjectionLayer.ext(); - var moduleLayer = InjectionLayer.specifiedChild(externalLayer, "module", (layer, injector) -> { - injector.installSpecified(BindingBuilder.create() - .bind(DATA_DIRECTORY_ELEMENT) - .toInstance(dataDirectory)); - injector.installSpecified(BindingBuilder.create() - .bind(MODULE_CONFIGURATION_ELEMENT) - .toInstance(moduleConfiguration)); - }); - // initialize all dependencies of the module var repositories = this.collectModuleProvidedRepositories(moduleConfiguration); var dependencies = this.loadDependencies(repositories, moduleConfiguration); - // create the class loader for the module - var loader = new ModuleURLClassLoader(url, dependencies.first(), moduleLayer); - loader.registerGlobally(); - // try to load and create the main class instance - var mainModuleClass = loader.loadClass(moduleConfiguration.main()); - // check if the main class is an instance of the IModule class - if (!Module.class.isAssignableFrom(mainModuleClass)) { - throw new AssertionError(String.format("Module main class %s is not assignable from %s", - mainModuleClass.getCanonicalName(), Module.class.getCanonicalName())); - } - // create an instance of the class and the main module wrapper - var moduleInstance = (Module) moduleLayer.instance(mainModuleClass); - var moduleWrapper = new DefaultModuleWrapper(url, moduleInstance, dataDirectory, - this, loader, dependencies.second(), moduleConfiguration, moduleLayer); - // initialize the module instance now - moduleInstance.init(loader, moduleWrapper, moduleConfiguration); - // register the module, load it and return the created wrapper - this.modules.add(moduleWrapper); - return moduleWrapper.loadModule(); + return this.loadAndInitialize(url, dependencies, moduleConfiguration); } catch (IOException | URISyntaxException exception) { throw new AssertionError("Exception reading module information of " + url, exception); } catch (ReflectiveOperationException exception) { @@ -257,13 +217,23 @@ public void moduleDependencyLoader(@NonNull ModuleDependencyLoader moduleDepende } } + /** + * {@inheritDoc} + * + * @see #loadAll(Collection) + */ @Override public @NonNull ModuleProvider loadAll() { - FileUtil.walkFileTree( - this.moduleDirectory, - ($, current) -> this.loadModule(current), - false, - "*.{jar,war}"); + var urls = new ArrayList(); + FileUtil.walkFileTree(this.moduleDirectory, (_, current) -> { + try { + urls.add(current.toUri().toURL()); + } catch (MalformedURLException exception) { + LOGGER.error("Unable to resolve url of module path", exception); + } + }, false, "*.{jar,war}"); + + this.loadAll(urls); return this; } @@ -363,6 +333,224 @@ public void notifyPostModuleLifecycleChange(@NonNull ModuleWrapper wrapper, @Non } } + /** + * Loads modules from a {@link Collection} of {@link URL}s + * + * @param urls a collection of URLs to modules that should be loaded + * @throws ModuleCyclicDependenciesException if cyclic dependencies are detected + * @see ModuleWrapper#moduleLifeCycle() + * @see ModuleLifeCycle#canChangeTo(ModuleLifeCycle) + * @see #loadAll() + */ + protected void loadAll(Collection urls) { + // Collect the configurations before loading any classes + var loadableModules = new HashMap(); + var loadableUrls = new HashMap(); + + for (URL url : urls) { + try { + this.loadModuleConfigurationIfValid(url).ifPresent(moduleConfiguration -> { + loadableModules.put(moduleConfiguration.name(), moduleConfiguration); + loadableUrls.put(moduleConfiguration.name(), url); + }); + } catch (IOException | URISyntaxException exception) { + throw new AssertionError("Exception reading module information of " + url, exception); + } + } + + // map for easy dependency resolution by name. this will be populated further, so it must be mutable. + var knownModules = this.modules().stream().map(ModuleWrapper::moduleConfiguration) + .collect(Collectors.toMap(ModuleConfiguration::name, configuration -> configuration, ($1, $2) -> { + throw new AssertionError("Two modules must not have the same name"); + }, HashMap::new)); + + // iterate in a queue-like manner. Using a map is preferable, because it makes dependency resolution easier. + while (!loadableModules.isEmpty()) { + var moduleName = loadableModules.keySet().iterator().next(); + var moduleConfiguration = loadableModules.remove(moduleName); + var url = loadableUrls.remove(moduleName); + try { + // load the module after recursively loading all its dependencies. + this.loadModuleAndDependencies(url, moduleConfiguration, knownModules, loadableUrls, loadableModules, + new ArrayDeque<>()); + } catch (ReflectiveOperationException exception) { + throw new AssertionError("Exception creating module instance for module " + moduleName, exception); + } catch (URISyntaxException exception) { + throw new AssertionError("Exception reading module information of " + url, exception); + } + } + } + + /** + * Recursively loads a module and its dependencies + * + * @param url the url to the module + * @param moduleConfiguration the module configuration + * @param knownModules all modules that are already loaded + * @param loadableUrls urls to all modules that can be loaded + * @param loadableModules configurations to all modules that can be loaded + * @throws ReflectiveOperationException if there is an access problem + * @throws ModuleCyclicDependenciesException if cyclic dependencies are detected + * @throws URISyntaxException if the syntax of the given url is invalid + */ + protected void loadModuleAndDependencies(@NonNull URL url, @NonNull ModuleConfiguration moduleConfiguration, + @NonNull Map knownModules, @NonNull Map loadableUrls, + @NonNull Map loadableModules, @NonNull Deque dependencyPath) + throws ReflectiveOperationException, URISyntaxException { + var moduleName = moduleConfiguration.name(); + // add this module to the dependency path. this allows for cyclic dependency detection + dependencyPath.offerLast(moduleName); + var repositories = this.collectModuleProvidedRepositories(moduleConfiguration); + var dependencies = this.loadDependencies(repositories, moduleConfiguration); + // make sure all dependencies are loaded. + for (var dependency : dependencies.second()) { + var dependencyName = dependency.name(); + if (dependencyPath.contains(dependencyName)) { + // cyclic dependencies detected! add the last dependency for complete cycle and throw an error + dependencyPath.offerLast(dependencyName); + throw new ModuleCyclicDependenciesException(dependencyPath.toArray(String[]::new)); + } + + // do we already know the dependency? + if (!knownModules.containsKey(dependencyName)) { + // can we load the dependency? + if (!loadableModules.containsKey(dependencyName)) { + // the dependency wasn't found. + throw new ModuleDependencyNotFoundException(dependencyName, moduleName); + } + // load the dependency (and its dependencies). + var dependencyConfiguration = loadableModules.remove(dependencyName); + var dependencyUrl = loadableUrls.remove(dependencyName); + + // recursive load for the dependency module + this.loadModuleAndDependencies(dependencyUrl, dependencyConfiguration, knownModules, loadableUrls, + loadableModules, dependencyPath); + } + // the dependency is available. + var presentDependency = knownModules.get(dependencyName); + // make sure the version demands are met + ModuleDependencyUtil.checkDependencyVersion(moduleConfiguration, presentDependency, dependency); + } + // all the dependency requirements are met. Now we have to load the module + this.loadAndInitialize(url, dependencies, moduleConfiguration); + // after loading the module, store it so other modules can resolve it. + knownModules.put(moduleName, moduleConfiguration); + // don't forget removing this module from the dependency path + dependencyPath.pollLast(); + } + + /** + * Creates an {@link InjectionLayer} for the module, then calls + * {@link #loadAndInitialize(URL, Tuple2, InjectionLayer, ModuleConfiguration, Path)} + * + * @param url the url to the module file + * @param dependencies the dependencies for the module + * @param moduleConfiguration the module configuration + * @return the loaded and initialized wrapper for the module + * @throws ReflectiveOperationException if there is an access problem + * @throws URISyntaxException if the syntax of the given url is invalid + * @throws ModuleDependencyNotFoundException if a {@link ModuleDependency} is missing + */ + protected @NonNull ModuleWrapper loadAndInitialize(@NonNull URL url, + @NonNull Tuple2, Set> dependencies, @NonNull ModuleConfiguration moduleConfiguration) + throws ReflectiveOperationException, ModuleDependencyNotFoundException, URISyntaxException { + // get the data directory of the module + var dataDirectory = moduleConfiguration.dataFolder(this.moduleDirectory); + + // create the injection layer for the module + var externalLayer = InjectionLayer.ext(); + var moduleLayer = InjectionLayer.specifiedChild(externalLayer, "module", (layer, injector) -> { + injector.installSpecified(BindingBuilder.create().bind(DATA_DIRECTORY_ELEMENT).toInstance(dataDirectory)); + injector.installSpecified( + BindingBuilder.create().bind(MODULE_CONFIGURATION_ELEMENT).toInstance(moduleConfiguration)); + }); + return this.loadAndInitialize(url, dependencies, moduleLayer, moduleConfiguration, dataDirectory); + } + + /** + * @param url the url to the module file + * @param dependencies the dependencies for the module + * @param moduleLayer the injection layer for the module + * @param moduleConfiguration the module configuration + * @param dataDirectory the data directory of the module + * @return the loaded and initialized wrapper for the module + * @throws ReflectiveOperationException if there is an access problem + * @throws URISyntaxException if the syntax of the given url is invalid + * @throws ModuleDependencyNotFoundException if a {@link ModuleDependency} is missing + */ + protected @NonNull ModuleWrapper loadAndInitialize(@NonNull URL url, + @NonNull Tuple2, Set> dependencies, + @NonNull InjectionLayer moduleLayer, @NonNull ModuleConfiguration moduleConfiguration, + @NonNull Path dataDirectory) + throws ReflectiveOperationException, ModuleDependencyNotFoundException, URISyntaxException { + for (ModuleDependency dependency : dependencies.second()) { + var wrapper = this.module(dependency.name()); + // ensure that the wrapper is present + if (wrapper == null) { + throw new ModuleDependencyNotFoundException(dependency.name(), moduleConfiguration.name()); + } + } + // create the class loader for the module + var loader = new ModuleURLClassLoader(url, dependencies.first(), moduleLayer); + loader.registerGlobally(); + // try to load and create the main class instance + var mainModuleClass = loader.loadClass(moduleConfiguration.main()); + // check if the main class is an instance of the IModule class + if (!Module.class.isAssignableFrom(mainModuleClass)) { + throw new AssertionError( + String.format("Module main class %s is not assignable from %s", mainModuleClass.getCanonicalName(), + Module.class.getCanonicalName())); + } + + // create an instance of the class and the main module wrapper + var moduleInstance = (Module) moduleLayer.instance(mainModuleClass); + var moduleWrapper = new DefaultModuleWrapper(url, moduleInstance, dataDirectory, this, loader, + dependencies.second(), moduleConfiguration, moduleLayer); + // initialize the module instance now + moduleInstance.init(loader, moduleWrapper, moduleConfiguration); + // register the module, load it and return the created wrapper + this.modules.add(moduleWrapper); + return moduleWrapper.loadModule(); + } + + /** + * Same as {@link #findModuleConfiguration(URL)} with additional checks: + *
    + *
  • the {@code moduleFile} must exist
  • + *
  • there must be no other module loaded from the same url
  • + *
  • the module must support the current java version
  • + *
+ * + * @param moduleFile the module file to find the module configuration of. + * @return the deserialized module configuration file located in the provided module file, or an empty Optional if a + * check failed. + * @throws ModuleConfigurationNotFoundException if the file doesn't contain a module.json. + * @throws IOException if an I/O or deserialize exception occurs. + * @throws NullPointerException if the given module file is null. + */ + protected @NonNull Optional loadModuleConfigurationIfValid(@NonNull URL moduleFile) + throws IOException, URISyntaxException { + // check if there is any other module loaded from the same url + if (this.findModuleBySource(moduleFile).isPresent()) { + return Optional.empty(); + } + // check if we can load the module configuration from the file + var moduleConfiguration = this.findModuleConfiguration(moduleFile).orElse(null); + if (moduleConfiguration == null) { + throw new ModuleConfigurationNotFoundException(moduleFile); + } + // check if the module can run on the current java version release. + if (!moduleConfiguration.canRunOn(JavaVersion.runtimeVersion())) { + LOGGER.warn( + "Unable to load module {}:{} because it only supports Java {}+", + moduleConfiguration.group(), + moduleConfiguration.name(), + moduleConfiguration.minJavaVersionId()); + return Optional.empty(); + } + return Optional.of(moduleConfiguration); + } + /** * Finds the module.json file in the provided module file and deserializes it. * diff --git a/driver/src/main/java/eu/cloudnetservice/driver/module/ModuleCyclicDependenciesException.java b/driver/src/main/java/eu/cloudnetservice/driver/module/ModuleCyclicDependenciesException.java new file mode 100644 index 0000000000..8e010ae887 --- /dev/null +++ b/driver/src/main/java/eu/cloudnetservice/driver/module/ModuleCyclicDependenciesException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019-2024 CloudNetService team & contributors + * + * 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 + * + * 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 eu.cloudnetservice.driver.module; + +import lombok.NonNull; + +/** + * Represents an exception thrown when there are cyclic dependencies between modules + * + * @since 4.0 + */ +public class ModuleCyclicDependenciesException extends RuntimeException { + + private final String[] dependencyPath; + + public ModuleCyclicDependenciesException(@NonNull String[] dependencyPath) { + this.dependencyPath = dependencyPath; + } + + @Override + public String getMessage() { + return "Cyclic dependencies detected: " + String.join(" -> ", this.dependencyPath); + } + + public @NonNull String[] dependencyPath() { + return this.dependencyPath; + } +} diff --git a/driver/src/main/java/eu/cloudnetservice/driver/module/ModuleDependencyOutdatedException.java b/driver/src/main/java/eu/cloudnetservice/driver/module/ModuleDependencyOutdatedException.java index b2dd6b2baa..4f6bcc9898 100644 --- a/driver/src/main/java/eu/cloudnetservice/driver/module/ModuleDependencyOutdatedException.java +++ b/driver/src/main/java/eu/cloudnetservice/driver/module/ModuleDependencyOutdatedException.java @@ -38,7 +38,29 @@ public class ModuleDependencyOutdatedException extends RuntimeException { * @throws NullPointerException if requiringModule, dependency or semverIndex is null. */ public ModuleDependencyOutdatedException( - @NonNull ModuleWrapper requiringModule, + @NonNull ModuleConfiguration requiringModule, + @NonNull ModuleDependency dependency, + @NonNull String semverIndex, + int required, + int actual + ) { + this(requiringModule.group(), requiringModule.name(), dependency, semverIndex, required, actual); + } + + /** + * Creates a new instance of this ModuleDependencyOutdatedException. + * + * @param requiringModuleGroup the group of the module which requires the dependency. + * @param requiringModuleName the name of the module which requires the dependency. + * @param dependency the dependency which is outdated. + * @param semverIndex the semver index name: major, minor, patch + * @param required the required version of the semver index. + * @param actual the actual running version of the semver index. + * @throws NullPointerException if requiringModuleGroup, requiringModuleName, dependency or semverIndex is null. + */ + public ModuleDependencyOutdatedException( + @NonNull String requiringModuleGroup, + @NonNull String requiringModuleName, @NonNull ModuleDependency dependency, @NonNull String semverIndex, int required, @@ -46,11 +68,12 @@ public ModuleDependencyOutdatedException( ) { super(String.format( "Module %s:%s requires minimum %s version %d of %s:%s but is currently %d", - requiringModule.module().group(), requiringModule.module().name(), + requiringModuleGroup, requiringModuleName, semverIndex, required, dependency.group(), dependency.name(), actual )); } + } diff --git a/driver/src/main/java/eu/cloudnetservice/driver/module/ModuleProvider.java b/driver/src/main/java/eu/cloudnetservice/driver/module/ModuleProvider.java index 8a0b6e4b91..7f20fb7765 100644 --- a/driver/src/main/java/eu/cloudnetservice/driver/module/ModuleProvider.java +++ b/driver/src/main/java/eu/cloudnetservice/driver/module/ModuleProvider.java @@ -125,12 +125,12 @@ public interface ModuleProvider { * * @param url the url to load the module from. * @return the loaded module or null if checks failed or a module from this url is already loaded. - * @throws ModuleConfigurationNotFoundException if the file associated with the url doesn't contain a - * module.json. - * @throws NullPointerException if required properties are missing in dependency or repository - * information. - * @throws AssertionError if any exception occurs during the load of the module. - * @throws NullPointerException if url is null. + * @throws ModuleConfigurationNotFoundException if the file associated with the url doesn't contain a module.json. + * @throws ModuleDependencyNotFoundException if a dependency module is not loaded. + * @throws NullPointerException if required properties are missing in dependency or repository + * information. + * @throws AssertionError if any exception occurs during the load of the module. + * @throws NullPointerException if url is null. */ @Nullable ModuleWrapper loadModule(@NonNull URL url); @@ -139,12 +139,11 @@ public interface ModuleProvider { * * @param path the path to load the module from. * @return the loaded module or null if checks failed or a module from this path is already loaded. - * @throws ModuleConfigurationNotFoundException if the file associated with the url doesn't contain a - * module.json. - * @throws NullPointerException if required properties are missing in dependency or repository - * information. - * @throws AssertionError if any exception occurs during the load of the module. - * @throws NullPointerException if path is null. + * @throws ModuleConfigurationNotFoundException if the file associated with the url doesn't contain a module.json. + * @throws NullPointerException if required properties are missing in dependency or repository + * information. + * @throws AssertionError if any exception occurs during the load of the module. + * @throws NullPointerException if path is null. * @see #loadModule(URL) */ @Nullable ModuleWrapper loadModule(@NonNull Path path); @@ -153,10 +152,11 @@ public interface ModuleProvider { * Loads all modules which files are located at the module directory. * * @return the same instance of the class, for chaining. + * @throws ModuleCyclicDependenciesException if cyclic dependencies are detected * @see ModuleWrapper#moduleLifeCycle() * @see ModuleLifeCycle#canChangeTo(ModuleLifeCycle) */ - @NonNull ModuleProvider loadAll(); + @NonNull ModuleProvider loadAll() throws ModuleCyclicDependenciesException; /** * Starts all modules which are loaded by this provided and can change to the started state. diff --git a/driver/src/main/java/eu/cloudnetservice/driver/module/util/ModuleDependencyUtil.java b/driver/src/main/java/eu/cloudnetservice/driver/module/util/ModuleDependencyUtil.java index b11524d222..c7a2201f99 100644 --- a/driver/src/main/java/eu/cloudnetservice/driver/module/util/ModuleDependencyUtil.java +++ b/driver/src/main/java/eu/cloudnetservice/driver/module/util/ModuleDependencyUtil.java @@ -17,6 +17,7 @@ package eu.cloudnetservice.driver.module.util; import com.google.common.base.Preconditions; +import eu.cloudnetservice.driver.module.ModuleConfiguration; import eu.cloudnetservice.driver.module.ModuleDependency; import eu.cloudnetservice.driver.module.ModuleDependencyNotFoundException; import eu.cloudnetservice.driver.module.ModuleDependencyOutdatedException; @@ -77,7 +78,7 @@ private ModuleDependencyUtil() { visitedNodes.add(caller); // we iterate over the root layer here to collect the first layer of dependencies of the module for (var dependingModule : caller.dependingModules()) { - var wrapper = associatedModuleWrapper(dependingModule, moduleProvider, caller); + var wrapper = associatedModuleWrapper(dependingModule, moduleProvider, caller.moduleConfiguration()); // register the module as a root dependency of the calling module rootDependencyNodes.add(wrapper); // now we visit every dependency of the module giving in a new tree to build @@ -109,7 +110,7 @@ private static void visitDependencies( @NonNull ModuleProvider moduleProvider ) { for (var dependency : dependencies) { - var wrapper = associatedModuleWrapper(dependency, moduleProvider, dependencyHolder); + var wrapper = associatedModuleWrapper(dependency, moduleProvider, dependencyHolder.moduleConfiguration()); // now verify that there is no circular dependency to the original caller Preconditions.checkArgument( !wrapper.module().name().equals(originalSource.module().name()), @@ -137,22 +138,38 @@ private static void visitDependencies( private static @NonNull ModuleWrapper associatedModuleWrapper( @NonNull ModuleDependency dependency, @NonNull ModuleProvider provider, - @NonNull ModuleWrapper dependencyHolder + @NonNull ModuleConfiguration dependencyHolder ) { var wrapper = provider.module(dependency.name()); // ensure that the wrapper is present if (wrapper == null) { - throw new ModuleDependencyNotFoundException(dependency.name(), dependencyHolder.module().name()); + throw new ModuleDependencyNotFoundException(dependency.name(), dependencyHolder.name()); } + checkDependencyVersion(dependencyHolder, wrapper.moduleConfiguration(), dependency); + return wrapper; + } + + /** + * Validates that the given {@code presentDependency} is a valid version for the {@code declaredDependency} + * + * @param requiringModule the module that requires the dependency + * @param presentDependency the module configuration of the dependency that is present + * @param declaredDependency the declared dependency to validate the version + * @throws ModuleDependencyOutdatedException if the module configuration is not valid for the declared dependency + */ + public static void checkDependencyVersion( + @NonNull ModuleConfiguration requiringModule, + @NonNull ModuleConfiguration presentDependency, + @NonNull ModuleDependency declaredDependency + ) throws ModuleDependencyOutdatedException { // try to make a semver check - var dependencyVersion = SEMVER_PATTERN.matcher(dependency.version()); - var moduleVersion = SEMVER_PATTERN.matcher(wrapper.module().version()); + var dependencyVersion = SEMVER_PATTERN.matcher(declaredDependency.version()); + var moduleVersion = SEMVER_PATTERN.matcher(presentDependency.version()); // check if both of the matchers had at least one match if (dependencyVersion.matches() && moduleVersion.matches()) { // assert that the versions are compatible - checkDependencyVersion(dependencyHolder, dependency, dependencyVersion, moduleVersion); + checkDependencyVersion(requiringModule, declaredDependency, dependencyVersion, moduleVersion); } - return wrapper; } /** @@ -169,11 +186,11 @@ private static void visitDependencies( * null. */ private static void checkDependencyVersion( - @NonNull ModuleWrapper requiringModule, + @NonNull ModuleConfiguration requiringModule, @NonNull ModuleDependency dependency, @NonNull Matcher dependencyVersion, @NonNull Matcher moduleVersion - ) { + ) throws ModuleDependencyOutdatedException { // extract both major versions var moduleMajor = Integer.parseInt(moduleVersion.group(1)); var dependencyMajor = Integer.parseInt(dependencyVersion.group(1)); diff --git a/driver/src/test/java/eu/cloudnetservice/driver/module/ModuleDependencyUtilTest.java b/driver/src/test/java/eu/cloudnetservice/driver/module/ModuleDependencyUtilTest.java index 329ff69308..26a03b3c88 100644 --- a/driver/src/test/java/eu/cloudnetservice/driver/module/ModuleDependencyUtilTest.java +++ b/driver/src/test/java/eu/cloudnetservice/driver/module/ModuleDependencyUtilTest.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.mockito.Mockito; +import org.mockito.stubbing.Answer; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class ModuleDependencyUtilTest { @@ -110,9 +111,18 @@ private ModuleWrapper mockModule(ModuleProvider pro, String name, String version var moduleWrapper = Mockito.mock(ModuleWrapper.class); Mockito.when(moduleWrapper.moduleProvider()).thenReturn(pro); Mockito.when(moduleWrapper.module()).thenReturn(mockedModule); + Mockito.when(moduleWrapper.moduleConfiguration()).then(this.mockModuleConfigurationAnswer(name, version)); mod.accept(moduleWrapper); return moduleWrapper; } + + /** + * We need this because we can't mock ModuleConfiguration with Mockito + */ + private Answer mockModuleConfigurationAnswer(String name, String version) { + return invocation -> new ModuleConfiguration(false, false, "eu.cloudnet", name, version, "", null, null, null, null, + null, null, 0, null); + } }