/*
* 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);
}
}
}