Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into feature/secrets-pro…
Browse files Browse the repository at this point in the history
…vider
  • Loading branch information
1azyman committed Feb 8, 2024
2 parents 7d1f05a + 49020b5 commit 3f4ccd0
Show file tree
Hide file tree
Showing 3 changed files with 264 additions and 0 deletions.
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();
}
}
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;
}
}
}
1 change: 1 addition & 0 deletions infra/util/testng-unit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<class name="com.evolveum.midpoint.util.TestMiscUtil" />
<class name="com.evolveum.midpoint.util.TestStringSubstitutorUtil" />
<class name="com.evolveum.midpoint.util.TestReflectionUtil" />
<class name="com.evolveum.midpoint.util.DependencyGraphTest" />
</classes>
</test>
<test name="Profiling">
Expand Down

0 comments on commit 3f4ccd0

Please sign in to comment.