All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.ferstl.depgraph.graph.GraphBuilder Maven / Gradle / Ivy

Go to download

This Maven plugin generates dependency graphs on single modules or in an aggregated form on multi-module projects. The graphs are represented by .dot files. In case that Graphviz is installed on the machine where this plugin is run, the .dot file can be directly converted into all supported image files.

There is a newer version: 4.0.3
Show newest version
/*
 * Copyright (c) 2014 - 2017 the original author or authors.
 *
 * 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 com.github.ferstl.depgraph.graph;

import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import com.github.ferstl.depgraph.graph.dot.DotAttributeBuilder;
import com.github.ferstl.depgraph.graph.dot.DotGraphFormatter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;

/**
 * A builder to create DOT strings by defining edges between
 * Nodes. The builder allows some customizations including custom {@link NodeRenderer}s and
 * {@link EdgeRenderer}s.
 *
 * @param  Type of the graph nodes.
 */
public final class GraphBuilder {

  private final NodeRenderer nodeIdRenderer;
  private final Map> nodeDefinitions;
  private final Set edges;
  private final ReachabilityMap reachabilityMap;

  private String graphName;
  private GraphFormatter graphFormatter;
  private NodeRenderer nodeNameRenderer;
  private EdgeRenderer edgeRenderer;
  private boolean omitSelfReferences;

  public static  GraphBuilder create(NodeRenderer nodeIdRenderer) {
    return new GraphBuilder<>(nodeIdRenderer);
  }

  private GraphBuilder(NodeRenderer nodeIdRenderer) {
    this.nodeIdRenderer = nodeIdRenderer;
    this.nodeDefinitions = new LinkedHashMap<>();
    this.edges = new LinkedHashSet<>();
    this.reachabilityMap = new ReachabilityMap();

    DotAttributeBuilder graphAttributeBuilder = new DotAttributeBuilder();
    DotAttributeBuilder nodeAttributeBuilder = new DotAttributeBuilder().shape("box").fontName("Helvetica");
    DotAttributeBuilder edgeAttributeBuilder = new DotAttributeBuilder().fontName("Helvetica").fontSize(10);

    this.graphName = "G";
    this.graphFormatter = new DotGraphFormatter(graphAttributeBuilder, nodeAttributeBuilder, edgeAttributeBuilder);
    this.nodeNameRenderer = createDefaultNodeNameRenderer();
    this.edgeRenderer = createDefaultEdgeRenderer();
  }

  public GraphBuilder graphName(String name) {
    this.graphName = name;
    return this;
  }

  public GraphBuilder useNodeNameRenderer(NodeRenderer nodeNameRenderer) {
    this.nodeNameRenderer = nodeNameRenderer;
    return this;
  }

  public GraphBuilder useEdgeRenderer(EdgeRenderer edgeRenderer) {
    this.edgeRenderer = edgeRenderer;
    return this;
  }

  public GraphBuilder omitSelfReferences() {
    this.omitSelfReferences = true;
    return this;
  }

  public GraphBuilder graphFormatter(GraphFormatter formatter) {
    this.graphFormatter = formatter;
    return this;
  }

  public boolean isEmpty() {
    return this.nodeDefinitions.isEmpty();
  }

  /**
   * Adds a single node to the graph.
   *
   * @param node The node to add.
   * @return This builder.
   */
  public GraphBuilder addNode(T node) {
    String nodeId = this.nodeIdRenderer.render(node);
    String nodeName = this.nodeNameRenderer.render(node);
    this.nodeDefinitions.put(nodeId, new Node<>(nodeId, nodeName, node));

    return this;
  }

  public GraphBuilder addEdge(T from, T to) {
    return addEdgeInternal(from, to, false);
  }

  public GraphBuilder addPermanentEdge(T from, T to) {
    return addEdgeInternal(from, to, true);
  }

  /**
   * Returns the node that was added first to this builder or the given node if new.
   *
   * @param node Node.
   * @return The firstly added node or the given node if not present.
   */
  public T getEffectiveNode(T node) {
    String key = this.nodeIdRenderer.render(node);
    if (this.nodeDefinitions.containsKey(key)) {
      return this.nodeDefinitions.get(key).nodeObject;
    }

    return node;
  }

  public void reduceEdges() {
    Iterator edgeIterator = this.edges.iterator();
    while (edgeIterator.hasNext()) {
      Edge edge = edgeIterator.next();
      if (!edge.isPermanent() && this.reachabilityMap.hasOlderPath(edge.getToNodeId(), edge.getFromNodeId())) {
        edgeIterator.remove();
      }
    }
  }

