diff --git a/base/src/main/java/de/staropensource/sosengine/base/utility/DependencyResolver.java b/base/src/main/java/de/staropensource/sosengine/base/utility/DependencyResolver.java index df3c173e..be8f5ab1 100644 --- a/base/src/main/java/de/staropensource/sosengine/base/utility/DependencyResolver.java +++ b/base/src/main/java/de/staropensource/sosengine/base/utility/DependencyResolver.java @@ -19,15 +19,17 @@ package de.staropensource.sosengine.base.utility; +import de.staropensource.sosengine.base.classes.VersioningSystem; import de.staropensource.sosengine.base.exceptions.UnexpectedThrowableException; import de.staropensource.sosengine.base.exceptions.dependency.UnmetDependenciesException; +import de.staropensource.sosengine.base.logging.LoggerInstance; import de.staropensource.sosengine.base.types.DependencyVector; -import de.staropensource.sosengine.base.types.immutable.ImmutableArrayList; -import de.staropensource.sosengine.base.types.immutable.ImmutableLinkedList; import lombok.Getter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.lang.reflect.InvocationTargetException; import java.util.*; /** @@ -38,18 +40,33 @@ import java.util.*; @SuppressWarnings({ "unused", "UnusedReturnValue", "JavadocDeclaration" }) public final class DependencyResolver { /** - * A list of {@link DependencyVector}s. + * Contains the {@link LoggerInstance} for this instance. + * + * @see LoggerInstance + * @since v1-alpha4 + */ + private final @NotNull LoggerInstance logger = new LoggerInstance.Builder().setClazz(getClass()).setOrigin("ENGINE").setMetadata(String.valueOf(hashCode())).build(); + + /** + * List of {@link DependencyVector}s to resolve. * * @since v1-alpha1 */ - List vectors = new ArrayList<>(); + Set vectors = new HashSet<>(); /** - * {@code true} if the current {@link DependencyVector} list has been resolved successfully. + * List of identifiers of already resolved vectors. + * + * @since v1-alpha4 + */ + Set<@NotNull String> vectorsResolved = new HashSet<>(); + + /** + * Contains whether the current {@link DependencyVector} list has been resolved successfully. * * @since v1-alpha1 * -- GETTER -- - * Returns {@code true} if the current {@link DependencyVector} list has been resolved successfully. + * Returns whether the current {@link DependencyVector} list has been resolved successfully. * * @return resolved status * @since v1-alpha1 @@ -72,7 +89,9 @@ public final class DependencyResolver { * @since v1-alpha1 */ public synchronized DependencyResolver addVector(@NotNull DependencyVector vector) { - vectors.add(vector); + try { + vectors.add(vector); + } catch (IllegalArgumentException ignored) {} resolved = false; return this; } @@ -84,10 +103,8 @@ public final class DependencyResolver { * @return itself * @since v1-alpha1 */ - public synchronized DependencyResolver addVectors(@NotNull DependencyVector[] vectors) { - addVectors(Arrays.stream(vectors).toList()); - resolved = false; - return this; + public DependencyResolver addVectors(@NotNull DependencyVector[] vectors) { + return addVectors(Arrays.stream(vectors).toList()); } /** @@ -97,61 +114,9 @@ public final class DependencyResolver { * @return itself * @since v1-alpha1 */ - public synchronized DependencyResolver addVectors(@NotNull Collection vectors) { - this.vectors.addAll(vectors); - resolved = false; - return this; - } - - /** - * Adds multiple dependency vectors. - * - * @param vectors dependency vectors to add - * @return itself - * @since v1-alpha1 - */ - public synchronized DependencyResolver addVectors(@NotNull List vectors) { - this.vectors.addAll(vectors); - resolved = false; - return this; - } - - /** - * Adds multiple dependency vectors. - * - * @param vectors dependency vectors to add - * @return itself - * @since v1-alpha1 - */ - public synchronized DependencyResolver addVectors(@NotNull ImmutableArrayList vectors) { - this.vectors.addAll(vectors); - resolved = false; - return this; - } - - /** - * Adds multiple dependency vectors. - * - * @param vectors dependency vectors to add - * @return itself - * @since v1-alpha1 - */ - public synchronized DependencyResolver addVectors(@NotNull ImmutableLinkedList vectors) { - this.vectors.addAll(vectors); - resolved = false; - return this; - } - - /** - * Adds multiple dependency vectors. - * - * @param vectors dependency vectors to add - * @return itself - * @since v1-alpha1 - */ - public synchronized DependencyResolver addVectors(@NotNull Set vectors) { - this.vectors.addAll(vectors); - resolved = false; + public DependencyResolver addVectors(@NotNull Collection vectors) { + for (DependencyVector vector : vectors) // thread-safety + addVector(vector); return this; } @@ -161,100 +126,252 @@ public final class DependencyResolver { * * @return itself * @throws UnmetDependenciesException when dependencies are unmet + * @throws IllegalStateException when encountering an invalid dependency or provider * @throws UnexpectedThrowableException when some unknown error occurs * @since v1-alpha1 */ - @SuppressWarnings("JavaReflectionInvocation") public synchronized DependencyResolver resolve() throws UnmetDependenciesException, UnexpectedThrowableException { Map unmetDependencies = new HashMap<>(); - resolved = false; - try { - for (DependencyVector vector : vectors) - for (String dependency : vector.getDependencies()) { - int match = 0; - - if (dependency.contains("=")) { - String dependencyIdentifier = dependency.substring(0, dependency.indexOf("=")); - String dependencyVersion = dependency.substring(dependency.indexOf("=")); - - for (DependencyVector vectorCheck : vectors) - if (vectorCheck.getIdentifier().equals(dependency)) { - if (vectorCheck.getVersioningSystem().getDeclaredConstructor().newInstance(dependencyVersion).compare(vectorCheck.getVersioningSystem().getDeclaredConstructor().newInstance(vectorCheck.getVersion())) == 1) - match = -1; - else - match = 2; - - break; - } - - if (match == 0) - unmetDependencies.put(vector, "Depends on '" + dependencyIdentifier + "', which is missing"); - else { - unmetDependencies.put(vector, "Depends exactly on '" + dependency + "', which is not installed"); - } - } else if (dependency.contains("<")) { - String dependencyIdentifier = dependency.substring(0, dependency.indexOf("<")); - String dependencyVersion = dependency.substring(dependency.indexOf("<")); - - for (DependencyVector vectorCheck : vectors) - if (vectorCheck.getIdentifier().equals(dependency)) { - if (vectorCheck.getVersioningSystem().getDeclaredConstructor().newInstance(dependencyVersion).compare(vectorCheck.getVersioningSystem().getDeclaredConstructor().newInstance(vectorCheck.getVersion())) == 0) - match = -1; - else - match = 2; - - break; - } - - if (match == 0) - unmetDependencies.put(vector, "Depends on '" + dependencyIdentifier + "', which is missing"); - else { - unmetDependencies.put(vector, "Depends at maximum on '" + dependency + "', which is not installed"); - } - } else if (dependency.contains(">")) { - String dependencyIdentifier = dependency.substring(0, dependency.indexOf(">")); - String dependencyVersion = dependency.substring(dependency.indexOf(">")); - - for (DependencyVector vectorCheck : vectors) - if (vectorCheck.getIdentifier().equals(dependency)) { - if (vectorCheck.getVersioningSystem().getDeclaredConstructor().newInstance(dependencyVersion).compare(vectorCheck.getVersioningSystem().getDeclaredConstructor().newInstance(vectorCheck.getVersion())) == 2) - match = -1; - else - match = 2; - - break; - } - - if (match == 0) - unmetDependencies.put(vector, "Depends on '" + dependencyIdentifier + "', which is missing"); - else { - unmetDependencies.put(vector, "Depends at minimum on '" + dependency + "', which is not installed"); - } - } else { - for (DependencyVector vectorCheck : vectors) - if (vectorCheck.getIdentifier().equals(dependency)) { - match = -1; - break; - } - - if (match == 0) - unmetDependencies.put(vector, "Depends on any version of '" + dependency + "', which is missing"); - } - } - } catch (Exception exception) { - // Throw UnexpectedThrowableException when something horribly fails - throw new UnexpectedThrowableException(exception); + for (DependencyVector vector : vectors) { + if (!vectorsResolved.contains(vector.getIdentifier())) { + resolveVector(vector); + vectorsResolved.add(vector.getIdentifier()); + } } - // Check for any unmet dependencies - if (!unmetDependencies.isEmpty()) - throw new UnmetDependenciesException(unmetDependencies); - resolved = true; return this; } + /** + * Resolves a vector. + * Throws an exception when detecting an unmet dependency or a dependency cycle. + * + * @return list of unmet dependencies + * @throws IllegalStateException when encountering an invalid dependency or provider + * @throws Exception when some unknown error occurs + * @since v1-alpha4 + */ + private @NotNull List<@NotNull String> resolveVector(@NotNull DependencyVector vector) throws IllegalStateException { + List<@NotNull String> unmetDependencies = new ArrayList<>(); + + // provides + + for (String dependency : vector.getDependencies()) { + // 0 = identifier + // 1 = version equal + // 2 = version smaller + // 3 = version bigger + int mode = 0; + boolean[] duplicateCheck = new boolean[3]; + StringBuilder identifier = new StringBuilder(); + StringBuilder versionEqual = new StringBuilder(); + StringBuilder versionSmaller = new StringBuilder(); + StringBuilder versionBigger = new StringBuilder(); + + // Get variables + for (char character : dependency.toCharArray()) { + switch (character) { + case '=' -> { + mode = 1; + continue; + } + case '<' -> { + mode = 2; + continue; + } + case '>' -> { + mode = 3; + continue; + } + } + + switch (mode) { + // Identifier + case 0 -> identifier.append(character); + // Version equal + case 1 -> { + // Check for duplicate + if (character == '=' && duplicateCheck[0]) + throw new IllegalStateException("The dependency listing \"" + dependency + "\" cannot include multiple '=' characters"); + duplicateCheck[0] = true; + + // Check for smaller and bigger than + if (duplicateCheck[1] || duplicateCheck[2]) + throw new IllegalStateException("The dependency listing \"" + dependency + "\" cannot require a specific version and have minimum and maximum version specifiers"); + + versionEqual.append(character); + } + // Version smaller + case 2 -> { + // Check for duplicate + if (character == '<' && duplicateCheck[1]) + throw new IllegalStateException("The dependency listing \"" + dependency + "\" cannot include multiple '<' characters"); + duplicateCheck[1] = true; + + // Check for equal + if (duplicateCheck[0]) + throw new IllegalStateException("The dependency listing \"" + dependency + "\" cannot require a specific version and have minimum and maximum version specifiers"); + + versionSmaller.append(character); + } + // Version bigger + case 3 -> { + // Check for duplicate + if (character == '>' && duplicateCheck[2]) + throw new IllegalStateException("The dependency listing \"" + dependency + "\" cannot include multiple '>' characters"); + duplicateCheck[2] = true; + + // Check for equal + if (duplicateCheck[0]) + throw new IllegalStateException("The dependency listing \"" + dependency + "\" cannot require a specific version and have minimum and maximum version specifiers"); + + versionBigger.append(character); + } + } + } + + // Resolve vector + DependencyVector dependencyResolved = getMatchingVector(identifier.toString()); + if (dependencyResolved == null) { + unmetDependencies.add("Dependency \"" + dependency + "\" is not met: Not found"); + continue; + } + + VersioningSystem versioningSystemResolved; + + // Get resolved versioning system + try { + versioningSystemResolved = dependencyResolved.getVersioningSystem().getDeclaredConstructor(String.class).newInstance(dependencyResolved.getVersion()); + } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException exception) { + logger.crash("Unable to check version of dependency \"" + dependency + "\": Unable to initialize versioning system " + dependencyResolved.getVersioningSystem().getName(), exception); + break; + } + + // Compare + if (!versionEqual.isEmpty()) { // Version equals + VersioningSystem versioningSystemEquals; + + // Get expected VersioningSystem + try { + versioningSystemEquals = dependencyResolved.getVersioningSystem().getDeclaredConstructor(String.class).newInstance(versionEqual.toString()); + } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException exception) { + logger.crash("Unable to check version of dependency \"" + dependency + "\": Unable to initialize versioning system " + dependencyResolved.getVersioningSystem().getName(), exception); + break; + } + + // Compare versions + if (versioningSystemResolved.compare(versioningSystemEquals) != 1) + unmetDependencies.add("Dependency \"" + dependency + "\" is not met: Expected version " + versionEqual + " does not match found version " + vector.getVersion()); + } else { + VersioningSystem versioningSystemSmaller = null; + VersioningSystem versioningSystemBigger = null; + + if (!versionSmaller.isEmpty()) + // Get expected VersioningSystem + try { + versioningSystemSmaller = dependencyResolved.getVersioningSystem().getDeclaredConstructor(String.class).newInstance(versionSmaller.toString()); + } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException exception) { + logger.crash("Unable to check version of dependency \"" + dependency + "\": Unable to initialize versioning system " + dependencyResolved.getVersioningSystem().getName(), exception); + break; + } + if (!versionBigger.isEmpty()) + // Get expected VersioningSystem + try { + versioningSystemBigger = dependencyResolved.getVersioningSystem().getDeclaredConstructor(String.class).newInstance(versionBigger.toString()); + } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException exception) { + logger.crash("Unable to check version of dependency \"" + dependency + "\": Unable to initialize versioning system " + dependencyResolved.getVersioningSystem().getName(), exception); + break; + } + + // Compare versions + if (versioningSystemSmaller != null && versioningSystemBigger != null) { + if (versioningSystemResolved.compare(versioningSystemSmaller) != 0 && versioningSystemResolved.compare(versioningSystemBigger) != 2) + unmetDependencies.add("Dependency \"" + dependency + "\" is not met: Version " + vector.getVersion() + " is not in range " + versionSmaller + " to " + versionBigger); + } else { + if (versioningSystemSmaller != null) + if (versioningSystemResolved.compare(versioningSystemSmaller) != 0) + unmetDependencies.add("Dependency \"" + dependency + "\" is not met: Version " + vector.getVersion() + " is bigger than " + versionSmaller); + if (versioningSystemBigger != null) + if (versioningSystemResolved.compare(versioningSystemBigger) != 2) + unmetDependencies.add("Dependency \"" + dependency + "\" is not met: Version " + vector.getVersion() + " is smaller than " + versionBigger); + } + } + + /* + if (dependency.contains("=")) { + // Check for '<' and '>' + if (dependency.contains("<") || dependency.contains(">")) + throw new IllegalStateException("The dependency listing \"" + dependency + "\" can't require a specific version and have minimum and maximum version specifiers"); + + // Check for multiple '=' + if (dependency.split("\\\\=").length != 1) + throw new IllegalStateException("The dependency listing \"" + dependency + "\" can't include multiple equals characters"); + + // Get identifier and required version + int index = dependency.indexOf("="); + String identifier = dependency.substring(0, index); + String version = dependency.substring(index + 1); + + DependencyVector dependencyResolved = getMatchingVector(identifier); + if (dependencyResolved == null) + unmetDependencies.add("Dependency \"" + dependency + "\" is not met: Not found"); + else { + VersioningSystem versioningSystemResolved; + VersioningSystem versioningSystemEquals; + + // Create VersioningSystem instances for comparing versions + try { + versioningSystemResolved = dependencyResolved.getVersioningSystem().getDeclaredConstructor(String.class).newInstance(dependencyResolved.getVersion()); + } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | + InvocationTargetException exception) { + logger.crash("Unable to check version of dependency \"" + dependency + "\": Unable to initialize versioning system " + dependencyResolved.getVersioningSystem().getName(), exception); + break; + } + try { + versioningSystemEquals = dependencyResolved.getVersioningSystem().getDeclaredConstructor(String.class).newInstance(version); + } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | + InvocationTargetException exception) { + logger.crash("Unable to initialize versioning system " + vector.getVersioningSystem().getName() + " of vector " + vector.getIdentifier(), exception); + break; + } + + // Compare versions + if (versioningSystemResolved.compare(versioningSystemEquals) != 1) + unmetDependencies.add("Dependency \"" + dependency + "\" is not met: Expected version " + version + " does not match found version " + vector.getVersion()); + } + } else { + throw new IllegalStateException("The dependency listing \"" + dependency + "\" does not contain a version identifier"); + } + */ + } + + return unmetDependencies; + } + + /** + * Searches all registered {@link DependencyVector}s for the specified identifier + * and returns the first matching one. + * + * @return matching vector or {@code null} if not found + * @since v1-alpha4 + */ + private @Nullable DependencyVector getMatchingVector(@NotNull String identifier) { + for (DependencyVector vector : vectors) { + if (vector.getIdentifier().equals(identifier)) { + return vector; + } else { + // Search 'provides' for matches + for (String provider : vector.getProvides()) + if (provider.substring(0, provider.indexOf("=")).equals(identifier)) + return vector; + } + } + + return null; + } + /** * Returns the correct order which stuff needs to be loaded/done in. *