/* * Copyright Aduna (http://www.aduna-software.com/) (c) 1997-2007. * * Licensed under the Aduna BSD-style license. */ package org.openrdf.rio.rdfxml.util; import java.io.Closeable; import java.io.Flushable; import java.io.IOException; import java.io.OutputStream; import java.io.Writer; import java.util.Stack; import org.openrdf.model.Literal; import org.openrdf.model.Resource; import org.openrdf.model.Statement; import org.openrdf.model.URI; import org.openrdf.model.Value; import org.openrdf.model.vocabulary.RDF; import org.openrdf.model.vocabulary.RDFS; import org.openrdf.rio.RDFHandlerException; import org.openrdf.rio.rdfxml.RDFXMLWriter; /** * An extension of RDFXMLWriter that outputs a more concise form of RDF/XML. The * resulting output is semantically equivalent to the output of an RDFXMLWriter * (it produces the same set of statements), but it is usually easier to read * for humans. * <p> * This is a quasi-streaming RDFWriter. Statements are cached as long as the * striped syntax is followed (i.e. the subject of the next statement is the * object of the previous statement) and written to the output when the stripe * is broken. * <p> * The abbreviations used are <a * href="http://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-typed-nodes">typed * node elements</a>, <a * href="http://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-empty-property-elements">empty * property elements</a> and <a * href="http://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-node-property-elements">striped * syntax</a>. Note that these abbreviations require that statements are * written in the appropriate order. * <p> * Striped syntax means that when the object of a statement is the subject of * the next statement we can nest the descriptions in each other. * <p> * Example: * * <pre> * <rdf:Seq> * <rdf:li> * <foaf:Person> * <foaf:knows> * <foaf:Person> * <foaf:mbox rdf:resource="..."/> * </foaf:Person> * </foaf:knows> * </foaf:Person> * </rdf:li> * </rdf:Seq> * </pre> * * Typed node elements means that we write out type information in the short * form of * * <pre> * <foaf:Person rdf:about="..."> * ... * </foaf:Person> * </pre> * * instead of * * <pre> * <rdf:Description rdf:about="..."> * <rdf:type rdf:resource="http://xmlns.com/foaf/0.1/Person"/> * ... * </rdf:Description> * </pre> * * Empty property elements are of the form * * <pre> * <foaf:Person> * <foaf:homepage rdf:resource="http://www.cs.vu.nl/˜marta"/> * </foaf:Person> * </pre> * * instead of * * <pre> * <foaf:Person> * <foaf:homepage> * <rdf:Description rdf:about="http://www.cs.vu.nl/˜marta"/> * <foaf:homepage> * </foaf:Person> * </pre> * * @author Peter Mika (pmika@cs.vu.nl) */ public class RDFXMLPrettyWriter extends RDFXMLWriter implements Closeable, Flushable { /*-----------* * Variables * *-----------*/ /* * We implement striped syntax by using two stacks, one for predicates and * one for subjects/objects. */ /** * Stack for remembering the nodes (subjects/objects) of statements at each * level. */ private Stack<Node> nodeStack = new Stack<Node>(); /** * Stack for remembering the predicate of statements at each level. */ private Stack<URI> predicateStack = new Stack<URI>(); /*--------------* * Constructors * *--------------*/ /** * Creates a new RDFXMLPrintWriter that will write to the supplied * OutputStream. * * @param out * The OutputStream to write the RDF/XML document to. */ public RDFXMLPrettyWriter(OutputStream out) { super(out); } /** * Creates a new RDFXMLPrintWriter that will write to the supplied Writer. * * @param out * The Writer to write the RDF/XML document to. */ public RDFXMLPrettyWriter(Writer out) { super(out); } /*---------* * Methods * *---------*/ @Override protected void writeHeader() throws IOException { // This export format needs the RDF Schema namespace to be defined: setNamespace("rdfs", RDFS.NAMESPACE, false); super.writeHeader(); } public void flush() throws IOException { if (writingStarted) { if (!headerWritten) { writeHeader(); } flushPendingStatements(); writer.flush(); } } public void close() throws IOException { try { if (writingStarted) { endRDF(); } } catch (RDFHandlerException e) { if (e.getCause() instanceof IOException) { throw (IOException)e.getCause(); } else { IOException ioe = new IOException(e.getMessage()); ioe.initCause(e); throw ioe; } } finally { writer.close(); } } @Override protected void flushPendingStatements() throws IOException { if (!nodeStack.isEmpty()) { popStacks(null); } } /** * Write out the stacks until we find subject. If subject == null, write out * the entire stack * * @param newSubject */ private void popStacks(Resource newSubject) throws IOException { // Write start tags for the part of the stacks that are not yet // written for (int i = 0; i < nodeStack.size() - 1; i++) { Node node = nodeStack.get(i); if (!node.isWritten()) { if (i > 0) { writeIndents(i * 2 - 1); URI predicate = predicateStack.get(i - 1); writeStartTag(predicate.getNamespace(), predicate.getLocalName()); writeNewLine(); } writeIndents(i * 2); writeNodeStartTag(node); node.setIsWritten(true); } } // Write tags for the top subject Node topNode = nodeStack.pop(); if (predicateStack.isEmpty()) { // write out an empty subject writeIndents(nodeStack.size() * 2); writeNodeEmptyTag(topNode); writeNewLine(); } else { URI topPredicate = predicateStack.pop(); if (!topNode.hasType()) { // we can use an abbreviated predicate writeIndents(nodeStack.size() * 2 - 1); writeAbbreviatedPredicate(topPredicate, topNode.getValue()); } else { // we cannot use an abbreviated predicate because the type needs to // written out as well writeIndents(nodeStack.size() * 2 - 1); writeStartTag(topPredicate.getNamespace(), topPredicate.getLocalName()); writeNewLine(); // write out an empty subject writeIndents(nodeStack.size() * 2); writeNodeEmptyTag(topNode); writeNewLine(); writeIndents(nodeStack.size() * 2 - 1); writeEndTag(topPredicate.getNamespace(), topPredicate.getLocalName()); writeNewLine(); } } // Write out the end tags until we find the subject while (!nodeStack.isEmpty()) { Node nextElement = nodeStack.peek(); if (nextElement.getValue().equals(newSubject)) { break; } else { nodeStack.pop(); // We have already written out the subject/object, // but we still need to close the tag writeIndents(predicateStack.size() + nodeStack.size()); writeNodeEndTag(nextElement); if (predicateStack.size() > 0) { URI nextPredicate = predicateStack.pop(); writeIndents(predicateStack.size() + nodeStack.size()); writeEndTag(nextPredicate.getNamespace(), nextPredicate.getLocalName()); writeNewLine(); } } } } @Override public void handleStatement(Statement st) throws RDFHandlerException { if (!writingStarted) { throw new RuntimeException("Document writing has not yet been started"); } Resource subj = st.getSubject(); URI pred = st.getPredicate(); Value obj = st.getObject(); try { if (!headerWritten) { writeHeader(); } if (!nodeStack.isEmpty() && !subj.equals(nodeStack.peek().getValue())) { // Different subject than we had before, empty the stack // until we find it popStacks(subj); } // Stack is either empty or contains the same subject at top if (nodeStack.isEmpty()) { // Push subject nodeStack.push(new Node(subj)); } // Stack now contains at least one element Node topSubject = nodeStack.peek(); // Check if current statement is a type statement and use a typed node // element is possible // FIXME: verify that an XML namespace-qualified name can be created // for the type URI if (pred.equals(RDF.TYPE) && obj instanceof URI && !topSubject.hasType() && !topSubject.isWritten()) { // Use typed node element topSubject.setType((URI)obj); } else { // Push predicate and object predicateStack.push(pred); nodeStack.push(new Node(obj)); } } catch (IOException e) { throw new RDFHandlerException(e); } } /** * Write out the opening tag of the subject or object of a statement up to * (but not including) the end of the tag. Used both in writeStartSubject and * writeEmptySubject. */ private void writeNodeStartOfStartTag(Node node) throws IOException { Value value = node.getValue(); if (node.hasType()) { // We can use abbreviated syntax writeStartOfStartTag(node.getType().getNamespace(), node.getType().getLocalName()); } else { // We cannot use abbreviated syntax writeStartOfStartTag(RDF.NAMESPACE, "Description"); } if (value instanceof URI) { URI uri = (URI)value; writeAttribute(RDF.NAMESPACE, "about", uri.toString()); } // else { // BNode bNode = (BNode)subj; // writeAttribute(RDF.NAMESPACE, "nodeID", bNode.getID()); // } } /** * Write out the opening tag of the subject or object of a statement. */ private void writeNodeStartTag(Node node) throws IOException { writeNodeStartOfStartTag(node); writeEndOfStartTag(); writeNewLine(); } /** * Write out the closing tag for the subject or object of a statement. */ private void writeNodeEndTag(Node node) throws IOException { if (node.getType() != null) { writeEndTag(node.getType().getNamespace(), node.getType().getLocalName()); } else { writeEndTag(RDF.NAMESPACE, "Description"); } writeNewLine(); } /** * Write out an empty tag for the subject or object of a statement. */ private void writeNodeEmptyTag(Node node) throws IOException { writeNodeStartOfStartTag(node); writeEndOfEmptyTag(); } /** * Write out an empty property element. */ private void writeAbbreviatedPredicate(URI pred, Value obj) throws IOException { writeStartOfStartTag(pred.getNamespace(), pred.getLocalName()); if (obj instanceof Resource) { Resource objRes = (Resource)obj; if (objRes instanceof URI) { URI uri = (URI)objRes; writeAttribute(RDF.NAMESPACE, "resource", uri.toString()); } // else { // BNode bNode = (BNode)objRes; // writeAttribute(RDF.NAMESPACE, "nodeID", bNode.getID()); // } writeEndOfEmptyTag(); } else if (obj instanceof Literal) { Literal objLit = (Literal)obj; // language attribute if (objLit.getLanguage() != null) { writeAttribute("xml:lang", objLit.getLanguage()); } // datatype attribute boolean isXmlLiteral = false; URI datatype = objLit.getDatatype(); if (datatype != null) { // Check if datatype is rdf:XMLLiteral isXmlLiteral = datatype.equals(RDF.XMLLITERAL); if (isXmlLiteral) { writeAttribute(RDF.NAMESPACE, "parseType", "Literal"); } else { writeAttribute(RDF.NAMESPACE, "datatype", datatype.toString()); } } writeEndOfStartTag(); // label if (isXmlLiteral) { // Write XML literal as plain XML writer.write(objLit.getLabel()); } else { writeCharacterData(objLit.getLabel()); } writeEndTag(pred.getNamespace(), pred.getLocalName()); } writeNewLine(); } protected void writeStartTag(String namespace, String localName) throws IOException { writeStartOfStartTag(namespace, localName); writeEndOfStartTag(); } /** * Writes <tt>n</tt> indents. */ protected void writeIndents(int n) throws IOException { for (int i = 0; i < n; i++) { writeIndent(); } } /*------------------* * Inner class Node * *------------------*/ private static class Node { private Value value; // type == null means that we use <rdf:Description> private URI type = null; private boolean isWritten = false; /** * Creates a new Node for the supplied Value. */ public Node(Value value) { this.value = value; } public Value getValue() { return value; } public void setType(URI type) { this.type = type; } public URI getType() { return type; } public boolean hasType() { return type != null; } public void setIsWritten(boolean isWritten) { this.isWritten = isWritten; } public boolean isWritten() { return isWritten; } } }