  @Override
  public String toString() {
    // Work around some generics restrictions
    ImmutableList.Builder> nodeListBuilder = ImmutableList.builder();
    for (Node node : this.nodeDefinitions.values()) {
      nodeListBuilder.add(node);
    }
    ImmutableList> nodeList = nodeListBuilder.build();
    ImmutableSet edgeSet = ImmutableSet.copyOf(this.edges);

    return this.graphFormatter.format(this.graphName, nodeList, edgeSet);
  }

  /**
   * Adds the two given nodes to the graph and creates an edge between them if they are not {@code null}.
   * Nothing will be added to the graph if one or both nodes are {@code null}.
   *
   * @param from From node.
   * @param to To node.
   * @param permanent Whether the edge is permanent.
   * @return This builder.
   */
  private GraphBuilder addEdgeInternal(T from, T to, boolean permanent) {
    if (from != null && to != null) {
      addNode(from);
      addNode(to);

      safelyAddEdge(from, to, permanent);
    }

    return this;
  }

  private void safelyAddEdge(T fromNode, T toNode, boolean permanent) {
    String fromNodeId = this.nodeIdRenderer.render(fromNode);
    String toNodeId = this.nodeIdRenderer.render(toNode);

    if (!this.omitSelfReferences || !fromNodeId.equals(toNodeId)) {
      Edge edge = new Edge(fromNodeId, toNodeId, this.edgeRenderer.render(fromNode, toNode), permanent);
      this.edges.add(edge);
      this.reachabilityMap.registerEdge(fromNodeId, toNodeId);
    }
  }

  private static  EdgeRenderer createDefaultEdgeRenderer() {
    return new EdgeRenderer() {

      @Override
      public String render(T from, T to) {
        return "";
      }

    };
  }

  private static  NodeRenderer createDefaultNodeNameRenderer() {
    return new NodeRenderer() {

      @Override
      public String render(T node) {
        return "";
      }
    };
  }

  /**
   * A map that tracks which nodes are reachable from other nodes.
   * When a new edge 'A -> B' is added, the map registers node 'A' as a parent of node 'B'. To find out whether a node
   * 'Y' is reachable from node 'X', we can recursively traverse the parents of node 'Y'. When node 'X' is found in this
   * traversal, 'Y' is reachable via 'X'. When all nodes are traversed and 'X' is not found, 'Y' is not reachable via 'X'.
   * To handle cycles in the graph, the node traversal keeps track of all already traversed nodes.
   */
  private static class ReachabilityMap {

    private final Map> parentIndex = new LinkedHashMap<>();

    void registerEdge(String from, String to) {
      Set parents = safelyGetParents(to);
      parents.add(from);
    }

    boolean hasOlderPath(String target, String source) {
      return isReachable(target, source, true, new HashSet());
    }


    /**
     * Recursively traverses the parents of {@code target} trying to find {@code source} by keeping track of already traversed
     * nodes. If {@code olderParentsOnly} is set to {@code true}, only the parents that were inserted before
     * {@code source} will be considered.
     *
     * @return {@code true} if {@code target} is reachable via {@code source}, {@code false} else.
     */
    private boolean isReachable(String target, String source, boolean olderParentsOnly, Set alreadyVisited) {
      if (alreadyVisited.contains(target)) {
        return false;
      }

      alreadyVisited.add(target);

      Set parents = olderParentsOnly ? getOlderParents(target, source) : safelyGetParents(target);
      if (parents.contains(source)) {
        return true;
      }

      for (String parent : parents) {
        if (isReachable(parent, source, false, alreadyVisited)) {
          return true;
        }
      }

      return false;
    }

    private Set getOlderParents(String target, String source) {
      Set olderParents = new LinkedHashSet<>(safelyGetParents(target));
      boolean remove = false;
      Iterator iterator = olderParents.iterator();
      while (iterator.hasNext()) {
        String value = iterator.next();
        if (value.equals(source)) {
          remove = true;
        }

        if (remove) {
          iterator.remove();
        }
      }

      return olderParents;
    }

    private Set safelyGetParents(String node) {
      Set parentPath = this.parentIndex.get(node);
      if (parentPath == null) {
        parentPath = new LinkedHashSet<>();
        this.parentIndex.put(node, parentPath);
      }

      return parentPath;
    }

  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy