org.jgrapht.io.GraphMLImporter Maven / Gradle / Ivy
/*
* (C) Copyright 2016-2017, by Dimitrios Michail 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 org.jgrapht.io;
import java.io.*;
import java.util.*;
import java.util.Map.*;
import javax.xml.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.stream.*;
import javax.xml.validation.*;
import org.jgrapht.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
/**
* Imports a graph from a GraphML data source.
*
*
* For a description of the format see
* http://en.wikipedia.org/wiki/ GraphML or the
* GraphML Primer.
*
*
*
* Below is small example of a graph in GraphML format.
*
*
* {@code
*
*
*
* yellow
*
*
*
*
* green
*
*
*
* blue
*
*
* red
*
*
*
* turquoise
*
*
* 1.0
*
*
* 1.0
*
*
* 2.0
*
*
*
*
*
* 1.1
*
*
*
* }
*
*
*
* The importer reads the input into a graph which is provided by the user. In case the graph is
* weighted and the corresponding edge key with attr.name="weight" is defined, the importer also
* reads edge weights. Otherwise edge weights are ignored. To test whether the graph is weighted,
* method {@link Graph#getType()} can be used.
*
*
* GraphML-Attributes Values are read as string key-value pairs and passed on to the
* {@link VertexProvider} and {@link EdgeProvider} respectively.
*
*
* The provided graph object, where the imported graph will be stored, must be able to support the
* features of the graph that is read. For example if the GraphML file contains self-loops then the
* graph provided must also support self-loops. The same for multiple edges. Moreover, the parser
* completely ignores the attribute "edgedefault" which denotes whether an edge is directed or not.
* Whether edges are directed or not depends on the underlying implementation of the user provided
* graph object.
*
*
* The importer by default validates the input using the 1.0
* GraphML Schema. The user can
* (not recommended) disable the validation by calling {@link #setSchemaValidation(boolean)}.
*
* @param the graph vertex type
* @param the graph edge type
*
* @author Dimitrios Michail
* @since July 2016
*/
public class GraphMLImporter
extends AbstractBaseImporter
implements GraphImporter
{
private static final String GRAPHML_SCHEMA_FILENAME = "graphml.xsd";
private static final String XLINK_SCHEMA_FILENAME = "xlink.xsd";
// special attributes
private static final String EDGE_WEIGHT_DEFAULT_ATTRIBUTE_NAME = "weight";
private String edgeWeightAttributeName = EDGE_WEIGHT_DEFAULT_ATTRIBUTE_NAME;
private boolean schemaValidation;
/**
* Constructs a new importer.
*
* @param vertexProvider provider for the generation of vertices. Must not be null.
* @param edgeProvider provider for the generation of edges. Must not be null.
*/
public GraphMLImporter(VertexProvider vertexProvider, EdgeProvider edgeProvider)
{
super(vertexProvider, edgeProvider);
this.schemaValidation = true;
}
/**
* Get the attribute name for edge weights
*
* @return the attribute name
*/
public String getEdgeWeightAttributeName()
{
return edgeWeightAttributeName;
}
/**
* Set the attribute name to use for edge weights.
*
* @param edgeWeightAttributeName the attribute name
*/
public void setEdgeWeightAttributeName(String edgeWeightAttributeName)
{
if (edgeWeightAttributeName == null) {
throw new IllegalArgumentException("Edge weight attribute name cannot be null");
}
this.edgeWeightAttributeName = edgeWeightAttributeName;
}
/**
* Whether the importer validates the input
*
* @return true if the importer validates the input
*/
public boolean isSchemaValidation()
{
return schemaValidation;
}
/**
* Set whether the importer should validate the input
*
* @param schemaValidation value for schema validation
*/
public void setSchemaValidation(boolean schemaValidation)
{
this.schemaValidation = schemaValidation;
}
/**
* Import a graph.
*
*
* The provided graph must be able to support the features of the graph that is read. For
* example if the GraphML file contains self-loops then the graph provided must also support
* self-loops. The same for multiple edges.
*
*
* If the provided graph is a weighted graph, the importer also reads edge weights.
*
*
* GraphML-Attributes Values are read as string key-value pairs and passed on to the
* {@link VertexProvider} and {@link EdgeProvider} respectively.
*
* @param graph the output graph
* @param input the input reader
* @throws ImportException in case an error occurs, such as I/O or parse error
*/
@Override
public void importGraph(Graph graph, Reader input)
throws ImportException
{
try {
// parse
XMLReader xmlReader = createXMLReader();
GraphMLHandler handler = new GraphMLHandler();
xmlReader.setContentHandler(handler);
xmlReader.setErrorHandler(handler);
xmlReader.parse(new InputSource(input));
// read result
handler.updateGraph(graph);
} catch (Exception se) {
throw new ImportException("Failed to parse GraphML", se);
}
}
private XMLReader createXMLReader()
throws ImportException
{
try {
SchemaFactory schemaFactory =
SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
// create parser
SAXParserFactory spf = SAXParserFactory.newInstance();
if (schemaValidation) {
// load schema
InputStream xsdStream =
Thread.currentThread().getContextClassLoader().getResourceAsStream(
GRAPHML_SCHEMA_FILENAME);
if (xsdStream == null) {
throw new ImportException("Failed to locate GraphML xsd");
}
InputStream xlinkStream =
Thread.currentThread().getContextClassLoader().getResourceAsStream(
XLINK_SCHEMA_FILENAME);
if (xlinkStream == null) {
throw new ImportException("Failed to locate XLink xsd");
}
Source[] sources = new Source[2];
sources[0] = new StreamSource(xlinkStream);
sources[1] = new StreamSource(xsdStream);
Schema schema = schemaFactory.newSchema(sources);
spf.setSchema(schema);
}
spf.setNamespaceAware(true);
SAXParser saxParser = spf.newSAXParser();
// create reader
return saxParser.getXMLReader();
} catch (Exception se) {
throw new ImportException("Failed to parse GraphML", se);
}
}
// content handler
private class GraphMLHandler
extends DefaultHandler
{
private static final String GRAPH = "graph";
private static final String GRAPH_ID = "id";
private static final String NODE = "node";
private static final String NODE_ID = "id";
private static final String EDGE = "edge";
private static final String ALL = "all";
private static final String EDGE_SOURCE = "source";
private static final String EDGE_TARGET = "target";
private static final String KEY = "key";
private static final String KEY_FOR = "for";
private static final String KEY_ATTR_NAME = "attr.name";
private static final String KEY_ATTR_TYPE = "attr.type";
private static final String KEY_ID = "id";
private static final String DEFAULT = "default";
private static final String DATA = "data";
private static final String DATA_KEY = "key";
// collect graph elements here
private Map nodes;
private List edges;
// record state of parser
private boolean insideDefault;
private boolean insideData;
// temporary state while reading elements
// stack needed due to nested graphs in GraphML
private Data currentData;
private Key currentKey;
private Deque currentGraphElement;
// collect custom keys
private Map nodeValidKeys;
private Map edgeValidKeys;
// construct the actual graph after parsing
public void updateGraph(Graph graph)
throws ImportException
{
if (nodes.isEmpty()) {
return;
}
// create nodes
Map graphNodes = new HashMap();
for (Entry en : nodes.entrySet()) {
String nodeId = en.getKey();
if (nodeId == null) {
throw new ImportException("Node id missing");
}
// create attributes
Map collectedAttributes = en.getValue().attributes;
Map finalAttributes = new LinkedHashMap<>();
for (Key validKey : nodeValidKeys.values()) {
String validId = validKey.id;
AttributeType validType = validKey.type;
if (collectedAttributes.containsKey(validId)) {
finalAttributes.put(
validKey.attributeName,
new DefaultAttribute<>(collectedAttributes.get(validId), validType));
} else if (validKey.defaultValue != null) {
finalAttributes.put(
validKey.attributeName,
new DefaultAttribute<>(validKey.defaultValue, validType));
}
}
// create the actual node
V v = vertexProvider.buildVertex(nodeId, finalAttributes);
graphNodes.put(nodeId, v);
graph.addVertex(v);
}
// check how to handle special edge weight
boolean handleSpecialEdgeWeights = false;
double defaultSpecialEdgeWeight = Graph.DEFAULT_EDGE_WEIGHT;
if (graph.getType().isWeighted()) {
for (Key k : edgeValidKeys.values()) {
if (k.attributeName.equals(edgeWeightAttributeName)) {
handleSpecialEdgeWeights = true;
String defaultValue = k.defaultValue;
try {
if (defaultValue != null) {
defaultSpecialEdgeWeight = Double.parseDouble(defaultValue);
}
} catch (NumberFormatException e) {
// ignore
}
// first key only which maps to special edge "weight"
break;
}
}
}
// create edges
for (GraphElement p : edges) {
if (p.id1 == null) {
throw new ImportException("Edge source vertex missing");
}
V from = graphNodes.get(p.id1);
if (from == null) {
throw new ImportException("Source vertex " + p.id1 + " not found");
}
if (p.id2 == null) {
throw new ImportException("Edge target vertex missing");
}
V to = graphNodes.get(p.id2);
if (to == null) {
throw new ImportException("Target vertex " + p.id2 + " not found");
}
// create attributes
Map collectedAttributes = p.attributes;
Map finalAttributes = new LinkedHashMap<>();
for (Key validKey : edgeValidKeys.values()) {
String validId = validKey.id;
AttributeType validType = validKey.type;
if (collectedAttributes.containsKey(validId)) {
finalAttributes.put(
validKey.attributeName,
new DefaultAttribute<>(collectedAttributes.get(validId), validType));
} else {
if (validKey.defaultValue != null) {
finalAttributes.put(
validKey.attributeName,
new DefaultAttribute<>(validKey.defaultValue, validType));
}
}
}
E e = edgeProvider.buildEdge(from, to, "e_" + from + "_" + to, finalAttributes);
graph.addEdge(from, to, e);
// special handling for weighted graphs
if (handleSpecialEdgeWeights) {
if (finalAttributes.containsKey(edgeWeightAttributeName)) {
try {
graph.setEdgeWeight(
e, Double.parseDouble(
finalAttributes.get(edgeWeightAttributeName).getValue()));
} catch (NumberFormatException nfe) {
graph.setEdgeWeight(e, defaultSpecialEdgeWeight);
}
}
}
}
}
@Override
public void startDocument()
throws SAXException
{
nodes = new HashMap<>();
edges = new ArrayList<>();
nodeValidKeys = new HashMap<>();
edgeValidKeys = new HashMap<>();
insideDefault = false;
insideData = false;
currentKey = null;
currentData = null;
currentGraphElement = new ArrayDeque<>();
currentGraphElement.push(new GraphElement("graphml"));
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException
{
switch (localName) {
case GRAPH:
currentGraphElement.push(new GraphElement(findAttribute(GRAPH_ID, attributes)));
break;
case NODE:
currentGraphElement.push(new GraphElement(findAttribute(NODE_ID, attributes)));
break;
case EDGE:
currentGraphElement.push(
new GraphElement(
findAttribute(EDGE_SOURCE, attributes),
findAttribute(EDGE_TARGET, attributes)));
break;
case KEY:
String keyId = findAttribute(KEY_ID, attributes);
String keyFor = findAttribute(KEY_FOR, attributes);
String keyAttrName = findAttribute(KEY_ATTR_NAME, attributes);
String keyAttrType = findAttribute(KEY_ATTR_TYPE, attributes);
currentKey = new Key(keyId, keyAttrName, null, null);
if (keyAttrType != null) {
currentKey.type = AttributeType.create(keyAttrType);
}
if (keyFor != null) {
switch (keyFor) {
case EDGE:
currentKey.target = KeyTarget.EDGE;
break;
case NODE:
currentKey.target = KeyTarget.NODE;
break;
case ALL:
currentKey.target = KeyTarget.ALL;
break;
}
}
break;
case DEFAULT:
insideDefault = true;
break;
case DATA:
insideData = true;
currentData = new Data(findAttribute(DATA_KEY, attributes), null);
break;
default:
break;
}
}
@Override
public void endElement(String uri, String localName, String qName)
throws SAXException
{
switch (localName) {
case GRAPH:
currentGraphElement.pop();
break;
case NODE:
GraphElement currentNode = currentGraphElement.pop();
if (nodes.containsKey(currentNode.id1)) {
throw new SAXException("Node with id " + currentNode.id1 + " already exists");
}
nodes.put(currentNode.id1, currentNode);
break;
case EDGE:
GraphElement currentEdge = currentGraphElement.pop();
edges.add(currentEdge);
break;
case KEY:
if (currentKey.isValid()) {
switch (currentKey.target) {
case NODE:
nodeValidKeys.put(currentKey.id, currentKey);
break;
case EDGE:
edgeValidKeys.put(currentKey.id, currentKey);
break;
case ALL:
nodeValidKeys.put(currentKey.id, currentKey);
edgeValidKeys.put(currentKey.id, currentKey);
break;
}
}
currentKey = null;
break;
case DEFAULT:
insideDefault = false;
break;
case DATA:
if (currentData.isValid()) {
currentGraphElement.peek().attributes.put(currentData.key, currentData.value);
}
insideData = false;
currentData = null;
break;
default:
break;
}
}
@Override
public void characters(char ch[], int start, int length)
throws SAXException
{
if (insideDefault) {
currentKey.defaultValue = new String(ch, start, length);
} else if (insideData) {
currentData.value = new String(ch, start, length);
}
}
@Override
public void warning(SAXParseException e)
throws SAXException
{
throw e;
}
public void error(SAXParseException e)
throws SAXException
{
throw e;
}
public void fatalError(SAXParseException e)
throws SAXException
{
throw e;
}
private String findAttribute(String localName, Attributes attributes)
{
for (int i = 0; i < attributes.getLength(); i++) {
String attrLocalName = attributes.getLocalName(i);
if (attrLocalName.equals(localName)) {
return attributes.getValue(i);
}
}
return null;
}
}
// ----- Helper classes for storing partial parser results -----
private enum KeyTarget
{
NODE,
EDGE,
ALL
}
private class Key
{
String id;
String attributeName;
String defaultValue;
KeyTarget target;
AttributeType type;
public Key(String id, String attributeName, String defaultValue, KeyTarget target)
{
this.id = id;
this.attributeName = attributeName;
this.defaultValue = defaultValue;
this.target = target;
this.type = AttributeType.STRING;
}
public boolean isValid()
{
return id != null && attributeName != null && target != null;
}
}
private class Data
{
String key;
String value;
public Data(String key, String value)
{
this.key = key;
this.value = value;
}
public boolean isValid()
{
return key != null && value != null;
}
}
private class GraphElement
{
String id1;
String id2;
Map attributes;
public GraphElement(String id1)
{
this.id1 = id1;
this.id2 = null;
this.attributes = new LinkedHashMap();
}
public GraphElement(String id1, String id2)
{
this.id1 = id1;
this.id2 = id2;
this.attributes = new LinkedHashMap();
}
}
}