org.jtrim2.taskgraph.basic.DirectedGraph Maven / Gradle / Ivy
package org.jtrim2.taskgraph.basic;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.jtrim2.collections.CollectionsEx;
import org.jtrim2.utils.ExceptionHelper;
/**
* Defines a directed graph without any solitary nodes. That is, this class
* only defines the edges in a directed graph and cannot define nodes without
* part of an edge.
*
* An instance of {@code DirectedGraph} via its {@link DirectedGraph.Builder}.
*
*
Thread safety
* Instances of {@code DirectedGraph} are immutable (assuming the nodes themselves
* are immutable) and so can be used safely by multiple threads concurrently.
*
* Synchronization transparency
* The methods of {@code DirectedGraph} are synchronization transparent.
*
* @param the type of the node. The nodes are distinguished based on their
* {@code equals}.
*/
public final class DirectedGraph {
private final Map> childrenGraph;
private DirectedGraph(Builder builder) {
this(copy(builder.childrenGraph));
}
private DirectedGraph(Map> childrenGraph) {
this.childrenGraph = childrenGraph;
}
/**
* Checks if this graph is acyclic or not, throwing an exception if
* this graph is not acyclic.
*
* @throws IllegalStateException thrown if this graph is not acyclic.
*/
public void checkNotCyclic() {
Map> graph = new LinkedHashMap<>(childrenGraph);
while (!graph.isEmpty()) {
N key = graph.keySet().iterator().next();
checkNotCyclic(Collections.singleton(key), new LinkedHashSet<>(), graph);
}
}
private static void checkNotCyclic(
Set startNodes,
Set visited,
Map> graph) {
startNodes.forEach((key) -> {
if (visited.contains(key)) {
List cycle = new ArrayList<>(visited.size() + 1);
cycle.addAll(visited);
cycle.add(key);
cycle = afterFirstMatch(key, cycle);
throw new IllegalStateException("The graph is cyclic: " + cycle);
}
Set dependencies = graph.get(key);
if (dependencies != null) {
visited.add(key);
checkNotCyclic(dependencies, visited, graph);
visited.remove(key);
}
graph.remove(key);
});
}
private static List afterFirstMatch(T match, List list) {
int size = list.size();
for (int i = 0; i < size; i++) {
if (match.equals(list.get(i))) {
return list.subList(i, size);
}
}
return Collections.emptyList();
}
/**
* Returns a map, mapping the leaf nodes (nodes having no children) to nodes they are reachable where the
* image of the mapping only contains nodes specified in the argument. If multiple leaf nodes are
* reachable from a particular node, that particular node will be listed under all the reachable
* leaf nodes.
*
* The nodes from which a particular leaf is reachable are listed in the same order as
* they appear in the node list given in the argument.
*
* There will not be entry in the map for leaf nodes not reachable from any of the given
* nodes.
*
* @param rootNodes the nodes to be considered from where leaf nodes are reachable from.
* This argument cannot be {@code null} and may not contain {@code null} elements.
* @return a map, mapping the leaf nodes (nodes having no children) to nodes they are
* reachable where the image of the mapping only contains nodes specified in the argument.
* This method never returns {@code null}.
*/
public Map> getAllLeafToRootNodes(Iterable extends N> rootNodes) {
return getAllLeafToRootNodes(rootNodes, LinkedHashSet::new);
}
/**
* Returns a map, mapping the leaf nodes (nodes having no children) to nodes they are reachable where the
* image of the mapping only contains nodes specified in the argument. If multiple leaf nodes are
* reachable from a particular node, that particular node will be listed under all the reachable
* leaf nodes.
*
* The nodes from which a particular leaf is reachable will be added to the image sets in the same
* order as they appear in the node list given in the argument.
*
* There will not be entry in the map for leaf nodes not reachable from any of the given
* nodes.
*
* @param rootNodes the nodes to be considered from where leaf nodes are reachable from.
* This argument cannot be {@code null} and may not contain {@code null} elements.
* @param newSetFactory a factory creating a empty, mutable sets for the result map.
* This argument cannot be {@code null}.
* @return a map, mapping the leaf nodes (nodes having no children) to nodes they are
* reachable where the image of the mapping only contains nodes specified in the argument.
* This method never returns {@code null}.
*/
public Map> getAllLeafToRootNodes(
Iterable extends N> rootNodes,
Supplier extends Set> newSetFactory) {
Function> keyBasedSetFactory = (key) -> newSetFactory.get();
Map> result = new LinkedHashMap<>();
rootNodes.forEach((root) -> {
addLeafToRootNodes(root, result, keyBasedSetFactory);
});
return result;
}
private void addLeafToRootNodes(
N root,
Map> result,
Function> newSetFactory) {
addLeafToRootNodes(root, root, result, newSetFactory);
}
private void addLeafToRootNodes(
N root,
N currentNode,
Map> result,
Function> newSetFactory) {
Set children = getChildren(currentNode);
if (children.isEmpty()) {
Set roots = result.computeIfAbsent(currentNode, newSetFactory);
roots.add(root);
} else {
children.forEach((key) -> {
addLeafToRootNodes(root, key, result, newSetFactory);
});
}
}
/**
* Returns a map, mapping parent nodes to their direct children. The returned map
* does not contain mapping for nodes having no children.
*
* The returned map cannot be modified.
*
* @return a map, mapping parent nodes to their direct children. This method
* never returns {@code null}.
*/
public Map> getRawGraph() {
return childrenGraph;
}
/**
* Returns the set of direct children of the given node.
*
* @param node the node whose children are to be returned. This argument cannot
* be {@code null}.
* @return the set of direct children of the given node. This method
* never returns {@code null}, if the node has no children, an empty
* set is returned.
*/
public Set getChildren(N node) {
Objects.requireNonNull(node, "node");
Set result = childrenGraph.get(node);
return result != null ? result : Collections.emptySet();
}
/**
* Returns {@code true} if the given node has at least one child node,
* {@code false} otherwise. That is, this method returns {@code true},
* if and only, if the given node is not a leaf node.
*
* This method is effectively equivalent to:
*
* !getChildren(node).isEmpty()
*
*
* @param node the node to be checked. This argument cannot be {@code null}.
* @return {@code true} if the given node has at least one child node,
* {@code false} otherwise
*/
public boolean hasChildren(N node) {
Objects.requireNonNull(node, "node");
// We do not store empty children lists.
return childrenGraph.containsKey(node);
}
/**
* Returns a {@code DirectedGraph} where the edges a reversed compared to this
* graph.
*
* @return a {@code DirectedGraph} where the edges a reversed compared to this
* graph. This method never returns {@code null}.
*/
public DirectedGraph reverseGraph() {
Map> reverseGraph = CollectionsEx.newLinkedHashMap(childrenGraph.size());
childrenGraph.forEach((node, children) -> {
children.forEach((child) -> {
Set parents = reverseGraph.computeIfAbsent(child, (x) -> new LinkedHashSet<>());
parents.add(node);
});
});
Iterator>> entryItr = reverseGraph.entrySet().iterator();
while (entryItr.hasNext()) {
Map.Entry> entry = entryItr.next();
entry.setValue(Collections.unmodifiableSet(entry.getValue()));
}
return new DirectedGraph<>(Collections.unmodifiableMap(reverseGraph));
}
/**
* The {@code Builder} used to create {@link DirectedGraph} instances.
*
* Thread safety
* The methods of this class may not be used from multiple threads concurrently.
*
* Synchronization transparency
* The methods of this class are synchronization transparent.
*
* @param the type of the node. The nodes are distinguished based on their
* {@code equals}.
*/
public static final class Builder {
private final Map> childrenGraph;
/**
* Creates a new {@code Builder} with no nodes added yet.
*/
public Builder() {
this.childrenGraph = new LinkedHashMap<>();
}
/**
* Adds a node to the graph and returns a {@code ChildrenBuilder} which can be used
* to define the children of the given node.
*
* If you do not add any child to this node, the node will not appear in the built graph,
* unless this node is the child of another node.
*
* Calling this method multiple times with the same node is the same as calling it only once
* and using any of the returned {@code ChildrenBuilder} instances is effectively the same.
*
* @param node the node to which child nodes are to be added to. This argument cannot be
* {@code null}.
* @return the {@code ChildBuilder} which can be used to add new child nodes
* to the given node. This method never returns {@code null}.
*/
public ChildrenBuilder addNode(N node) {
Set childrenList = getChildrenList(node);
return new ChildrenBuilderImpl<>(this, childrenList);
}
/**
* Adds a node to the graph and immediately allows configuring its children.
*
* If you do not add any child to this node, the node will not appear in the built graph,
* unless this node is the child of another node.
*
* Calling this method multiple times with the same node is the same as calling it only once
* and adding all children in the same
*
* @param node the node to which child nodes are to be added to. This argument cannot be
* {@code null}.
* @param childSpec the action which can add child nodes to the given node. This
* argument cannot be {@code null}.
*/
public void addNode(N node, Consumer super ChildrenBuilder> childSpec) {
ChildrenBuilder childBuilder = addNode(node);
childSpec.accept(childBuilder);
}
/**
* Adds a node to the graph and immediately add some children to that node.
*
* If you do not add any child to this node, the node will not appear in the built graph,
* unless this node is the child of another node.
*
* @param node the node to which child nodes are to be added to. This argument cannot be
* {@code null}.
* @param children the children to be added to the given node. This argument cannot be {@code null},
* and cannot contain {@code null} elements. However, it can be an empty collection, in which
* case, this method does effectively nothing.
*/
public void addNodeWithChildren(N node, Collection extends N> children) {
Set childrenList = getChildrenList(node);
childrenList.addAll(children);
}
private Set getChildrenList(N node) {
Objects.requireNonNull(node, "node");
return childrenGraph.computeIfAbsent(node, (key) -> new LinkedHashSet<>());
}
/**
* Creates a snapshot of the currently built graph. Nodes and edges added after this
* method call have effect on the returned {@code DirectedGraph}.
*
* @return a snapshot of the currently built graph. This method never returns {@code null}.
*/
public DirectedGraph build() {
return new DirectedGraph<>(this);
}
private static class ChildrenBuilderImpl implements ChildrenBuilder {
private final Builder builder;
private final Set childrenList;
public ChildrenBuilderImpl(
Builder builder,
Set childrenList) {
this.builder = builder;
this.childrenList = childrenList;
}
@Override
public void addChild(N child, Consumer super ChildrenBuilder> grandChildSpec) {
addChild(child);
Set grandChildren = builder.getChildrenList(child);
grandChildSpec.accept(new ChildrenBuilderImpl<>(builder, grandChildren));
}
@Override
public void addChild(N child) {
Objects.requireNonNull(child, "child");
childrenList.add(child);
}
@Override
public void addChildren(Collection extends N> children) {
ExceptionHelper.checkNotNullElements(children, "children");
childrenList.addAll(children);
}
}
}
private static Map> copy(Map> src) {
Map> result = CollectionsEx.newLinkedHashMap(src.size());
src.forEach((key, value) -> {
if (!value.isEmpty()) {
result.put(key, Collections.unmodifiableSet(new LinkedHashSet<>(value)));
}
});
return Collections.unmodifiableMap(result);
}
/**
* Defines a builder to add children to a particular node in the graph.
*
* Thread safety
* The methods of this class may not be used from multiple threads concurrently.
*
* Synchronization transparency
* The methods of this class are synchronization transparent.
*
* @param the type of the node. The nodes are distinguished based on their
* {@code equals}.
*
* @see DirectedGraph.Builder#addNode(Object) DirectedGraph.Builder.addNode
*/
public interface ChildrenBuilder {
/**
* Adds a new child to the associated node and continues with adding children
* to the newly added child node.
*
* @param child the new child to be added to the associated node. This argument
* cannot be {@code null}.
* @param grandChildSpec the action which can add child nodes to the newly added child node.
* This argument cannot be {@code null}.
*/
public void addChild(N child, Consumer super ChildrenBuilder> grandChildSpec);
/**
* Adds new children to the associated node.
*
* The default implementation repeatedly calls the one argument
* {@link #addChild(Object) addChild} method for all the elements in the
* children collection.
*
* @param children the children to be added to the associated node. This argument
* cannot be {@code null} and cannot contain {@code null} elements. However, it
* can be empty, in which case, this method does effectively nothing.
*/
public default void addChildren(Collection extends N> children) {
children.forEach(this::addChild);
}
/**
* Adds a new child to the associated node.
*
* The default implementation calls the two argument
* {@link #addChild(Object, Consumer) addChild} method} with an action doing nothing.
*
* @param child the new child to be added to the associated node. This argument
* cannot be {@code null}.
*/
public default void addChild(N child) {
addChild(child, (grandChildBuilder) -> { });
}
}
}