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

com.salesforce.jgrapht.ext.DOTImporter Maven / Gradle / Ivy

Go to download

This project contains the apt processor that implements all the checks enumerated in @Verify. It is a self contained, and shaded jar.

There is a newer version: 2.0.7
Show newest version
/*
 * (C) Copyright 2015-2017, by Wil Selwood and Contributors.
 *
 * JGraphT : a free Java graph-theory library
 *
 * This program and the accompanying materials are dual-licensed under
 * either
 *
 * (a) the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation, or (at your option) any
 * later version.
 *
 * or (per the licensee's choosing)
 *
 * (b) the terms of the Eclipse Public License v1.0 as published by
 * the Eclipse Foundation.
 */
package com.salesforce.jgrapht.ext;

import java.io.*;
import java.util.*;
import java.util.stream.*;

import com.salesforce.jgrapht.*;
import com.salesforce.jgrapht.graph.*;

/**
 * Imports a graph from a DOT file.
 *
 * 

* For a description of the format see * http://en.wikipedia.org/wiki/DOT_language and * * http://www.graphviz.org/doc/info/lang.html *

* * state machine description (In dot format naturally): * *
 * 
 *
 * digraph G {
 *    1 [label="start" description="Entry point"];
 *    2 [label="header" description="Processing The header"];
 *    3 [label="next" description="work out what the type of the next node is"];
 *    4 [label="edge" description="process an edge entry"];
 *    5 [label="edge_quotes" description="process a section of an edge in quotes"];
 *    6 [label="node" description="process a node entry"];
 *    7 [label="node_quotes" description="process a section of a node in quotes"];
 *    8 [label="line_comment" description="process and ignore a line comment"];
 *    9 [label="block_comment" description="process and ignore a block comment"];
 *    10 [label="done" description="exit point"];
 *    1 -> 2;
 *    2 -> 3;
 *    3 -> 4;
 *    4 -> 3;
 *    4 -> 5;
 *    5 -> 4;
 *    3 -> 6;
 *    6 -> 3;
 *    6 -> 7;
 *    7 -> 6;
 *    3 -> 10;
 *    2 -> 8;
 *    8 -> 2;
 *    2 -> 9;
 *    9 -> 2;
 *    3 -> 8;
 *    8 -> 3;
 *    3 -> 9;
 *    9 -> 3;
 *    4 -> 8;
 *    8 -> 4;
 *    4 -> 9;
 *    9 -> 4;
 *    6 -> 8;
 *    8 -> 6;
 *    6 -> 9;
 *    9 -> 6;
 * }
 *
 * 
 * 
* * @param the graph vertex type * @param the graph edge type * * @author Wil Selwood */ public class DOTImporter implements GraphImporter { /** * Default key used in the graph updater (if provided) for the graph ID. */ public static final String DEFAULT_GRAPH_ID_KEY = "ID"; // Constants for the state machine private static final int HEADER = 1; private static final int NODE = 2; private static final int EDGE = 3; private static final int LINE_COMMENT = 4; private static final int BLOCK_COMMENT = 5; private static final int NODE_QUOTES = 6; private static final int EDGE_QUOTES = 7; private static final int NEXT = 8; private static final int DONE = 32; private VertexProvider vertexProvider; private ComponentUpdater vertexUpdater; private EdgeProvider edgeProvider; private ComponentUpdater> graphUpdater; /** * Constructs a new DOTImporter with the given providers * * @param vertexProvider Provider to create a vertex * @param edgeProvider Provider to create an edge */ public DOTImporter(VertexProvider vertexProvider, EdgeProvider edgeProvider) { this(vertexProvider, edgeProvider, null); } /** * Constructs a new DOTImporter with the given providers * * @param vertexProvider Provider to create a vertex * @param edgeProvider Provider to create an edge * @param vertexUpdater Method used to update an existing Vertex */ public DOTImporter( VertexProvider vertexProvider, EdgeProvider edgeProvider, ComponentUpdater vertexUpdater) { this(vertexProvider, edgeProvider, vertexUpdater, null); } /** * Constructs a new DOTImporter with the given providers * * @param vertexProvider Provider to create a vertex * @param edgeProvider Provider to create an edge * @param vertexUpdater Method used to update an existing Vertex * @param graphUpdater Method used to update the graph ID */ public DOTImporter( VertexProvider vertexProvider, EdgeProvider edgeProvider, ComponentUpdater vertexUpdater, ComponentUpdater> graphUpdater) { this.vertexProvider = vertexProvider; this.vertexUpdater = vertexUpdater; this.edgeProvider = edgeProvider; this.graphUpdater = (graphUpdater != null) ? graphUpdater : (c, a) -> { }; } /** * Read a dot formatted input and populate the provided graph. * * The current implementation reads the whole input as a string and then parses the graph. * * @param graph the graph to update * @param input the input reader * * @throws ImportException if there is a problem parsing the file. */ @Override public void importGraph(Graph graph, Reader input) throws ImportException { BufferedReader br; if (input instanceof BufferedReader) { br = (BufferedReader) input; } else { br = new BufferedReader(input); } read(br.lines().collect(Collectors.joining("\n")), (Graph) graph); } /** * Read a dot formatted string and populate the provided graph. * * @param input the content of a dot file. * @param graph the graph to update. * * @throws ImportException if there is a problem parsing the file. */ private void read(String input, Graph graph) throws ImportException { if ((input == null) || input.isEmpty()) { throw new ImportException("Dot string was empty"); } Map vertexes = new HashMap<>(); int state = HEADER; int lastState = HEADER; int position = 0; StringBuilder sectionBuffer = new StringBuilder(); while ((state != DONE) && (position < input.length())) { int existingState = state; switch (state) { case HEADER: state = processHeader(input, position, sectionBuffer, graph); break; case NODE: state = processNode(input, position, sectionBuffer, graph, vertexes); break; case EDGE: state = processEdge(input, position, sectionBuffer, graph, vertexes); break; case LINE_COMMENT: state = processLineComment(input, position, sectionBuffer, lastState); if (state == lastState) { // when we leave a line comment we need the new line to // still appear in the old block position = position - 1; } break; case BLOCK_COMMENT: state = processBlockComment(input, position, lastState); break; case NODE_QUOTES: state = processNodeQuotes(input, position, sectionBuffer); break; case EDGE_QUOTES: state = processEdgeQuotes(input, position, sectionBuffer); break; case NEXT: state = processNext(input, position, sectionBuffer, graph, vertexes); break; // DONE not included here as we can't get to it with the while loop. default: throw new ImportException("Error importing escaped state machine"); } position = position + 1; if (state != existingState) { lastState = existingState; } } // if we get to the end and are some how still in the header the input // must be invalid. if (state == HEADER) { throw new ImportException("Invalid Header"); } } /** * Process the header block. * * @param input the input string to read from. * @param position how far along the input string we are. * @param sectionBuffer Current buffer. * @param graph the graph we are updating * * @return the new state. * * @throws ImportException if there is a problem with the header section. */ private int processHeader( String input, int position, StringBuilder sectionBuffer, Graph graph) throws ImportException { if (isStartOfLineComment(input, position)) { return LINE_COMMENT; } if (isStartOfBlockComment(input, position)) { return BLOCK_COMMENT; } char current = input.charAt(position); if (current == '{') { // reached the end of the header. Validate it. String[] headerParts = sectionBuffer.toString().trim().split(" ", 4); // only graph/digraph keywords are required by the language specifications if (headerParts.length < 1) { throw new ImportException("Not enough parts in header"); } int i = 0; if (graph instanceof AbstractBaseGraph && ((AbstractBaseGraph) graph).isAllowingMultipleEdges() && headerParts[i] .toLowerCase().equals(DOTUtils.DONT_ALLOW_MULTIPLE_EDGES_KEYWORD)) { throw new ImportException( "graph defines " + DOTUtils.DONT_ALLOW_MULTIPLE_EDGES_KEYWORD + " but Multigraph given."); } else if (headerParts[i] .toLowerCase().equals(DOTUtils.DONT_ALLOW_MULTIPLE_EDGES_KEYWORD)) { i = i + 1; } if (headerParts.length <= i) { throw new ImportException("Malformed header: " + sectionBuffer.toString()); } else if ((graph instanceof DirectedGraph) && headerParts[i].toLowerCase().equals(DOTUtils.UNDIRECTED_GRAPH_KEYWORD)) { throw new ImportException( "input asks for undirected graph and directed graph provided."); } else if (!(graph instanceof DirectedGraph) && headerParts[i].equals(DOTUtils.DIRECTED_GRAPH_KEYWORD)) { throw new ImportException( "input asks for directed graph but undirected graph provided."); } else if (!headerParts[i].toLowerCase().equals(DOTUtils.UNDIRECTED_GRAPH_KEYWORD) && !headerParts[i].toLowerCase().equals(DOTUtils.DIRECTED_GRAPH_KEYWORD)) { throw new ImportException("unknown graph type: " + headerParts[i]); } // only parse the graph ID if we will update the components and there is a provided // header if (headerParts.length > ++i) { final String idCandidate = String.join(" ", Arrays.copyOfRange(headerParts, i, headerParts.length)); if (!DOTUtils.isValidID(idCandidate)) { throw new ImportException( "ID in the graph is not formatted correctly: '" + idCandidate + "'"); } graphUpdater .update(graph, Collections.singletonMap(DEFAULT_GRAPH_ID_KEY, idCandidate)); } sectionBuffer.setLength(0); // reset the buffer. return NEXT; } else { // append the current character here to get rid of '{' when parsing the header sectionBuffer.append(current); } return HEADER; } /** * When we start a new section of the graph we don't know what it is going to be. We work in * here until we can work out what type of section this is. * * @param input the input string to read from. * @param position how far into the string we have got. * @param sectionBuffer the current section. * @param graph the graph we are creating. * @param vertexes the existing set of vertexes that have been created so far. * * @return the next state. * * @throws ImportException if there is a problem with creating a node. */ private int processNext( String input, int position, StringBuilder sectionBuffer, Graph graph, Map vertexes) throws ImportException { if (isStartOfLineComment(input, position)) { return LINE_COMMENT; } if (isStartOfBlockComment(input, position)) { return BLOCK_COMMENT; } char current = input.charAt(position); // ignore new line characters or section breaks between identified // sections. if ((current == '\n') || (current == '\r')) { return NEXT; } // if the buffer is currently empty skip spaces too. if ((sectionBuffer.length() == 0) && ((current == ' ') || (current == ';'))) { return NEXT; } // If we have a semi colon and some thing in the buffer we must be at // the end of a block. as we can't have had a dash yet we must be at the // end of a node. if (current == ';') { processCompleteNode(sectionBuffer.toString(), graph, vertexes); sectionBuffer.setLength(0); return NEXT; } sectionBuffer.append(input.charAt(position)); if (position < (input.length() - 1)) { char next = input.charAt(position + 1); if (current == '-') { if ((next == '-') && (graph instanceof DirectedGraph)) { throw new ImportException("graph is directed but undirected edge found"); } else if ((next == '>') && !(graph instanceof DirectedGraph)) { throw new ImportException("graph is undirected but directed edge found"); } else if ((next == '-') || (next == '>')) { return EDGE; } } } if (current == '[') { return NODE; // if this was an edge we should have found a dash before // here. } return NEXT; } /** * Process a node entry. When we detect that we are at the end of the node create it in the * graph. * * @param input the input string to read from. * @param position how far into the string we have got. * @param sectionBuffer the current section. * @param graph the graph we are creating. * @param vertexes the existing set of vertexes that have been created so far. * * @return the next state. * * @throws ImportException if there is a problem with creating a node. */ private int processNode( String input, int position, StringBuilder sectionBuffer, Graph graph, Map vertexes) throws ImportException { if (isStartOfLineComment(input, position)) { return LINE_COMMENT; } if (isStartOfBlockComment(input, position)) { return BLOCK_COMMENT; } char current = input.charAt(position); sectionBuffer.append(input.charAt(position)); if (current == '"') { return NODE_QUOTES; } if ((current == ']') || (current == ';')) { processCompleteNode(sectionBuffer.toString(), graph, vertexes); sectionBuffer.setLength(0); return NEXT; } return NODE; } /** * Process a quoted section of a node entry. This skips most of the exit conditions so quoted * strings can contain comments, semi colons, dashes, newlines and so on. * * @param input the input string to read from. * @param position how far into the string we have got. * @param sectionBuffer the current section. * * @return the state for the next character. */ private int processNodeQuotes(String input, int position, StringBuilder sectionBuffer) { char current = input.charAt(position); sectionBuffer.append(input.charAt(position)); if (current == '"') { if (input.charAt(position - 1) != '\\') { return NODE; } } return NODE_QUOTES; } private int processEdge( String input, int position, StringBuilder sectionBuffer, Graph graph, Map vertexes) throws ImportException { if (isStartOfLineComment(input, position)) { return LINE_COMMENT; } if (isStartOfBlockComment(input, position)) { return BLOCK_COMMENT; } char current = input.charAt(position); sectionBuffer.append(input.charAt(position)); if (current == '"') { return EDGE_QUOTES; } if ((current == ';') || (current == '\r') || (current == '\n')) { processCompleteEdge(sectionBuffer.toString(), graph, vertexes); sectionBuffer.setLength(0); return NEXT; } return EDGE; } private int processEdgeQuotes(String input, int position, StringBuilder sectionBuffer) { char current = input.charAt(position); sectionBuffer.append(input.charAt(position)); if (current == '"') { if (input.charAt(position - 1) != '\\') { return EDGE; } } return EDGE_QUOTES; } private int processLineComment( String input, int position, StringBuilder sectionBuffer, int returnState) { char current = input.charAt(position); if ((current == '\r') || (current == '\n')) { sectionBuffer.append(current); return returnState; } return LINE_COMMENT; } private int processBlockComment(String input, int position, int returnState) { char current = input.charAt(position); if (current == '/') { if (input.charAt(position - 1) == '*') { return returnState; } } return BLOCK_COMMENT; } private boolean isStartOfLineComment(String input, int position) { char current = input.charAt(position); if (current == '#') { return true; } else if (current == '/') { if (position < (input.length() - 1)) { if (input.charAt(position + 1) == '/') { return true; } } } return false; } private boolean isStartOfBlockComment(String input, int position) { char current = input.charAt(position); if (current == '/') { if (position < (input.length() - 1)) { if (input.charAt(position + 1) == '*') { return true; } } } return false; } private void processCompleteNode(String node, Graph graph, Map vertexes) throws ImportException { Map attributes = extractAttributes(node); String id = node.trim(); int bracketIndex = node.indexOf('['); if (bracketIndex > 0) { id = node.substring(0, node.indexOf('[')).trim(); } V existing = vertexes.get(id); if (existing == null) { V vertex = vertexProvider.buildVertex(id, attributes); graph.addVertex(vertex); vertexes.put(id, vertex); } else { if (vertexUpdater != null) { vertexUpdater.update(existing, attributes); } else { throw new ImportException( "Update required for vertex " + id + " but no vertexUpdater provided"); } } } private void processCompleteEdge(String edge, Graph graph, Map vertexes) throws ImportException { Map attributes = extractAttributes(edge); List ids = extractEdgeIds(edge); // for each pair of ids in the list create an edge. for (int i = 0; i < (ids.size() - 1); i++) { V v1 = getVertex(ids.get(i), vertexes, graph); V v2 = getVertex(ids.get(i + 1), vertexes, graph); E resultEdge = edgeProvider.buildEdge(v1, v2, attributes.get("label"), attributes); graph.addEdge(v1, v2, resultEdge); } } // if a vertex id doesn't already exist create one for it // with no attributes. private V getVertex(String id, Map vertexes, Graph graph) { V v = vertexes.get(id); if (v == null) { v = vertexProvider.buildVertex(id, new HashMap<>()); graph.addVertex(v); vertexes.put(id, v); } return v; } private List extractEdgeIds(String line) { String idChunk = line.trim(); if (idChunk.endsWith(";")) { idChunk = idChunk.substring(0, idChunk.length() - 1); } int bracketIndex = idChunk.indexOf('['); if (bracketIndex > 1) { idChunk = idChunk.substring(0, bracketIndex).trim(); } int index = 0; List ids = new ArrayList<>(); while (index < idChunk.length()) { int nextSpace = idChunk.indexOf(' ', index); String chunk; if (nextSpace > 0) { // is this the last chunk chunk = idChunk.substring(index, nextSpace); index = nextSpace + 1; } else { chunk = idChunk.substring(index); index = idChunk.length() + 1; } if (!chunk.equals(DOTUtils.UNDIRECTED_GRAPH_EDGEOP) && !chunk.equals(DOTUtils.DIRECTED_GRAPH_EDGEOP)) { // a label then? ids.add(chunk); } } return ids; } private Map extractAttributes(String line) throws ImportException { Map attributes = new HashMap<>(); int bracketIndex = line.indexOf("["); if (bracketIndex > 0) { attributes = splitAttributes(line.substring(bracketIndex + 1, line.lastIndexOf(']')).trim()); } return attributes; } private Map splitAttributes(String input) throws ImportException { int index = 0; Map result = new HashMap<>(); while (index < input.length()) { // skip any leading white space index = skipWhiteSpace(input, index); // Now check for quotes int endOfKey = findEndOfSection(input, index, '='); if (endOfKey < 0) { throw new ImportException("Invalid attributes"); } if (input.charAt(endOfKey) == '"') { index = index + 1; } String key = input.substring(index, endOfKey).trim(); if ((endOfKey + 1) >= input.length()) { throw new ImportException("Invalid attributes"); } // Attribute value may be quoted or a single word. // First ignore any white space before the start int start = skipWhiteSpace(input, endOfKey + 1); int endChar = findEndOfSection(input, start, ' '); if (input.charAt(start) == '"') { start = start + 1; } if (endChar < 0) { endChar = input.length(); } String value = input.substring(start, endChar); result.put(key, value); index = endChar + 1; } return result; } private int skipWhiteSpace(String input, int start) throws ImportException { int i = 0; while (Character.isWhitespace(input.charAt(start + i)) || (input.charAt(start + i) == '=')) { i = i + 1; if ((start + i) >= input.length()) { throw new ImportException("Invalid attributes"); } } return start + i; } private int findEndOfSection(String input, int start, char terminator) { if (input.charAt(start) == '"') { return findNextQuote(input, start); } else { return input.indexOf(terminator, start); } } private int findNextQuote(String input, int start) { int result = start; do { result = input.indexOf('\"', result + 1); // if the previous character is an escape then keep going } while ((input.charAt(result - 1) == '\\') && !((input.charAt(result - 1) == '\\') && (input.charAt(result - 2) == '\\'))); // unless // its // escaped return result; } } // End DOTImporter.java




© 2015 - 2025 Weber Informatics LLC | Privacy Policy