/*
* Copyright Aduna (http://www.aduna-software.com/) (c) 1997-2007.
*
* Licensed under the Aduna BSD-style license.
*/
package org.openrdf.rio.rdfxml;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.LinkedHashMap;
import java.util.Map;
import info.aduna.xml.XMLUtil;
import org.openrdf.model.BNode;
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.rio.RDFFormat;
import org.openrdf.rio.RDFHandlerException;
import org.openrdf.rio.RDFWriter;
/**
* An implementation of the RDFWriter interface that writes RDF documents in
* XML-serialized RDF format.
*/
public class RDFXMLWriter implements RDFWriter {
/*-----------*
* Variables *
*-----------*/
protected Writer writer;
protected Map<String, String> namespaceTable;
protected boolean writingStarted;
protected boolean headerWritten;
protected Resource lastWrittenSubject;
/*--------------*
* Constructors *
*--------------*/
/**
* Creates a new RDFXMLWriter that will write to the supplied OutputStream.
*
* @param out
* The OutputStream to write the RDF/XML document to.
*/
public RDFXMLWriter(OutputStream out) {
this(new OutputStreamWriter(out, Charset.forName("UTF-8")));
}
/**
* Creates a new RDFXMLWriter that will write to the supplied Writer.
*
* @param writer
* The Writer to write the RDF/XML document to.
*/
public RDFXMLWriter(Writer writer) {
this.writer = writer;
namespaceTable = new LinkedHashMap<String, String>();
writingStarted = false;
headerWritten = false;
lastWrittenSubject = null;
}
/*---------*
* Methods *
*---------*/
public RDFFormat getRDFFormat() {
return RDFFormat.RDFXML;
}
public void startRDF() {
if (writingStarted) {
throw new RuntimeException("Document writing has already started");
}
writingStarted = true;
}
protected void writeHeader()
throws IOException
{
try {
// This export format needs the RDF namespace to be defined, add a
// prefix for it if there isn't one yet.
setNamespace("rdf", RDF.NAMESPACE, false);
writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
writeStartOfStartTag(RDF.NAMESPACE, "RDF");
for (Map.Entry<String, String> entry : namespaceTable.entrySet()) {
String name = entry.getKey();
String prefix = entry.getValue();
writeNewLine();
writeIndent();
writer.write("xmlns");
if (prefix.length() > 0) {
writer.write(':');
writer.write(prefix);
}
writer.write("=\"");
writer.write(XMLUtil.escapeDoubleQuotedAttValue(name));
writer.write("\"");
}
writeEndOfStartTag();
writeNewLine();
}
finally {
headerWritten = true;
}
}
public void endRDF()
throws RDFHandlerException
{
if (!writingStarted) {
throw new RuntimeException("Document writing has not yet started");
}
try {
if (!headerWritten) {
writeHeader();
}
flushPendingStatements();
writeNewLine();
writeEndTag(RDF.NAMESPACE, "RDF");
writer.flush();
}
catch (IOException e) {
throw new RDFHandlerException(e);
}
finally {
writingStarted = false;
headerWritten = false;
}
}
public void handleNamespace(String prefix, String name) {
setNamespace(prefix, name, false);
}
protected void setNamespace(String prefix, String name, boolean fixedPrefix) {
if (headerWritten) {
// Header containing namespace declarations has already been written
return;
}
if (!namespaceTable.containsKey(name)) {
// Namespace not yet mapped to a prefix, try to give it the specified
// prefix
boolean isLegalPrefix = prefix.length() == 0 || XMLUtil.isNCName(prefix);
if (!isLegalPrefix || namespaceTable.containsValue(prefix)) {
// Specified prefix is not legal or the prefix is already in use,
// generate a legal unique prefix
if (fixedPrefix) {
if (isLegalPrefix) {
throw new IllegalArgumentException("Prefix is already in use: " + prefix);
}
else {
throw new IllegalArgumentException("Prefix is not a valid XML namespace prefix: " + prefix);
}
}
if (prefix.length() == 0 || !isLegalPrefix) {
prefix = "ns";
}
int number = 1;
while (namespaceTable.containsValue(prefix + number)) {
number++;
}
prefix += number;
}
namespaceTable.put(name, prefix);
}
}
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();
// Verify that an XML namespace-qualified name can be created for the
// predicate
String predString = pred.toString();
int predSplitIdx = XMLUtil.findURISplitIndex(predString);
if (predSplitIdx == -1) {
throw new RDFHandlerException("Unable to create XML namespace-qualified name for predicate: "
+ predString);
}
String predNamespace = predString.substring(0, predSplitIdx);
String predLocalName = predString.substring(predSplitIdx);
try {
if (!headerWritten) {
writeHeader();
}
// SUBJECT
if (!subj.equals(lastWrittenSubject)) {
flushPendingStatements();
// Write new subject:
writeNewLine();
writeStartOfStartTag(RDF.NAMESPACE, "Description");
if (subj instanceof BNode) {
BNode bNode = (BNode)subj;
writeAttribute(RDF.NAMESPACE, "nodeID", bNode.getID());
}
else {
URI uri = (URI)subj;
writeAttribute(RDF.NAMESPACE, "about", uri.toString());
}
writeEndOfStartTag();
writeNewLine();
lastWrittenSubject = subj;
}
// PREDICATE
writeIndent();
writeStartOfStartTag(predNamespace, predLocalName);
// OBJECT
if (obj instanceof Resource) {
Resource objRes = (Resource)obj;
if (objRes instanceof BNode) {
BNode bNode = (BNode)objRes;
writeAttribute(RDF.NAMESPACE, "nodeID", bNode.getID());
}
else {
URI uri = (URI)objRes;
writeAttribute(RDF.NAMESPACE, "resource", uri.toString());
}
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(predNamespace, predLocalName);
}
writeNewLine();
// Don't write </rdf:Description> yet, maybe the next statement
// has the same subject.
}
catch (IOException e) {
throw new RDFHandlerException(e);
}
}
public void handleComment(String comment)
throws RDFHandlerException
{
try {
if (!headerWritten) {
writeHeader();
}
flushPendingStatements();
writer.write("<!-- ");
writer.write(comment);
writer.write(" -->");
writeNewLine();
}
catch (IOException e) {
throw new RDFHandlerException(e);
}
}
protected void flushPendingStatements()
throws IOException
{
if (lastWrittenSubject != null) {
// The last statement still has to be closed:
writeEndTag(RDF.NAMESPACE, "Description");
writeNewLine();
lastWrittenSubject = null;
}
}
protected void writeStartOfStartTag(String namespace, String localName)
throws IOException
{
String prefix = namespaceTable.get(namespace);
if (prefix == null) {
writer.write("<");
writer.write(localName);
writer.write(" xmlns=\"");
writer.write(XMLUtil.escapeDoubleQuotedAttValue(namespace));
writer.write("\"");
}
else if (prefix.length() == 0) {
// default namespace
writer.write("<");
writer.write(localName);
}
else {
writer.write("<");
writer.write(prefix);
writer.write(":");
writer.write(localName);
}
}
protected void writeAttribute(String attName, String value)
throws IOException
{
writer.write(" ");
writer.write(attName);
writer.write("=\"");
writer.write(XMLUtil.escapeDoubleQuotedAttValue(value));
writer.write("\"");
}
protected void writeAttribute(String namespace, String attName, String value)
throws IOException
{
String prefix = namespaceTable.get(namespace);
if (prefix == null || prefix.length() == 0) {
throw new RuntimeException("No prefix has been declared for the namespace used in this attribute: "
+ namespace);
}
writer.write(" ");
writer.write(prefix);
writer.write(":");
writer.write(attName);
writer.write("=\"");
writer.write(XMLUtil.escapeDoubleQuotedAttValue(value));
writer.write("\"");
}
protected void writeEndOfStartTag()
throws IOException
{
writer.write(">");
}
protected void writeEndOfEmptyTag()
throws IOException
{
writer.write("/>");
}
protected void writeEndTag(String namespace, String localName)
throws IOException
{
String prefix = namespaceTable.get(namespace);
if (prefix == null || prefix.length() == 0) {
writer.write("</");
writer.write(localName);
writer.write(">");
}
else {
writer.write("</");
writer.write(prefix);
writer.write(":");
writer.write(localName);
writer.write(">");
}
}
protected void writeCharacterData(String chars)
throws IOException
{
writer.write(XMLUtil.escapeCharacterData(chars));
}
protected void writeIndent()
throws IOException
{
writer.write("\t");
}
protected void writeNewLine()
throws IOException
{
writer.write("\n");
}
}