blob: a932b052f92a4142a97b14111f7e23c2a46f51b4 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015 Manumitting Technologies Inc and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Brian de Alwis (MTI) - initial API and implementation
* Lars Vogel <Lars.Vogel@vogella.com> - Bug 472654
******************************************************************************/
package org.eclipse.e4.ui.internal.workbench;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* This class implements a topological sort. The implementation distinguishes between the objects to
* be sorted from their corresponding dependency identifier, and properly tracks where multiple
* objects have the same identifier. This is required for sorting Eclipse Extension Registry
* extensions by their plug-in dependencies, as a plug-in may contribute multiple sets of
* extensions.
*
* <p>
* The implementation creates a dependency graph where the vertices represent the object identifiers
* and the edges represent the required-relation between them. Subclasses must implement of
* {@link #getRequirements(Object)} and {@link #getDependencies(Object)}. Although these are
* normally symmetric operations (i.e., if <em>B</em> is a requirement of <em>A</em>, then
* <em>A</em> is a dependent of <em>B</em>), one may be easier to compute than the other in some
* contexts and so this class combines the results.
* </p>
*
* <p>
* Description of the algorithm:
* </p>
* <ol>
* <li>Construct the dependency graph with {@link #getRequirements(Object)} and
* {@link #getDependencies(Object)}. The out edges represent requirements, and in edges represent
* dependencies. This graph may be disconnected if there are nodes with no dependencies amongst each
* other.</li>
* <li>Sort the list of identifiers by their out-degree, and then by in-degree. Ids with out-degree
* 0 are roots; if there is no id with out-degree 0 then we have a cycle. We sort nodes with the
* same out-degree by their in-degree to privilege nodes within the cycle from those that are
* dependencies (e.g., consider: A &rarr; B, B &rarr; C, C &rarr; A, D &rarr; A; all have out-degree
* 1, but A has in-degree 2, and A must be output before D).</li>
* <li>While there are ids left to consider, take the id with lowest out-degree and add its objects
* to the list. Remove the id from all of its dependents' required lists. This removal changes the
* out-degree and may require that the id list be resorted since some of the dependents may now be
* roots. Note that should we encounter a cycle (i.e., no nodes of degree 0), we process all the
* bundles within the cycle, since any dependencies to any single ndoe within the cycle effectively
* extends to all.</li>
* </ol>
*
* @param <T>
* the type of objects being sorted
* @param <ID>
* the type of the object identifiers; multiple objects of T may correspond to the same
* ID
*/
public abstract class TopologicalSort<T, ID> {
private final Map<ID, Collection<T>> mappedObjects = new LinkedHashMap<>();
// Captures the bundles that are listed as requirements for a particular bundle.
private final Map<ID, Collection<ID>> requires = new LinkedHashMap<>();
// Captures the bundles that list a particular bundle as a requirement
private final Map<ID, Collection<ID>> depends = new LinkedHashMap<>();
/**
* Return the identifier for the given object. The implementation properly tracks where multiple
* objects have the same identifier.
*
* @param object
* the object
* @return the identifier
*/
protected abstract ID getId(T object);
/**
* Return the list of IDs required by {@code id}. Implementors may choose to return {@code null}
* or an empty collection if this information is more easily computed by
* {@link #getDependencies(Object)}.
*
* @param id
* the id
* @return the IDs required by {@code id}; may be {@code null}
*/
protected abstract Collection<ID> getRequirements(ID id);
/**
* Return the list of IDs depended upon by {@code id}. Implementors may choose to return
* {@code null} or an empty collection if this information is more easily computed by
* {@link #getRequirements(Object)}.
*
* @param id
* the id
* @return the IDs depended upon by {@code id}; may be {@code null}
*/
protected abstract Collection<ID> getDependencies(ID id);
/**
* Sort the provided extensions by the dependencies of their contributors. Note that sorting is
* done in-place.
*
* @param objects
* the objects to be sorted
* @return the objects in a topologically-sorted order
*/
public T[] sort(T[] objects) {
if (objects.length <= 1) {
return objects;
}
addAll(objects);
return process(objects);
}
/** Place results in results and return results */
private T[] process(T[] results) {
buildDependencyGraph();
// Sort nodes by out-degree ({@code requires}) and then by in-degree.
// There are two situations: we have disconnected graphs
// In case of a cycle, one of the nodes involved in the cycle should have
// higher in-degree from some other non-cyclic node
int resultsIndex = 0;
List<ID> sortedByOutdegree = new ArrayList<>(requires.keySet());
Comparator<ID> outdegreeSorter = new Comparator<ID>() {
@Override
public int compare(ID o1, ID o2) {
assert requires.containsKey(o1) && requires.containsKey(o2);
int comparison = requires.get(o1).size() - requires.get(o2).size();
if (comparison == 0) {
// Select the node whose removal would have the greatest effect
return depends.get(o2).size() - depends.get(o1).size();
}
return comparison;
}
};
Collections.sort(sortedByOutdegree, outdegreeSorter);
while (!sortedByOutdegree.isEmpty()) {
// don't sort unnecessarily: the current ordering is fine providing
// item #0 still has no dependencies
if (!requires.get(sortedByOutdegree.get(0)).isEmpty()) {
Collections.sort(sortedByOutdegree, outdegreeSorter);
}
LinkedList<ID> cycleToBeDone = new LinkedList<>();
cycleToBeDone.add(sortedByOutdegree.remove(0));
while (!cycleToBeDone.isEmpty()) {
ID bundleId = cycleToBeDone.removeFirst();
assert depends.containsKey(bundleId) && requires.containsKey(bundleId);
for (T ext : mappedObjects.get(bundleId)) {
results[resultsIndex++] = ext;
}
// requires.get(bundleId) is empty unless there's a cycle;
// we process all nodes within the cycle now
for (ID reqId : requires.get(bundleId)) {
cycleToBeDone.add(reqId);
sortedByOutdegree.remove(reqId);
depends.get(reqId).remove(bundleId);
}
requires.remove(bundleId);
for (ID depId : depends.get(bundleId)) {
requires.get(depId).remove(bundleId);
}
depends.remove(bundleId);
}
}
return results;
}
/**
* @param objects
*/
private void addAll(T[] objects) {
// first build up the list of object ids actually being considered
for (T o : objects) {
ID id = getId(o);
Collection<T> exts = mappedObjects.get(id);
if (exts == null) {
mappedObjects.put(id, exts = new LinkedHashSet<>());
}
exts.add(o);
}
}
private void buildDependencyGraph() {
// reset
requires.clear();
depends.clear();
for (ID id : mappedObjects.keySet()) {
requires.put(id, new LinkedHashSet<ID>());
depends.put(id, new LinkedHashSet<ID>());
}
// now populate the dependency graph
for (ID subjectId : mappedObjects.keySet()) {
assert requires.containsKey(subjectId) && depends.containsKey(subjectId);
Collection<ID> requirements = getRequirements(subjectId);
if (requirements != null) {
for (ID requiredId : requirements) {
assert !requiredId.equals(subjectId) : "self-cycles not supported"; //$NON-NLS-1$
// ignore objects not being sorted
if (!mappedObjects.containsKey(requiredId)) {
continue;
}
// So requiredId is a requirement for subjectId
// and subjectId is a dependent of requiredId
depends.get(requiredId).add(subjectId);
requires.get(subjectId).add(requiredId);
}
}
Collection<ID> dependencies = getDependencies(subjectId);
if (dependencies != null) {
for (ID dependentId : dependencies) {
assert !dependentId.equals(subjectId) : "self-cycles not supported"; //$NON-NLS-1$
// ignore objects not being sorted
if (!mappedObjects.containsKey(dependentId)) {
continue;
}
// So dependentId is a dependency of subjectId
// and subjectId is a requirement of dependentId
requires.get(dependentId).add(subjectId);
depends.get(subjectId).add(dependentId);
}
}
}
}
}