-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'origin/master' into feature/secrets-pro…
…vider
- Loading branch information
Showing
3 changed files
with
264 additions
and
0 deletions.
There are no files selected for viewing
137 changes: 137 additions & 0 deletions
137
infra/util/src/main/java/com/evolveum/midpoint/util/DependencyGraph.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
/* | ||
* Copyright (C) 2010-2024 Evolveum and contributors | ||
* | ||
* This work is dual-licensed under the Apache License 2.0 | ||
* and European Union Public License. See LICENSE file for details. | ||
*/ | ||
|
||
package com.evolveum.midpoint.util; | ||
|
||
import org.jetbrains.annotations.NotNull; | ||
|
||
import java.util.*; | ||
|
||
/** | ||
* Represents the dependencies between items. | ||
* | ||
* Provides {@link #getSortedItems()} and {@link #getTopologicalSort()} methods to sort the items topologically. | ||
* | ||
* Can be created right from the dependency map ({@link #ofMap(Map)}) or from individual items ({@link #ofItems(Collection)}). | ||
* The dependencies themselves must reference only known items. | ||
*/ | ||
public class DependencyGraph<X> { | ||
|
||
/** Client-supplied data. Should not be touched. */ | ||
@NotNull private final Map<X, Collection<? extends X>> dependencyMap; | ||
|
||
private DependencyGraph(@NotNull Map<X, Collection<? extends X>> dependencyMap) { | ||
this.dependencyMap = dependencyMap; | ||
} | ||
|
||
/** Creates the dependency graph from the given dependency map. */ | ||
public static <X> DependencyGraph<X> ofMap(Map<X, Collection<? extends X>> dependencyMap) { | ||
return new DependencyGraph<>(dependencyMap); | ||
} | ||
|
||
/** Creates the dependency graph from items that can tell us about their dependencies. */ | ||
public static <I extends Item<I>> DependencyGraph<I> ofItems(@NotNull Collection<I> items) { | ||
Map<I, Collection<? extends I>> dependencyMap = new HashMap<>(); | ||
for (I item : items) { | ||
dependencyMap.put(item, item.getDependencies()); | ||
} | ||
return DependencyGraph.ofMap(dependencyMap); | ||
} | ||
|
||
/** | ||
* Returns the items sorted topologically: if A is before B, then A does not depend on B, | ||
* or throws an exception if no such ordering exists. | ||
*/ | ||
public List<X> getSortedItems() { | ||
TopologicalSort<X> sort = new TopologicalSort<>(dependencyMap); | ||
if (sort.isComplete()) { | ||
return sort.getSortedItems(); | ||
} else { | ||
throw new IllegalStateException("Cyclic dependencies. Remaining items: " + sort.getRemainingItems()); | ||
} | ||
} | ||
|
||
/** Returns items topologically sorted (as much as possible). */ | ||
public @NotNull TopologicalSort<X> getTopologicalSort() { | ||
return new TopologicalSort<>(dependencyMap); | ||
} | ||
|
||
/** Represents a topological sort of items. The sorting itself is done at the construction time. */ | ||
public static class TopologicalSort<X> { | ||
|
||
@NotNull private final Map<X, Set<X>> remainingDependencies; | ||
@NotNull private final List<X> sortedItems; | ||
|
||
private TopologicalSort(Map<X, Collection<? extends X>> dependencyMap) { | ||
remainingDependencies = copyAndCheckMap(dependencyMap); | ||
sortedItems = new ArrayList<>(remainingDependencies.size()); | ||
for (;;) { | ||
X item = findItemWithNoDependencies(); | ||
if (item == null) { | ||
break; | ||
} | ||
sortedItems.add(item); | ||
remove(item); | ||
} | ||
} | ||
|
||
private Map<X, Set<X>> copyAndCheckMap(Map<X, Collection<? extends X>> origMap) { | ||
Map<X, Set<X>> targetMap = new HashMap<>(); | ||
for (Map.Entry<X, Collection<? extends X>> origEntry : origMap.entrySet()) { | ||
X origItem = origEntry.getKey(); | ||
Collection<? extends X> origItemDependencies = origEntry.getValue(); | ||
Set<X> targetDependencySet = new HashSet<>(); | ||
for (X dependency : origItemDependencies) { | ||
if (!origMap.containsKey(dependency)) { | ||
throw new IllegalStateException( | ||
"Item " + origItem + " depends on " + dependency + " which is not in the graph"); | ||
} | ||
targetDependencySet.add(dependency); | ||
} | ||
targetMap.put(origItem, targetDependencySet); | ||
} | ||
return targetMap; | ||
} | ||
|
||
private X findItemWithNoDependencies() { | ||
for (Map.Entry<X, Set<X>> entry : remainingDependencies.entrySet()) { | ||
if (entry.getValue().isEmpty()) { | ||
return entry.getKey(); | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
private void remove(X item) { | ||
remainingDependencies.remove(item); | ||
for (Set<X> dependencies : remainingDependencies.values()) { | ||
dependencies.remove(item); | ||
} | ||
} | ||
|
||
private boolean isComplete() { | ||
return remainingDependencies.isEmpty(); | ||
} | ||
|
||
@SuppressWarnings("WeakerAccess") | ||
public @NotNull List<X> getSortedItems() { | ||
return sortedItems; | ||
} | ||
|
||
@SuppressWarnings("WeakerAccess") | ||
public @NotNull Collection<X> getRemainingItems() { | ||
return remainingDependencies.keySet(); | ||
} | ||
} | ||
|
||
/** An item that can tell us about its dependencies. */ | ||
public interface Item<I extends Item<I>> { | ||
|
||
/** Returns the items that this item depends on. */ | ||
@NotNull Collection<I> getDependencies(); | ||
} | ||
} |
126 changes: 126 additions & 0 deletions
126
infra/util/src/test/java/com/evolveum/midpoint/util/DependencyGraphTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
/* | ||
* Copyright (c) 2010-2013 Evolveum and contributors | ||
* | ||
* This work is dual-licensed under the Apache License 2.0 | ||
* and European Union Public License. See LICENSE file for details. | ||
*/ | ||
package com.evolveum.midpoint.util; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.assertj.core.api.Assertions.fail; | ||
|
||
import org.jetbrains.annotations.NotNull; | ||
import org.testng.annotations.Test; | ||
|
||
import com.evolveum.midpoint.tools.testng.AbstractUnitTest; | ||
|
||
import java.util.Collection; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
|
||
public class DependencyGraphTest extends AbstractUnitTest { | ||
|
||
@Test | ||
public void testEmptyGraph() { | ||
var emptyGraph = DependencyGraph.ofMap(Map.of()); | ||
var sortedItems = emptyGraph.getSortedItems(); | ||
|
||
displayValue("sortedItems", sortedItems); | ||
assertThat(sortedItems).isEmpty(); | ||
} | ||
|
||
@Test | ||
public void testNoDependencies() { | ||
var noDepGraph = DependencyGraph.ofMap(Map.of("a", Set.of(), "b", Set.of(), "c", Set.of())); | ||
var sortedItems = noDepGraph.getSortedItems(); | ||
|
||
displayValue("sortedItems", sortedItems); | ||
assertThat(sortedItems).containsExactlyInAnyOrder("a", "b", "c"); | ||
} | ||
|
||
@Test | ||
public void testInvalidDependencies() { | ||
var invalidGraph = DependencyGraph.ofMap(Map.of("a", Set.of("b"))); | ||
try { | ||
invalidGraph.getSortedItems(); | ||
fail("unexpected success"); | ||
} catch (IllegalStateException e) { | ||
displayExpectedException(e); | ||
assertThat(e).hasMessage("Item a depends on b which is not in the graph"); | ||
} | ||
} | ||
|
||
@Test | ||
public void testSimpleCyclicDependencies() { | ||
var cycle = DependencyGraph.ofMap(Map.of("a", Set.of("a"), "b", Set.of())); | ||
try { | ||
cycle.getSortedItems(); | ||
fail("unexpected success"); | ||
} catch (IllegalStateException e) { | ||
displayExpectedException(e); | ||
assertThat(e).hasMessage("Cyclic dependencies. Remaining items: [a]"); | ||
} | ||
|
||
var sort = cycle.getTopologicalSort(); | ||
assertThat(sort.getSortedItems()).containsExactly("b"); | ||
assertThat(sort.getRemainingItems()).containsExactly("a"); | ||
} | ||
|
||
@Test | ||
public void testComplexCyclicDependencies() { | ||
var cycle = DependencyGraph.ofMap(Map.of("a", Set.of("b"), "b", Set.of("c"), "d", Set.of(), "c", Set.of("a"))); | ||
try { | ||
cycle.getSortedItems(); | ||
fail("unexpected success"); | ||
} catch (IllegalStateException e) { | ||
displayExpectedException(e); | ||
assertThat(e).hasMessageContaining("Cyclic dependencies. Remaining items:"); | ||
} | ||
|
||
var sort = cycle.getTopologicalSort(); | ||
assertThat(sort.getSortedItems()).containsExactly("d"); | ||
assertThat(sort.getRemainingItems()).containsExactlyInAnyOrder("a", "b", "c"); | ||
} | ||
|
||
@Test | ||
public void testConstruction() { | ||
var a = new TestItem("a", Set.of()); | ||
var b = new TestItem("b", Set.of(a)); | ||
var c = new TestItem("c", Set.of(a, b)); | ||
var d = new TestItem("d", Set.of()); | ||
|
||
var graph = DependencyGraph.ofItems(List.of(a, b, c, d)); | ||
var sortedItems = graph.getSortedItems(); | ||
displayValue("sortedItems", sortedItems); | ||
|
||
assertThat(sortedItems).containsExactlyInAnyOrder(a, b, c, d); | ||
int indexOfA = sortedItems.indexOf(a); | ||
int indexOfB = sortedItems.indexOf(b); | ||
int indexOfC = sortedItems.indexOf(c); | ||
assertThat(indexOfA).isLessThan(indexOfB); | ||
assertThat(indexOfA).isLessThan(indexOfC); | ||
assertThat(indexOfB).isLessThan(indexOfC); | ||
} | ||
|
||
static class TestItem implements DependencyGraph.Item<TestItem> { | ||
|
||
@NotNull private final String id; | ||
private final Collection<TestItem> dependencies; | ||
|
||
TestItem(@NotNull String id, Collection<TestItem> dependencies) { | ||
this.id = id; | ||
this.dependencies = dependencies; | ||
} | ||
|
||
@Override | ||
public @NotNull Collection<TestItem> getDependencies() { | ||
return dependencies; | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return id; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters