edu.uci.ics.jung.io.GraphMLReader Maven / Gradle / Ivy
Show all versions of jung-io Show documentation
/*
* Created on Sep 21, 2007
*
* Copyright (c) 2007, The JUNG Authors
*
* All rights reserved.
*
* This software is open-source under the BSD license; see either
* "license.txt" or
* https://github.com/jrtom/jung/blob/master/LICENSE for a description.
*/
package edu.uci.ics.jung.io;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.helpers.DefaultHandler;
import com.google.common.base.Supplier;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import edu.uci.ics.jung.algorithms.util.MapSettableTransformer;
import edu.uci.ics.jung.algorithms.util.SettableTransformer;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.Hypergraph;
import edu.uci.ics.jung.graph.util.EdgeType;
import edu.uci.ics.jung.graph.util.Pair;
/**
* Reads in data from a GraphML-formatted file and generates graphs based on
* that data. Currently supports the following parts of the GraphML
* specification:
*
* - graphs and hypergraphs
*
- directed and undirected edges
*
- graph, vertex, edge
data
* - graph, vertex, edge descriptions and
data
descriptions
* - vertex and edge IDs
*
* Each of these is exposed via appropriate get
methods.
*
* Does not currently support nested graphs or ports.
*
* Note that the user is responsible for supplying a graph
* Factory
that can support the edge types in the supplied
* GraphML file. If the graph generated by the Factory
is
* not compatible (for example: if the graph does not accept directed
* edges, and the GraphML file contains a directed edge) then the results
* are graph-implementation-dependent.
*
* @see "http://graphml.graphdrawing.org/specification.html"
*/
public class GraphMLReader, V, E> extends DefaultHandler
{
protected enum TagState {NO_TAG, VERTEX, EDGE, HYPEREDGE, ENDPOINT, GRAPH,
DATA, KEY, DESC, DEFAULT_KEY, GRAPHML, OTHER}
protected enum KeyType {NONE, VERTEX, EDGE, GRAPH, ALL}
protected SAXParser saxp;
protected EdgeType default_edgetype;
protected G current_graph;
protected V current_vertex;
protected E current_edge;
protected String current_key;
protected LinkedList current_states;
protected BiMap tag_state;
protected Supplier graph_factory;
protected Supplier vertex_factory;
protected Supplier edge_factory;
protected BiMap vertex_ids;
protected BiMap edge_ids;
protected Map> graph_metadata;
protected Map> vertex_metadata;
protected Map> edge_metadata;
protected Map vertex_desc;
protected Map edge_desc;
protected Map graph_desc;
protected KeyType key_type;
protected Collection hyperedge_vertices;
protected List graphs;
protected StringBuilder current_text = new StringBuilder();
/**
* Creates a GraphMLReader
instance with the specified
* vertex and edge factories.
*
* @param vertex_factory the vertex supplier to use to create vertex objects
* @param edge_factory the edge supplier to use to create edge objects
* @throws ParserConfigurationException if a SAX parser cannot be constructed
* @throws SAXException if the SAX parser factory cannot be constructed
*/
public GraphMLReader(Supplier vertex_factory,
Supplier edge_factory)
throws ParserConfigurationException, SAXException
{
current_vertex = null;
current_edge = null;
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
saxp = saxParserFactory.newSAXParser();
current_states = new LinkedList();
tag_state = HashBiMap.create();
tag_state.put("node", TagState.VERTEX);
tag_state.put("edge", TagState.EDGE);
tag_state.put("hyperedge", TagState.HYPEREDGE);
tag_state.put("endpoint", TagState.ENDPOINT);
tag_state.put("graph", TagState.GRAPH);
tag_state.put("data", TagState.DATA);
tag_state.put("key", TagState.KEY);
tag_state.put("desc", TagState.DESC);
tag_state.put("default", TagState.DEFAULT_KEY);
tag_state.put("graphml", TagState.GRAPHML);
this.key_type = KeyType.NONE;
this.vertex_factory = vertex_factory;
this.edge_factory = edge_factory;
}
/**
* Creates a GraphMLReader
instance that assigns the vertex
* and edge id
strings to be the vertex and edge objects,
* as well as their IDs.
* Note that this requires that (a) each edge have a valid ID, which is not
* normally a requirement for edges in GraphML, and (b) that the vertex
* and edge types be assignment-compatible with String
.
* @throws ParserConfigurationException if a SAX parser cannot be constructed
* @throws SAXException if the SAX parser factory cannot be constructed
*/
public GraphMLReader() throws ParserConfigurationException, SAXException
{
this(null, null);
}
/**
* Returns a list of the graphs parsed from the specified reader, as created by
* the specified Supplier.
* @param reader the source of the graph data in GraphML format
* @param graph_factory used to build graph instances
* @return the graphs parsed from the specified reader
* @throws IOException if an error is encountered while parsing the graph
*/
public List loadMultiple(Reader reader, Supplier graph_factory)
throws IOException
{
this.graph_factory = graph_factory;
initializeData();
clearData();
parse(reader);
return graphs;
}
/**
* Returns a list of the graphs parsed from the specified file, as created by
* the specified Supplier.
* @param filename the source of the graph data in GraphML format
* @param graph_factory used to build graph instances
* @return the graphs parsed from the specified file
* @throws IOException if an error is encountered while parsing the graph
*/
public List loadMultiple(String filename, Supplier graph_factory) throws IOException
{
return loadMultiple(new FileReader(filename), graph_factory);
}
/**
* Populates the specified graph with the data parsed from the reader.
* @param reader the source of the graph data in GraphML format
* @param g the graph instance to populate
* @throws IOException if an error is encountered while parsing the graph
*/
public void load(Reader reader, G g) throws IOException
{
this.current_graph = g;
this.graph_factory = null;
initializeData();
clearData();
parse(reader);
}
/**
* Populates the specified graph with the data parsed from the specified file.
* @param filename the source of the graph data in GraphML format
* @param g the graph instance to populate
* @throws IOException if an error is encountered while parsing the graph
*/
public void load(String filename, G g) throws IOException
{
load(new FileReader(filename), g);
}
protected void clearData()
{
this.vertex_ids.clear();
this.vertex_desc.clear();
this.edge_ids.clear();
this.edge_desc.clear();
this.graph_desc.clear();
this.hyperedge_vertices.clear();
}
/**
* This is separate from initialize() because these data structures are shared among all
* graphs loaded (i.e., they're defined inside graphml
rather than graph
.
*/
protected void initializeData()
{
this.vertex_ids = HashBiMap.create();
this.vertex_desc = new HashMap();
this.vertex_metadata = new HashMap>();
this.edge_ids = HashBiMap.create();
this.edge_desc = new HashMap();
this.edge_metadata = new HashMap>();
this.graph_desc = new HashMap();
this.graph_metadata = new HashMap>();
this.hyperedge_vertices = new ArrayList();
}
protected void parse(Reader reader) throws IOException
{
try
{
saxp.parse(new InputSource(reader), this);
reader.close();
}
catch (SAXException saxe)
{
throw new IOException(saxe.getMessage());
}
}
@Override
public void startElement(String uri, String name, String qName, Attributes atts) throws SAXNotSupportedException
{
String tag = qName.toLowerCase();
TagState state = tag_state.get(tag);
if (state == null)
state = TagState.OTHER;
switch (state)
{
case GRAPHML:
break;
case VERTEX:
if (this.current_graph == null)
throw new SAXNotSupportedException("Graph must be defined prior to elements");
if (this.current_edge != null || this.current_vertex != null)
throw new SAXNotSupportedException("Nesting elements not supported");
createVertex(atts);
break;
case ENDPOINT:
if (this.current_graph == null)
throw new SAXNotSupportedException("Graph must be defined prior to elements");
if (this.current_edge == null)
throw new SAXNotSupportedException("No edge defined for endpoint");
if (this.current_states.getFirst() != TagState.HYPEREDGE)
throw new SAXNotSupportedException("Endpoints must be defined inside hyperedge");
Map endpoint_atts = getAttributeMap(atts);
String node = endpoint_atts.remove("node");
if (node == null)
throw new SAXNotSupportedException("Endpoint must include an 'id' attribute");
V v = vertex_ids.inverse().get(node);
if (v == null)
throw new SAXNotSupportedException("Endpoint refers to nonexistent node ID: " + node);
this.current_vertex = v;
hyperedge_vertices.add(v);
break;
case EDGE:
case HYPEREDGE:
if (this.current_graph == null)
throw new SAXNotSupportedException("Graph must be defined prior to elements");
if (this.current_edge != null || this.current_vertex != null)
throw new SAXNotSupportedException("Nesting elements not supported");
createEdge(atts, state);
break;
case GRAPH:
if (this.current_graph != null && graph_factory != null)
throw new SAXNotSupportedException("Nesting graphs not currently supported");
// graph Supplier is null if there's only one graph
if (graph_factory != null)
current_graph = graph_factory.get();
// reset all non-key data structures (to avoid collisions between different graphs)
clearData();
// set up default direction of edges
Map graph_atts = getAttributeMap(atts);
String default_direction = graph_atts.remove("edgedefault");
if (default_direction == null)
throw new SAXNotSupportedException("All graphs must specify a default edge direction");
if (default_direction.equals("directed"))
this.default_edgetype = EdgeType.DIRECTED;
else if (default_direction.equals("undirected"))
this.default_edgetype = EdgeType.UNDIRECTED;
else
throw new SAXNotSupportedException("Invalid or unrecognized default edge direction: " + default_direction);
// put remaining attribute/value pairs in graph_data
addExtraData(graph_atts, graph_metadata, current_graph);
break;
case DATA:
if (this.current_states.contains(TagState.DATA))
throw new SAXNotSupportedException("Nested data not supported");
handleData(atts);
break;
case KEY:
createKey(atts);
break;
default:
break;
}
current_states.addFirst(state);
}
/**
*
* @param
* @param atts
* @param metadata_map
* @param current_elt
*/
private void addExtraData(Map atts,
Map> metadata_map, T current_elt)
{
// load in the default values; these override anything that might
// be in the attribute map (because that's not really a proper
// way to associate data)
for (Map.Entry> entry: metadata_map.entrySet())
{
GraphMLMetadata gmlm = entry.getValue();
if (gmlm.default_value != null)
{
SettableTransformer st =
(SettableTransformer)gmlm.transformer;
st.set(current_elt, gmlm.default_value);
}
}
// place remaining items in data
for (Map.Entry entry : atts.entrySet())
{
String key = entry.getKey();
GraphMLMetadata key_data = metadata_map.get(key);
SettableTransformer st;
if (key_data != null)
{
// if there's a default value, don't override it
if (key_data.default_value != null)
continue;
st = (SettableTransformer)key_data.transformer;
}
else
{
st = new MapSettableTransformer(
new HashMap());
key_data = new GraphMLMetadata(null, null, st);
metadata_map.put(key, key_data);
}
st.set(current_elt, entry.getValue());
}
}
@Override
public void characters(char[] ch, int start, int length) throws SAXNotSupportedException
{
this.current_text.append(new String(ch, start, length));
}
protected void addDatum(Map> metadata,
T current_elt, String text) throws SAXNotSupportedException
{
if (metadata.containsKey(this.current_key))
{
SettableTransformer st =
(SettableTransformer)(metadata.get(this.current_key).transformer);
st.set(current_elt, text);
}
else
throw new SAXNotSupportedException("key " + this.current_key +
" not valid for element " + current_elt);
}
@Override
public void endElement(String uri, String name, String qName) throws SAXNotSupportedException
{
String text = current_text.toString().trim();
current_text.setLength(0);
String tag = qName.toLowerCase();
TagState state = tag_state.get(tag);
if (state == null)
state = TagState.OTHER;
if (state == TagState.OTHER)
return;
if (state != current_states.getFirst())
throw new SAXNotSupportedException("Unbalanced tags: opened " +
tag_state.inverse().get(current_states.getFirst()) +
", closed " + tag);
switch(state)
{
case VERTEX:
case ENDPOINT:
current_vertex = null;
break;
case EDGE:
current_edge = null;
break;
case HYPEREDGE:
current_graph.addEdge(current_edge, hyperedge_vertices);
hyperedge_vertices.clear();
current_edge = null;
break;
case GRAPH:
current_graph = null;
break;
case KEY:
current_key = null;
break;
case DESC:
switch (this.current_states.get(1)) // go back one
{
case GRAPH:
graph_desc.put(current_graph, text);
break;
case VERTEX:
case ENDPOINT:
vertex_desc.put(current_vertex, text);
break;
case EDGE:
case HYPEREDGE:
edge_desc.put(current_edge, text);
break;
case DATA:
switch (key_type)
{
case GRAPH:
graph_metadata.get(current_key).description = text;
break;
case VERTEX:
vertex_metadata.get(current_key).description = text;
break;
case EDGE:
edge_metadata.get(current_key).description = text;
break;
case ALL:
graph_metadata.get(current_key).description = text;
vertex_metadata.get(current_key).description = text;
edge_metadata.get(current_key).description = text;
break;
default:
throw new SAXNotSupportedException("Invalid key type" +
" specified for default: " + key_type);
}
break;
default:
break;
}
break;
case DATA:
this.key_type = KeyType.NONE;
switch (this.current_states.get(1))
{
case GRAPH:
addDatum(graph_metadata, current_graph, text);
break;
case VERTEX:
case ENDPOINT:
addDatum(vertex_metadata, current_vertex, text);
break;
case EDGE:
case HYPEREDGE:
addDatum(edge_metadata, current_edge, text);
break;
default:
break;
}
break;
case DEFAULT_KEY:
if (this.current_states.get(1) != TagState.KEY)
throw new SAXNotSupportedException("'default' only defined in context of 'key' tag: " +
"stack: " + current_states.toString());
switch (key_type)
{
case GRAPH:
graph_metadata.get(current_key).default_value = text;
break;
case VERTEX:
vertex_metadata.get(current_key).default_value = text;
break;
case EDGE:
edge_metadata.get(current_key).default_value = text;
break;
case ALL:
graph_metadata.get(current_key).default_value = text;
vertex_metadata.get(current_key).default_value = text;
edge_metadata.get(current_key).default_value = text;
break;
default:
throw new SAXNotSupportedException("Invalid key type" +
" specified for default: " + key_type);
}
break;
default:
break;
}
current_states.removeFirst();
}
protected Map getAttributeMap(Attributes atts)
{
Map att_map = new HashMap();
for (int i = 0; i < atts.getLength(); i++)
att_map.put(atts.getQName(i), atts.getValue(i));
return att_map;
}
protected void handleData(Attributes atts) throws SAXNotSupportedException
{
switch (this.current_states.getFirst())
{
case GRAPH:
break;
case VERTEX:
case ENDPOINT:
break;
case EDGE:
break;
case HYPEREDGE:
break;
default:
throw new SAXNotSupportedException("'data' tag only defined " +
"if immediately containing tag is 'graph', 'node', " +
"'edge', or 'hyperedge'");
}
this.current_key = getAttributeMap(atts).get("key");
if (this.current_key == null)
throw new SAXNotSupportedException("'data' tag requires a key specification");
if (this.current_key.equals(""))
throw new SAXNotSupportedException("'data' tag requires a non-empty key");
if (!getGraphMetadata().containsKey(this.current_key) &&
!getVertexMetadata().containsKey(this.current_key) &&
!getEdgeMetadata().containsKey(this.current_key))
{
throw new SAXNotSupportedException("'data' tag's key specification must reference a defined key");
}
}
protected void createKey(Attributes atts) throws SAXNotSupportedException
{
Map key_atts = getAttributeMap(atts);
String id = key_atts.remove("id");
String for_type = key_atts.remove("for");
if (for_type == null || for_type.equals("") || for_type.equals("all"))
{
vertex_metadata.put(id,
new GraphMLMetadata(null, null,
new MapSettableTransformer(new HashMap())));
edge_metadata.put(id,
new GraphMLMetadata(null, null,
new MapSettableTransformer(new HashMap())));
graph_metadata.put(id,
new GraphMLMetadata(null, null,
new MapSettableTransformer(new HashMap())));
key_type = KeyType.ALL;
}
else
{
TagState type = tag_state.get(for_type);
switch (type)
{
case VERTEX:
vertex_metadata.put(id,
new GraphMLMetadata(null, null,
new MapSettableTransformer(new HashMap())));
key_type = KeyType.VERTEX;
break;
case EDGE:
case HYPEREDGE:
edge_metadata.put(id,
new GraphMLMetadata(null, null,
new MapSettableTransformer(new HashMap())));
key_type = KeyType.EDGE;
break;
case GRAPH:
graph_metadata.put(id,
new GraphMLMetadata(null, null,
new MapSettableTransformer(new HashMap())));
key_type = KeyType.GRAPH;
break;
default:
throw new SAXNotSupportedException(
"Invalid metadata target type: " + for_type);
}
}
this.current_key = id;
}
@SuppressWarnings("unchecked")
protected void createVertex(Attributes atts) throws SAXNotSupportedException
{
Map vertex_atts = getAttributeMap(atts);
String id = vertex_atts.remove("id");
if (id == null)
throw new SAXNotSupportedException("node attribute list missing " +
"'id': " + atts.toString());
V v = vertex_ids.inverse().get(id);
if (v == null)
{
if (vertex_factory != null)
v = vertex_factory.get();
else
v = (V)id;
vertex_ids.put(v, id);
this.current_graph.addVertex(v);
// put remaining attribute/value pairs in vertex_data
addExtraData(vertex_atts, vertex_metadata, v);
}
else
throw new SAXNotSupportedException("Node id \"" + id +
" is a duplicate of an existing node ID");
this.current_vertex = v;
}
@SuppressWarnings("unchecked")
protected void createEdge(Attributes atts, TagState state)
throws SAXNotSupportedException
{
Map edge_atts = getAttributeMap(atts);
String id = edge_atts.remove("id");
E e;
if (edge_factory != null)
e = edge_factory.get();
else
if (id != null)
e = (E)id;
else
throw new IllegalArgumentException("If no edge Supplier is supplied, " +
"edge id may not be null: " + edge_atts);
if (id != null)
{
if (edge_ids.containsKey(e))
throw new SAXNotSupportedException("Edge id \"" + id +
"\" is a duplicate of an existing edge ID");
edge_ids.put(e, id);
}
if (state == TagState.EDGE)
assignEdgeSourceTarget(e, atts, edge_atts); //, id);
// put remaining attribute/value pairs in edge_data
addExtraData(edge_atts, edge_metadata, e);
this.current_edge = e;
}
protected void assignEdgeSourceTarget(E e, Attributes atts,
Map edge_atts)//, String id)
throws SAXNotSupportedException
{
String source_id = edge_atts.remove("source");
if (source_id == null)
throw new SAXNotSupportedException("edge attribute list missing " +
"'source': " + atts.toString());
V source = vertex_ids.inverse().get(source_id);
if (source == null)
throw new SAXNotSupportedException("specified 'source' attribute " +
"\"" + source_id + "\" does not match any node ID");
String target_id = edge_atts.remove("target");
if (target_id == null)
throw new SAXNotSupportedException("edge attribute list missing " +
"'target': " + atts.toString());
V target = vertex_ids.inverse().get(target_id);
if (target == null)
throw new SAXNotSupportedException("specified 'target' attribute " +
"\"" + target_id + "\" does not match any node ID");
String directed = edge_atts.remove("directed");
EdgeType edge_type;
if (directed == null)
edge_type = default_edgetype;
else if (directed.equals("true"))
edge_type = EdgeType.DIRECTED;
else if (directed.equals("false"))
edge_type = EdgeType.UNDIRECTED;
else
throw new SAXNotSupportedException("Unrecognized edge direction specifier 'direction=\"" +
directed + "\"': " + "source: " + source_id + ", target: " + target_id);
if (current_graph instanceof Graph)
((Graph)this.current_graph).addEdge(e, source, target,
edge_type);
else
this.current_graph.addEdge(e, new Pair(source, target));
}
/**
* @return a bidirectional map relating vertices and IDs.
*/
public BiMap getVertexIDs()
{
return vertex_ids;
}
/**
* Returns a bidirectional map relating edges and IDs.
* This is not guaranteed to always be populated (edge IDs are not
* required in GraphML files.
* @return a bidirectional map relating edges and IDs.
*/
public BiMap getEdgeIDs()
{
return edge_ids;
}
/**
* @return a map from graph type name to type metadata
*/
public Map> getGraphMetadata()
{
return graph_metadata;
}
/**
* @return a map from vertex type name to type metadata
*/
public Map> getVertexMetadata()
{
return vertex_metadata;
}
/**
* @return a map from edge type name to type metadata
*/
public Map> getEdgeMetadata()
{
return edge_metadata;
}
/**
* @return a map from graphs to graph descriptions
*/
public Map getGraphDescriptions()
{
return graph_desc;
}
/**
* @return a map from vertices to vertex descriptions
*/
public Map getVertexDescriptions()
{
return vertex_desc;
}
/**
* @return a map from edges to edge descriptions
*/
public Map getEdgeDescriptions()
{
return edge_desc;
}
}