/* * Copyright 2015 MovingBlocks * * 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 org.terasology.module; import java.util.Arrays; import java.util.Collections; import java.util.Deque; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.terasology.module.dependencyResolution.OptionalResolutionStrategy; import org.terasology.naming.Name; import org.terasology.naming.Version; import org.terasology.naming.VersionRange; import org.terasology.util.collection.UniqueQueue; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Queues; import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; class ResolutionAttempt { private final OptionalResolutionStrategy optionalStrategy; private final ModuleRegistry registry; private Set<Name> rootModules; private SetMultimap<Name, PossibleVersion> moduleVersionPool; private ListMultimap<Name, Constraint> constraints; private UniqueQueue<Constraint> constraintQueue; ResolutionAttempt(ModuleRegistry registry, OptionalResolutionStrategy optionalStrategy) { this.registry = registry; this.optionalStrategy = optionalStrategy; } ResolutionResult resolve(Map<Name, Optional<VersionRange>> validVersions) { rootModules = ImmutableSet.copyOf(validVersions.keySet()); populateDomains(validVersions); populateConstraints(); if (!includesModules(rootModules)) { return new ResolutionResult(false, Collections.<Module>emptySet()); } constraintQueue = UniqueQueue.createWithExpectedSize(constraints.size()); constraintQueue.addAll(constraints.values()); processConstraints(); if (!includesModules(rootModules)) { return new ResolutionResult(false, Collections.<Module>emptySet()); } return new ResolutionResult(true, finaliseModules()); } /** * Populates the domains (modules of interest) for resolution. Includes all versions of all modules depended on by any version of a module of interest, recursively. */ private void populateDomains(Map<Name, Optional<VersionRange>> validVersions) { moduleVersionPool = HashMultimap.create(); Set<Name> involvedModules = Sets.newHashSet(); Deque<Name> moduleQueue = Queues.newArrayDeque(); for (Name rootModule : rootModules) { involvedModules.add(rootModule); moduleQueue.push(rootModule); } while (!moduleQueue.isEmpty()) { Name id = moduleQueue.pop(); for (Module version : registry.getModuleVersions(id)) { Optional<VersionRange> range = validVersions.getOrDefault(version.getId(), Optional.empty()); if (!range.isPresent() || range.get().contains(version.getVersion())) { moduleVersionPool.put(id, new PossibleVersion(version.getVersion())); for (DependencyInfo dependency : version.getMetadata().getDependencies()) { if (involvedModules.add(dependency.getId())) { moduleQueue.push(dependency.getId()); moduleVersionPool.put(dependency.getId(), PossibleVersion.OPTIONAL_VERSION); } } } } } } /** * Populates the constraints between the domains. For each module, any dependency that at least one version of the module has becomes a constraint * between the two, with a mapping of version to version-range. */ private void populateConstraints() { constraints = ArrayListMultimap.create(); for (Name name : moduleVersionPool.keySet()) { Set<Name> dependencies = Sets.newLinkedHashSet(); for (Module module : registry.getModuleVersions(name)) { dependencies.addAll(module.getMetadata().getDependencies().stream().map(DependencyInfo::getId).collect(Collectors.toList())); } for (Name dependency : dependencies) { Map<Version, CompatibleVersions> constraintTable = Maps.newHashMapWithExpectedSize(moduleVersionPool.get(name).size()); for (PossibleVersion version : moduleVersionPool.get(name)) { if (version.getVersion().isPresent()) { Module versionedModule = registry.getModule(name, version.getVersion().get()); DependencyInfo info = versionedModule.getMetadata().getDependencyInfo(dependency); if (info != null) { constraintTable.put(version.getVersion().get(), new CompatibleVersions(info.versionRange(), info.isOptional() && !optionalStrategy.isRequired())); } } } Constraint constraint = new VersionConstraint(name, dependency, constraintTable); constraints.put(name, constraint); constraints.put(dependency, constraint); } } } private boolean includesModules(Set<Name> modules) { for (Name module : modules) { if (moduleVersionPool.get(module).isEmpty()) { return false; } } return true; } /** * Processes queued constraints, until the queue is exhausted. */ private void processConstraints() { while (!constraintQueue.isEmpty() && includesModules(rootModules)) { Constraint constraint = constraintQueue.remove(); if (applyConstraintToDependency(constraint)) { for (Constraint relatedConstraint : constraints.get(constraint.getTo())) { if (!Objects.equals(relatedConstraint, constraint)) { constraintQueue.add(relatedConstraint); } } } if (applyConstraintToDependant(constraint)) { for (Constraint relatedConstraint : constraints.get(constraint.getFrom())) { constraintQueue.add(relatedConstraint); } } } } /** * Applies a constraint on dependencies based on the available versions of the dependant. A dependency version is removed if there is * no dependant that it is compatible with. * <p> * Example: if core-1.0.0 depends on child [1.0.0-2.0.0), then child-3.0.0 will be removed unless there is either another version of core that it is compatible with, * or a version of core with no dependency on it at all. * </p> * * @param constraint The constraint to process * @return Whether a change was applied the "to" domain of the constraint. */ private boolean applyConstraintToDependency(Constraint constraint) { return constraint.constrainTo(Collections.unmodifiableSet(moduleVersionPool.get(constraint.getFrom())), moduleVersionPool.get(constraint.getTo())); } /** * Applies a constraint on a dependant based on available versions of the dependencies. A dependant version is removed if there is no compatible dependency version * available to support it. * * @param constraint The constraint to process * @return Whether a change was applied to the "from" domain of the constraint */ private boolean applyConstraintToDependant(Constraint constraint) { return constraint.constrainFrom(moduleVersionPool.get(constraint.getFrom()), Collections.unmodifiableSet(moduleVersionPool.get(constraint.getTo()))); } /** * Taking the already constrained moduleVersionPool, works through the remaining possibilities restricting down to the latest possible versions. * <p> * Root modules are restricted first and in order, to keep their versions as recent as possible. * Dependencies are then followed, restricted them to latest as needed. * As dependencies are followed, any modules that aren't required by the finally selected versions will not be present in the final result. * </p> * * @return The final set of compatible modules. */ private Set<Module> finaliseModules() { Set<Module> finalModuleSet = Sets.newLinkedHashSetWithExpectedSize(moduleVersionPool.keySet().size()); Deque<Module> moduleQueue = Queues.newArrayDeque(); for (Name rootModule : rootModules) { Version latestVersion = reduceToFinalVersion(rootModule, true).get(); Module module = registry.getModule(rootModule, latestVersion); finalModuleSet.add(module); moduleQueue.push(module); } while (!moduleQueue.isEmpty()) { Module module = moduleQueue.pop(); for (DependencyInfo dependency : module.getMetadata().getDependencies()) { Optional<Version> latestVersion = reduceToFinalVersion(dependency.getId(), optionalStrategy.isDesired()); if (latestVersion.isPresent()) { Module dependencyModule = registry.getModule(dependency.getId(), latestVersion.get()); if (finalModuleSet.add(dependencyModule)) { moduleQueue.push(dependencyModule); } } } } return finalModuleSet; } /** * Reduces the available versions of the given module to just the latest remaining version, * and then processes constraints affected by this reduction. Should only be called of there is at least * one version available. * * @param module The module to limit to the latest version * @return The latest version of the module. */ private Optional<Version> reduceToFinalVersion(Name module, boolean includeIfOptional) { switch (moduleVersionPool.get(module).size()) { case 0: return Optional.empty(); case 1: return moduleVersionPool.get(module).iterator().next().getVersion(); default: PossibleVersion version; if (!includeIfOptional && moduleVersionPool.get(module).contains(PossibleVersion.OPTIONAL_VERSION)) { version = PossibleVersion.OPTIONAL_VERSION; } else { List<PossibleVersion> versions = Lists.newArrayList(moduleVersionPool.get(module)); Collections.sort(versions); version = versions.get(versions.size() - 1); } moduleVersionPool.replaceValues(module, Arrays.asList(version)); constraintQueue.addAll(constraints.get(module)); processConstraints(); return version.getVersion(); } } /** * Describes a constraint, in the form of a mapping of Versions of the "from" module to allowed ranges of the "to" modules. */ private interface Constraint { Name getFrom(); Name getTo(); boolean constrainFrom(Set<PossibleVersion> fromVersions, Set<PossibleVersion> toVersions); boolean constrainTo(Set<PossibleVersion> fromVersions, Set<PossibleVersion> toVersions); } /** * Describes a constraint, in the form of a mapping of Versions of the "from" module to allowed ranges of the "to" modules. */ private static final class VersionConstraint implements Constraint { private final Name from; private final Name to; private final Map<Version, CompatibleVersions> versionCompatibilities; private VersionConstraint(Name from, Name to, Map<Version, CompatibleVersions> versionCompatibilities) { this.from = from; this.to = to; this.versionCompatibilities = versionCompatibilities; } @Override public Name getFrom() { return from; } @Override public Name getTo() { return to; } @Override public boolean constrainFrom(Set<PossibleVersion> fromVersions, Set<PossibleVersion> toVersions) { boolean changed = false; Iterator<PossibleVersion> validVersions = fromVersions.iterator(); while (validVersions.hasNext()) { PossibleVersion version = validVersions.next(); if (version.getVersion().isPresent()) { CompatibleVersions compatibility = versionCompatibilities.get(version.getVersion().get()); if (compatibility != null) { boolean valid = false; for (PossibleVersion dependencyVersion : toVersions) { if (compatibility.isCompatible(dependencyVersion)) { valid = true; break; } } if (!valid) { validVersions.remove(); changed = true; } } } } return changed; } @Override public boolean constrainTo(Set<PossibleVersion> fromVersions, Set<PossibleVersion> toVersions) { boolean changed = false; Iterator<PossibleVersion> dependencyVersions = toVersions.iterator(); while (dependencyVersions.hasNext()) { PossibleVersion dependencyVersion = dependencyVersions.next(); boolean valid = false; for (PossibleVersion version : fromVersions) { if (version.getVersion().isPresent()) { CompatibleVersions compatibility = versionCompatibilities.get(version.getVersion().get()); if (compatibility == null || compatibility.isCompatible(dependencyVersion)) { valid = true; break; } } else { valid = true; } } if (!valid) { dependencyVersions.remove(); changed = true; } } return changed; } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof VersionConstraint) { VersionConstraint other = (VersionConstraint) obj; return Objects.equals(from, other.from) && Objects.equals(to, other.to); } return false; } @Override public int hashCode() { return Objects.hash(from, to); } @Override public String toString() { return from + "==>" + to; } } private static class CompatibleVersions { private final VersionRange versionRange; private final boolean missingAllowed; public CompatibleVersions(VersionRange versionRange, boolean missingAllowed) { this.versionRange = versionRange; this.missingAllowed = missingAllowed; } public boolean isCompatible(PossibleVersion version) { if (version.getVersion().isPresent()) { return versionRange.contains(version.getVersion().get()); } else { return missingAllowed; } } } private static class PossibleVersion implements Comparable<PossibleVersion> { public static final PossibleVersion OPTIONAL_VERSION = new PossibleVersion(); private final Optional<Version> version; private PossibleVersion() { version = Optional.empty(); } public PossibleVersion(Version version) { this.version = Optional.of(version); } public Optional<Version> getVersion() { return version; } @Override public int compareTo(PossibleVersion o) { if (!version.isPresent()) { if (o.version.isPresent()) { return -1; } return 0; } else { if (o.version.isPresent()) { return version.get().compareTo(o.version.get()); } return 1; } } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof PossibleVersion) { return compareTo((PossibleVersion) obj) == 0; } return false; } @Override public int hashCode() { return Objects.hash(version); } } }