/*
* The contents of this file are subject to the Mozilla Public License
* Version 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
* the License for the specific language governing rights and limitations
* under the License.
*
* The Original Code is the Kowari Metadata Store.
*
* The Initial Developer of the Original Code is Plugged In Software Pty
* Ltd (http://www.pisoftware.com, mailto:info@pisoftware.com). Portions
* created by Plugged In Software Pty Ltd are Copyright (C) 2001,2002
* Plugged In Software Pty Ltd. All Rights Reserved.
*
* Contributor(s): N/A.
*
* [NOTE: The text of this Exhibit A may differ slightly from the text
* of the notices in the Source Code files of the Original Code. You
* should use the text of this Exhibit A rather than the text found in the
* Original Code Source Code for Your Modifications.]
*
*/
package org.mulgara.client.jrdf.writer;
// Java 2 standard packages
import java.io.*;
import java.net.*;
import java.util.*;
// Apache packages
import org.apache.log4j.Logger;
import org.apache.xerces.util.EncodingMap;
// JRDF
import org.jrdf.vocabulary.*;
// Local packages
import org.jrdf.graph.GraphException;
import org.jrdf.util.ClosableIterator;
import org.jrdf.graph.*;
//import org.mulgara.store.exporter.GraphXMLWriter;
import org.mulgara.client.jrdf.util.*;
import org.mulgara.util.StringUtil;
/**
* A Writer used to write RDF/XML for a Client Graph. Client-side in memory
* version.
*
* @created 2004-08-02
*
* @author <a href="mailto:robert.turner@tucanatech.com">Robert Turner</a>
*
* @version $Revision: 1.8 $
*
* @modified $Date: 2005/01/05 04:57:37 $
*
* @maintenanceAuthor $Author: newmana $
*
* @company <A href="mailto:info@PIsoftware.com">Plugged In Software</A>
*
* @copyright ©2001 <a href="http://www.pisoftware.com/">Plugged In
* Software Pty Ltd</a>
*
* @licence <a href="{@docRoot}/../../LICENCE">Mozilla Public License v1.1</a>
*/
public class MemoryXMLWriter {
/**
* Logger. This is named after the class.
*/
@SuppressWarnings("unused")
private final static Logger log = Logger.getLogger(MemoryXMLWriter.class.getName());
/** Prefix used to abbreviate RDF Namespace */
private static final String RDF_PREFIX = "rdf";
/** Prefix used to abbreviate RDFS Namespace */
private static final String RDFS_PREFIX = "rdfs";
/** Convenience reference to the new line character */
private static final String NEWLINE = System.getProperty("line.separator");
/**
* Map used to replace commonly used namespaces with prefixes. Values can be
* either Strings or URIs, so a general type is used.
*/
protected Map<String,Object> namespaces = null;
/**
* Default Constructor
*/
public MemoryXMLWriter() {
}
/**
* Writes the contents of the JRDFGraph to a PrintWriter in RDF/XML format.
*
* @param graph Graph containing the Statements to be written.
* @param writer PrintWriter Where to write the statements.
* @throws GraphException
*/
synchronized public void write(Graph graph, PrintWriter writer) throws
GraphException {
//write Header, Body and Footer
try {
//initialize the namespaces first
this.populateNamespaces(graph);
//write document
this.writeHeader(writer);
this.writeBody(graph, writer);
this.writeFooter(writer);
}
catch (IOException ioException) {
throw new GraphException("Could not write Graph.", ioException);
}
}
/**
* Writes the contents of the JRDFGraph to a PrintWriter in RDF/XML format
* with the encoding specified in the opening XML tag.
*
* @param graph JRDFGraph
* @param writer PrintWriter
* @throws GraphException
*/
synchronized public void write(Graph graph, OutputStreamWriter writer) throws
GraphException {
//write Header, Body and Footer
try {
//wrap writer and enable auto flushing
PrintWriter out = new PrintWriter(writer, true);
//initialize the namespaces first
this.populateNamespaces(graph);
//write document
this.writeHeader(writer);
this.writeBody(graph, out);
this.writeFooter(out);
}
catch (IOException ioException) {
throw new GraphException("Could not write Graph.", ioException);
}
}
/**
* Writes the XML Declaration and the opening RDF tag to the print Writer.
* Encoding not specified.
*
* @param out PrintWriter
* @throws IOException
*/
protected void writeHeader(PrintWriter out) throws IOException {
//validate
if (out != null) {
//print opening tags
out.println("<?xml version=\"1.0\"?>");
//print the opening RDF tag (including namespaces)
this.writeRDFHeader(out);
}
else {
throw new IllegalArgumentException("Cannot write to null Writer.");
}
}
/**
* Writes the XML Declaration and the opening RDF tag to the print Writer.
* Encoding attribute is specified as the encoding argument.
*
* @param out PrintWriter
* @throws IOException
*/
protected void writeHeader(OutputStreamWriter out) throws IOException {
//validate
if (out != null) {
//wrapper for output stream writer (enable autoflushing)
PrintWriter writer = new PrintWriter(out, true);
//get encoding from the Encoding map
String encoding = EncodingMap.getJava2IANAMapping(out.getEncoding());
//only insert encoding if there is a value
if (encoding != null) {
//print opening tags <?xml version="1.0" encoding=*encoding*?>
writer.println("<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>");
}
else {
//print opening tags <?xml version="1.0"?>
writer.println("<?xml version=\"1.0\"?>");
}
//print the Entities
this.writeXMLEntities(writer);
//print the opening RDF tag (including namespaces)
this.writeRDFHeader(writer);
}
else {
throw new IllegalArgumentException("Cannot write to null Writer.");
}
}
/**
* Writes the XML Entities (used for namespaces) to the print Writer.
*
* @param out PrintWriter
* @throws IOException
*/
protected void writeXMLEntities(PrintWriter out) throws IOException {
//validate
if (out != null) {
//print opening DOCTYPE DECLARATION tag
out.print(NEWLINE + "<!DOCTYPE rdf:RDF [");
//print namespaces
Set<String> keys = this.namespaces.keySet();
if (keys != null) {
Iterator<String> keyIter = keys.iterator();
String currentKey = null;
Object currentValue = null;
while (keyIter.hasNext()) {
currentKey = keyIter.next();
currentValue = this.namespaces.get(currentKey);
if ( (currentKey != null)
&& (currentValue != null)) {
//write as: <!ENTITY ns 'http://example.org/abc#'>
out.print(NEWLINE + " <!ENTITY " + currentKey + " '" + currentValue + "'>");
}
}
}
//close the opening tag (add a space for readability)
out.print("]>" + NEWLINE + NEWLINE);
} else {
throw new IllegalArgumentException("Cannot write to null Writer.");
}
}
/**
* Writes the opening RDF tag (with namespaces) to the print Writer.
*
* @param out PrintWriter
* @throws IOException
*/
protected void writeRDFHeader(PrintWriter out) throws IOException {
//validate
if (out != null) {
//print opening RDF tag (including namespaces)
out.print("<rdf:RDF ");
//print namespaces
Set<String> keys = this.namespaces.keySet();
if (keys != null) {
Iterator<String> keyIter = keys.iterator();
String currentKey = null;
Object currentValue = null;
while (keyIter.hasNext()) {
currentKey = keyIter.next();
currentValue = this.namespaces.get(currentKey);
if ( (currentKey != null)
&& (currentValue != null)) {
//use entities: xmlns:ns="&ns;"
out.print(NEWLINE + " xmlns:" + currentKey + "=\"&" + currentKey + ";\"");
}
}
}
//close the opening tag (add a space for readability)
out.print(">" + NEWLINE + NEWLINE);
} else {
throw new IllegalArgumentException("Cannot write to null Writer.");
}
}
/**
* Writes the Graph's statements as RDF/XML to the print Writer.
*
* @param graph Graph
* @param out PrintWriter
* @throws IOException
* @throws GraphException
*/
protected void writeBody(Graph graph, PrintWriter out) throws IOException,
GraphException {
//validate
if ( (out != null)
&& (graph != null)) {
//iterator used to access subjects
ClosableIterator<Triple> subjectIter = ClientGraphUtil.getUniqueSubjects(graph);
//write every (unique) subject
if (subjectIter != null) {
//current Triple
Triple triple = null;
//current Subject
SubjectNode subject = null;
while (subjectIter.hasNext()) {
//get the next triple
triple = subjectIter.next();
if (triple != null) {
subject = triple.getSubject();
this.writeSubject(graph, subject, out);
}
}
//close the Iterator
subjectIter.close();
}
} else {
//message for exception to be thrown
String message = "Could not write Graph. Invlaid arguments provided. ";
if (out == null) {
message += "Writer is null. ";
}
if (graph == null) {
message += "Graph is null. ";
}
throw new IllegalArgumentException(message);
}
}
/**
* Writes the closing RDF tag to the writer.
*
* @param out PrintWriter
* @throws IOException
*/
protected void writeFooter(PrintWriter out) throws IOException {
//validate
if (out != null) {
//print closing RDF tag
out.println("</" + RDF_PREFIX + ":RDF>");
} else {
throw new IllegalArgumentException("Cannot write to null Writer.");
}
}
/**
* Writes a Subject to the writer
*
* @param graph JRDFGraph
* @param subject SubjectNode
* @param writer PrintWriter
* @throws GraphException
*/
protected void writeSubject(Graph graph, SubjectNode subject,
PrintWriter writer) throws GraphException {
//opening subject tag (includes literals)
this.writeOpeningTag(graph, subject, writer);
//subject contents
this.writeSubjectBody(graph, subject, writer);
//closing subject tag
this.writeClosingTag(graph, subject, writer);
//put a space between Subjects
writer.println("");
}
/**
* Writes the opening tag for the subject
*
* @param graph JRDFGraph
* @param subject SubjectNode
* @param writer PrintWriter
* @throws GraphException
*/
protected void writeOpeningTag(Graph graph, SubjectNode subject, PrintWriter writer) throws
GraphException {
if (writer != null) {
//write the subject type and rdf:about
if (subject != null) {
//open tag - BlankNodes are a special case
if (subject instanceof BlankNode) {
this.writeOpeningTag(graph, (BlankNode) subject, writer);
} else {
writer.print(" <" + RDF_PREFIX + ":Description " + RDF_PREFIX +
":about=\"" + this.getNodeString(subject) + "\">" +
NEWLINE);
}
} else {
throw new IllegalArgumentException("Could not write opening tag for subject. Subject Node is null.");
}
}
}
/**
* Writes the opening tag for a blank node.
*
* @param graph JRDFGraph
* @param subject BlankNode
* @param writer PrintWriter
* @throws GraphException
*/
protected void writeOpeningTag(Graph graph, BlankNode subject,
PrintWriter writer) throws GraphException {
ObjectNode subjectType = this.getSubjectType(graph, subject);
//only display as a subject if it can be found
if (subjectType != null) {
//opening tag
writer.print(" <" + this.getURI(subjectType) + ">" + NEWLINE);
} else {
//opening tag
writer.print(" <" + RDF_PREFIX + ":Description>" + NEWLINE);
}
}
/**
* Writes the opening tag for the subject
*
* @param graph JRDFGraph
* @param subject SubjectNode
* @param writer PrintWriter
* @throws GraphException
*/
protected void writeClosingTag(Graph graph, SubjectNode subject, PrintWriter writer) throws
GraphException {
//Blank Nodes are written differently
if (subject instanceof BlankNode) {
this.writeClosingTag(graph, (BlankNode) subject, writer);
} else {
//closing tag
writer.print(" </" + RDF_PREFIX + ":Description>" + NEWLINE);
}
}
/**
* Writes the opening tag for the subject
*
* @param graph JRDFGraph
* @param subject SubjectNode
* @param writer PrintWriter
* @throws GraphException
*/
protected void writeClosingTag(Graph graph, BlankNode subject, PrintWriter writer) throws
GraphException {
ObjectNode subjectType = this.getSubjectType(graph, subject);
//only display as a subject if it can be found
if (subjectType != null) {
//closing tag
writer.print(" </" + this.getURI(subjectType) + ">" + NEWLINE);
} else {
//closing tag
writer.print(" </" + RDF_PREFIX + ":Description>" + NEWLINE);
}
}
/**
* Writes the Resources for a subject (one per line).
*
* eg. <predicateURI rdf:resource="resourceURI"/>
* (<predicateURI rdf:nodeID="resourceURI"/> for Blank Nodes)
*
* @param graph Graph
* @param subject SubjectNode
* @param writer PrintWriter
* @throws GraphException
*/
protected void writeSubjectBody(Graph graph, SubjectNode subject,
PrintWriter writer) throws GraphException {
//get all statements for the Subject
ClosableIterator<Triple> subjectIter = graph.find(subject, null, null);
//order the statements by predicate, then object (for sequences).
ClosableIterator<Triple> tripleIter = ClientGraphUtil.orderBySPO(subjectIter);
if (tripleIter != null) {
//current Triple
Object triple = null;
//current predicate
PredicateNode predicate = null;
//current object (URIReference)
ObjectNode object = null;
//evaluate all triples
while (tripleIter.hasNext()) {
triple = tripleIter.next();
//validate triple
if ( (triple != null)
&& (triple instanceof Triple)) {
//retrieve Predicate and Object
predicate = ( (Triple) triple).getPredicate();
object = ( (Triple) triple).getObject();
//Literals and Resources are written differently
if (object != null) {
this.writeStatement(graph, subject, predicate, object, writer);
}
}
}
//close the Iterator
tripleIter.close();
}
}
/**
* Used to write Resources for a Subject. Literals will by-pass this method
* and use "Literal" method.
*
* @param predicate PredicateNode
* @param object ObjectNode
* @param writer PrintWriter
* @throws GraphException
*/
protected void writeStatement(Graph graph, SubjectNode subject,
PredicateNode predicate, ObjectNode object,
PrintWriter writer) throws GraphException {
//Literals are written differently
if (object instanceof Literal) {
this.writeStatement(graph, subject, predicate, (Literal) object, writer);
} else if (object instanceof BlankNode) {
//write as: <predicateURI> *blank node as subject* </predicateURI>
writer.println(" <" + this.getURI(predicate) + ">");
//write blank node as a "subject"
this.writeSubject(graph, (BlankNode) object, writer);
writer.println(" </" + this.getURI(predicate) + ">");
} else if (subject instanceof BlankNode) {
//predicatNode representing RDF Type
PredicateNode rdfTypeNode = null;
try {
rdfTypeNode = graph.getElementFactory().createResource(RDF.TYPE);
} catch (GraphElementFactoryException factoryException) {
throw new GraphException("Could not create RDF Type node.", factoryException);
}
//do not write the RDF Type element
if (!rdfTypeNode.equals(predicate)) {
//write as: <predicateURI rdf:resource="resourceURI"/>
writer.println(" <" + this.getURI(predicate) + " " + RDF_PREFIX +
":resource=\"" + this.getNodeString(object) + "\"/>");
}
} else {
//write as: <predicateURI rdf:resource="resourceURI"/>
writer.println(" <" + this.getURI(predicate) + " " + RDF_PREFIX +
":resource=\"" + this.getNodeString(object) + "\"/>");
}
}
/**
* Used to write Resources for a Subject. Resources will use "ObjectNode"
* method.
*
* @param predicate PredicateNode
* @param object Literal
* @param writer PrintWriter
* @throws GraphException
*/
protected void writeStatement(Graph graph, SubjectNode subject,
PredicateNode predicate, Literal object,
PrintWriter writer) throws GraphException {
//determine if the Literal has a datatype
URI datatype = object.getDatatypeURI();
// Get the lexical form of the literal
String literalObject = object.getLexicalForm();
// Create the StringBuffer to hold the resultant string
StringBuffer buffer = new StringBuffer();
// Escape the XML string
StringUtil.quoteAV(literalObject, buffer);
if (datatype != null) {
//write as: <predicateURI rdf:datatype="datatype">"Literal value"
// </predicateURI>
writer.println(" <" + this.getURI(predicate) + " " + RDF_PREFIX +
":datatype=\"" + datatype + "\">" +
buffer.toString() +
"</" + this.getURI(predicate) + ">");
} else {
//write as: <predicateURI>"Literal value"</predicateURI>
writer.println(" <" + this.getURI(predicate) + ">" +
buffer.toString() + "</" +
this.getURI(predicate) + ">");
}
}
/**
* Finds the RDF Type for a given subject.
*
* @param graph Graph
* @param subject SubjectNode
* @throws GraphException
* @return ObjectNode
*/
protected ObjectNode getSubjectType(Graph graph, SubjectNode subject) throws GraphException {
//value to be returned
ObjectNode type = null;
//validate graph
if ( (graph == null)) {
throw new IllegalArgumentException("Graph argument must not be null.");
}
//predicatNode representing RDF Type
PredicateNode rdfType = null;
try {
rdfType = graph.getElementFactory().createResource(RDF.TYPE);
} catch (GraphElementFactoryException factoryException) {
throw new GraphException("Could not create RDF Type node.", factoryException);
}
//get the Subject's RDF type
ClosableIterator<Triple> typeIter = graph.find(subject, rdfType, null);
if (typeIter != null) {
Object typeTriple = null;
//validate "first" triple and extract it's object (rdf type)
if (typeIter.hasNext()) {
typeTriple = typeIter.next();
if (typeTriple != null) {
type = ( (Triple) typeTriple).getObject();
} else {
throw new GraphException("Could not find RDF type for Subject: " +
subject + " . Invalid Triple returned.");
}
}
//close the Iterator
typeIter.close();
}
return type;
}
/**
* Returns a URI that represents the Node.
*
* @param node the node representing the URI.
* @throws GraphException
* @return URI
*/
protected String getURI(Node node) throws GraphException {
//value to be returned
String uri = null;
//determine type of subject node and create uri from it
if (node != null) {
try {
if (node instanceof URIReference) {
uri = ( (URIReference) node).getURI().toString();
} else if (node instanceof BlankNode) {
uri = new URI("#" + ( (BlankNode) node).toString()).toString();
} else {
uri = node.toString();
}
} catch (URISyntaxException uriException) {
throw new GraphException("Could not get URI for Node: " + node + ".", uriException);
}
} else {
throw new GraphException("Could not get URI for Node: " + node + ". Node is null.");
}
//return the URI with any namespaces replaced with prefixes
return this.replaceNamespace(uri);
}
/**
* Returns a String representation of an ObjectNode. Object values do not have
* to be escaped.
*
* @param node Node
* @throws GraphException
* @return String
*/
protected String getNodeString(Node node) throws GraphException {
//value to be returned
String object = null;
//determine type of subject node and create uri from it
if (node != null) {
try {
if (node instanceof URIReference) {
object = ( (URIReference) node).getURI().toString();
} else if (node instanceof BlankNode) {
object = new URI("#" + ( (BlankNode) node).toString()).toString();
} else if (node instanceof Literal) {
object = ((Literal) node).getLexicalForm();
} else {
object = node.toString();
}
} catch (URISyntaxException uriException) {
throw new GraphException("Could not get String for ObjectNode: " + node + ".", uriException);
}
} else {
throw new GraphException("Could not get String for ObjectNode: " + node + ". ObjectNode is null.");
}
return object;
}
/**
* If the URI contains the URI of a known namespace, it is replaced.
*
* @param original original URI.
* @return new URI with namespace references.
*/
protected String replaceNamespace(String original) throws GraphException {
//value to be returned
String uri = original;
//validate URI (only replace uri's with fragments)
if (original != null) {
//replace any URI occurances with namespace prefixes
Set<String> keys = this.namespaces.keySet();
Iterator<String> keyIter = keys.iterator();
String currentKey = null;
Object currentValue = null;
if (keyIter != null) {
while (keyIter.hasNext()) {
currentKey = keyIter.next();
currentValue = this.namespaces.get(currentKey);
//validate the Objects
if ((currentKey != null)
&& (currentValue != null)) {
// int index = uriAsString.indexOf(currentValue.toString());
//if the entire namespace is used, replace it with an entity
if (original.equals(currentValue.toString())) {
uri = "&" + currentKey + ";";
} else if (original.startsWith(currentValue.toString())) {
//replace with namespace
uri = original.replaceAll(currentValue.toString(), currentKey + ":");
}
}
}
}
}
//return the URI with any collection/container items renamed
return replaceCollection(uri);
}
/**
* If the URI has a fragment representing a collection (eg. Bag) item, it is
* replaced with li.
*
* @param original original URI.
* @return new URI with any necessary li.
*/
protected String replaceCollection(String original) throws GraphException {
//value to be returned
String uri = original;
//validate URI
if (original != null) {
uri = original.replaceAll("_[0-9]+", "li");
}
return uri;
}
/**
* Populates the namespaces map with default namespaces and namespaces used
* by the graph.
*
* @param graph Graph
* @throws GraphException
*/
protected void populateNamespaces(Graph graph) throws GraphException {
//default namespaces
this.namespaces = new HashMap<String,Object>();
this.namespaces.put(RDF_PREFIX, RDF.BASE_URI);
this.namespaces.put(RDFS_PREFIX, RDFS.BASE_URI);
this.namespaces.put("owl", "http://www.w3.org/2002/07/owl#");
this.namespaces.put("dc", "http://purl.org/dc/elements/1.1/");
//validate graph before reading
if (graph == null) {
throw new IllegalArgumentException("Graph argument is null.");
}
//get all statements
ClosableIterator<Triple> tripleIter = graph.find(null, null, null);
if (tripleIter != null) {
//current Triple/Predicate
Triple triple = null;
SubjectNode subject = null;
PredicateNode predicate = null;
ObjectNode object = null;
while (tripleIter.hasNext()) {
//get the next triple
triple = (Triple) tripleIter.next();
if (triple != null) {
//evaluate subject
subject = triple.getSubject();
if (subject instanceof URIReference) {
this.addNamespaceURI(((URIReference) subject).getURI());
}
//evaluate predicate (must be URIReference)
predicate = triple.getPredicate();
this.addNamespaceURI(((URIReference) predicate).getURI());
//evaluate object
object = triple.getObject();
if (object instanceof URIReference) {
this.addNamespaceURI(((URIReference) object).getURI());
}
}
}
//close the Iterator
tripleIter.close();
}
}
/**
* Evaluates a URI and adds it to the namespace map as a namespace.
*
* @param uri URI
*/
protected void addNamespaceURI(URI uri) {
if (uri == null) {
throw new IllegalArgumentException("URI argument is null.");
}
//extract URI without fragment
String uriString = uri.toString();
String newURI = null;
if (uriString != null) {
//determine what comes last a '#' or '/'
int hashindex = uriString.lastIndexOf('#');
int slashindex = uriString.lastIndexOf('/');
//validate (URI must contain a forward slash)
if (slashindex == -1) {
//namespace may have been evaluated already
return;
}
//is there a '/' after the '#'?
if (slashindex > hashindex) {
//remove everything after the last '/'
int index = uriString.lastIndexOf('/');
newURI = uriString.substring(0, index) + "/";
} else {
//'#' comes after last '/' (remove entire fragment)
newURI = uriString.replaceAll(uri.getFragment(), "");
}
//only add namespace if it is new
if ( (newURI != null)
&& (!this.namespaces.containsValue(newURI))) {
//add to namespaces
this.namespaces.put("ns" + this.namespaces.size(), newURI);
}
}
}
}