blob: 7f4429467649d9ae7cfae04ce2a0725c2dc0a88c [file] [log] [blame]
/*******************************************************************************
* Crown Copyright (c) 2006, 2007, Copyright (c) 2006, 2007 Jiva Medical.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Jiva Medical - initial API and implementation
*******************************************************************************/
package org.eclipse.uomo.xml.impl;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import org.eclipse.uomo.xml.IXMLWriter;
import org.eclipse.uomo.xml.XMLUtil;
/**
* XML Writer class.
*/
public class XMLWriter extends OutputStreamWriter implements IXMLWriter {
private boolean xmlHeader = true;
private String charset;
private boolean prettyBase;
private boolean prettyHeader;
private boolean pendingClose;
private boolean pendingOpen;
private String pendingComment;
private int lineType = LINE_UNIX;
private OutputStream stream;
private boolean started = false;
private String[] specialAttributeNames = new String[] {"id", "name" };
private boolean sortAttributes;
private int attributeLineWrap;
public final static int LINE_UNIX = 0;
public final static int LINE_WINDOWS = 1;
public XMLWriter(OutputStream stream, String charset) throws UnsupportedEncodingException {
super(stream, charset);
this.stream = stream;
this.charset = charset;
}
protected boolean condition(boolean bTest, String message) throws IOException {
if (!bTest)
throw new IOException(message);
return bTest;
}
// -- writing context ------------------------------------------------
/**
* Returns the encoding.
*
* @param charset
* @return encoding
* @throws IOException
*/
public static String getXMLCharsetName(String charset) throws IOException {
if (charset == null || charset.equals(""))
return "UTF-8";
else if (charset.equals("US-ASCII"))
return "UTF-8";
else if (XMLUtil.charSetImpliesAscii(charset))
return "ISO-8859-1";
else if (charset.equals("UTF-8"))
return "UTF-8";
else if (charset.equals("UTF-16") || charset.equals("UTF-16BE") || charset.equals("UTF-16LE"))
return "UTF-16";
else
throw new IOException("Unknown charset encoding "+charset);
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#start()
*/
public void start() throws IOException {
condition(!started, "attempt to start after starting");
levels.clear();
attributes = null;
try {
if (xmlHeader) {
write("<?xml version=\"1.0\" encoding=\""+getXMLCharsetName(charset)+"\"?>");
if (prettyBase || prettyHeader)
write(lineType == LINE_UNIX ? "\n" : "\r\n");
}
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
throw new IOException(e.getMessage());
}
started = true;
}
private void checkStarted () throws IOException {
condition(started, "not started");
}
private void checkInElement() throws IOException {
condition(levels.size() > 0, "not in an element");
}
// -- attributes ----------------------------------------------------
private String[][] attributes;
private void addAttribute(String name, String value) throws IOException {
addAttribute(name, value, false);
}
private void addAttribute(String name, String value, boolean noLines) throws IOException {
if (!XMLUtil.isNMToken(name))
throw new IOException("XML name "+name+" is not valid");
newLevelIfRequired();
value = XMLUtil.escapeXML(value, charset, noLines);
if (attributes == null)
attributes = new String[][] {{name, value}};
else {
String[][] newattr = new String[attributes.length+1][];
for (int i = 0; i < attributes.length; i++) {
condition(!attributes[i][0].equals(name), "attempt to define attribute with name "+name+" more than once");
newattr[i] = attributes[i];
}
attributes = newattr;
attributes[attributes.length-1] = new String[] {name, value};
}
}
protected String getAttribute(String name) {
if (attributes != null) {
for (int i = 0; i < attributes.length; i++) {
if (attributes[i][0].equals(name)) {
return attributes[i][1];
}
}
}
return null;
}
protected void setAttribute(String name, String value) throws IOException {
newLevelIfRequired();
if (attributes == null)
addAttribute(name, value, false);
else {
for (int i = 0; i < attributes.length; i++) {
if (attributes[i][0].equals(name)) {
attributes[i][1] = XMLUtil.escapeXML(value, charset, false);
return;
}
}
addAttribute(name, value);
}
}
protected void commitAttributes() throws IOException {
}
private boolean nameIsSpecial(String name) {
for (int i = 0; i < specialAttributeNames.length; i++) {
String n = specialAttributeNames[i];
if (n.equalsIgnoreCase(name))
return true;
}
return false;
}
private void writeAttributes(int col) throws IOException {
commitAttributes();
if (attributes != null && sortAttributes)
sortAttributes();
int c = col;
c = writeAttributeSet(true, c, col);
writeAttributeSet(false, c, col);
attributes = null;
}
private void sortAttributes() {
// bubble sort - look, it's easy
for (int i = 0; i < attributes.length - 1; i++) {
for (int j = 0; j < attributes.length - 1; j++) {
if (String.CASE_INSENSITIVE_ORDER.compare(attributes[j][0], attributes[j+1][0]) < 0) {
String[] t = attributes[j];
attributes[j] = attributes[j+1];
attributes[j+1] = t;
}
}
}
}
private int writeAttributeSet(boolean special, int col, int wrap) throws IOException {
// first pass: name, id
if (attributes != null) {
for (int i=0; i < attributes.length; i++) {
String[] element = attributes[i];
if (nameIsSpecial(element[0]) == special) {
col = col + element[0].length()+element[1].length() + 4;
if (isPretty() && attributeLineWrap > 0 && col > attributeLineWrap && col > wrap) {
write(lineType == LINE_UNIX ? "\n" : "\r\n");
for (int j = 0; j < wrap; j++)
write(" ");
col = wrap;
}
write(' ');
write(element[0]);
write("=\"");
if (element[1] != null)
write(element[1]);
write("\"");
}
}
}
return col;
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#attribute(java.lang.String, java.lang.String, java.lang.String, boolean)
*/
public void attribute(String namespace, String name, String value, boolean onlyIfNotEmpty) throws IOException {
if (!onlyIfNotEmpty || value != null && !value.equals(""))
attribute(namespace, name, value);
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#attribute(java.lang.String, java.lang.String, java.lang.String)
*/
public void attribute(String namespace, String name, String value) throws IOException {
checkStarted();
if (namespace == null || namespace.equals(""))
addAttribute(name, value);
else
addAttribute(getNSAbbreviation(namespace)+name, value);
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#attribute(java.lang.String, java.lang.String, boolean)
*/
public void attribute(String name, String value, boolean onlyIfNotEmpty) throws IOException {
if (!onlyIfNotEmpty || value != null && !value.equals(""))
attribute(name, value);
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#attribute(java.lang.String, java.lang.String)
*/
public void attribute(String name, String value) throws IOException {
checkStarted();
addAttribute(name, value);
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#attributeNoLines(java.lang.String, java.lang.String)
*/
public void attributeNoLines(String name, String value) throws IOException {
checkStarted();
addAttribute(name, value, true);
}
// -- levels -------------------------------------------------
private XMLWriterStateStack levels = new XMLWriterStateStack();
private void newLevelIfRequired() throws IOException {
if (!pendingOpen) {
if (!levels.empty())
levels.current().seeChild();
XMLWriterState level = new XMLWriterState();
level.setPretty(isPretty());
levels.push(level);
pendingOpen = true;
}
}
// -- namespaces ---------------------------------------------
private void defineNamespace(String namespace, String abbrev) throws IOException {
checkStarted();
if (namespace != null && !namespace.equals("")) {
if (abbrev.equals(""))
abbrev = null;
newLevelIfRequired();
levels.current().addNamespaceDefn(namespace, abbrev);
if (abbrev == null)
addAttribute("xmlns", namespace);
else
addAttribute("xmlns:"+abbrev, namespace);
}
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#findByNamespace(java.lang.String)
*/
public XMLNamespace findByNamespace(String namespace) {
for (int i = levels.size() - 1; i >= 0; i--) {
XMLNamespace ns = levels.item(i).getDefnByNamespace(namespace);
if (ns != null)
return ns;
}
return null;
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#namespaceDefined(java.lang.String)
*/
public boolean namespaceDefined(String namespace) {
return namespace == null || namespace.equals("") || findByNamespace(namespace) != null;
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#findByAbbreviation(java.lang.String)
*/
public XMLNamespace findByAbbreviation(String abbreviation) {
for (int i = levels.size() - 1; i >= 0; i--) {
XMLNamespace ns = levels.item(i).getDefnByAbbreviation(abbreviation);
if (ns != null)
return ns;
}
return null;
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#abbreviationDefined(java.lang.String)
*/
public boolean abbreviationDefined(String abbreviation) {
return findByAbbreviation(abbreviation) != null;
}
protected XMLNamespace findDefaultNamespace() {
for (int i = levels.size() - 1; i >= 0; i--) {
XMLNamespace ns = levels.item(i).getDefaultNamespace();
if (ns != null)
return ns;
}
return null;
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#getDefaultNamespace()
*/
public String getDefaultNamespace() {
XMLNamespace ns = findDefaultNamespace();
if (ns == null)
return null;
else
return ns.getNamespace();
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#namespace(java.lang.String)
*/
public void namespace(String namespace) throws IOException {
if (!namespaceDefined(namespace)) {
int index = 0;
while (abbreviationDefined("ns"+Integer.toString(index)))
index++;
defineNamespace(namespace, "ns"+Integer.toString(index));
}
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#defaultNamespace(java.lang.String)
*
* Replace defaultNamespace()
*/
public void setDefaultNamespace(String namespace) throws IOException {
if ((namespace == null && getDefaultNamespace() != null) ||
(namespace != null && !namespace.equals(getDefaultNamespace())))
defineNamespace(namespace, "");
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#namespace(java.lang.String, java.lang.String)
*/
public void namespace(String namespace, String abbreviation) throws IOException {
XMLNamespace ns = findByAbbreviation(abbreviation);
if (ns == null || !ns.getNamespace().equals(namespace))
defineNamespace(namespace, abbreviation);
}
private String getNSAbbreviation(String namespace) throws IOException {
if ("http://www.w3.org/XML/1998/namespace".equals(namespace))
return "xml:";
if (namespace == null || "".equals(namespace))
return "";
XMLNamespace ns = findByNamespace(namespace);
if (ns == null)
throw new IOException("Namespace "+namespace+" is not defined");
else if (ns.getAbbreviation() == null)
return "";
else
return ns.getAbbreviation()+":";
}
// -- public API -----------------------------------------------------------
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#comment(java.lang.String, boolean)
*/
public void comment(String comment, boolean doPretty) throws IOException {
checkStarted();
if (pendingClose) {
write('>');
writePendingComment();
pendingClose = false;
}
if (doPretty) {
writePretty();
if (isPretty()) {
for (int i = 0; i < levels.size(); i++)
write(" ");
}
}
if (levels.inComment())
write("<!-- "+comment+" -- >");
else
write("<!-- "+comment+" -->");
if (doPretty && !isPretty())
writePretty();
}
private void writePendingComment() throws IOException {
if (pendingComment != null) {
if (isPretty())
write(" ");
if (levels.inComment())
write("<!-- "+pendingComment+" -- >");
else
write("<!-- "+pendingComment+" -->");
}
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#open(java.lang.String, java.lang.String)
*/
public void open(String namespace, String name) throws IOException {
open(namespace, name, null);
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#open(java.lang.String, java.lang.String, java.lang.String)
*/
public void open(String namespace, String name, String comment) throws IOException {
if (!XMLUtil.isNMToken(name))
throw new IOException("XML name "+name+" is not valid");
checkStarted();
if (pendingClose) {
write('>');
writePendingComment();
pendingClose = false;
}
if (name == null) {
throw new IOException("name is null");
}
newLevelIfRequired();
levels.current().setName(name);
levels.current().setNamespace(namespace);
int col = writePretty();
write('<');
if (namespace == null) {
write(name);
col = col + name.length()+1;
} else {
String n = getNSAbbreviation(namespace)+name;
write(n);
col = col + n.length()+1;
}
writeAttributes(col);
pendingOpen = false;
pendingClose = true;
pendingComment = comment;
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#close(java.lang.String)
*/
public void close(String name) throws IOException {
checkStarted();
if (levels.empty())
throw new IOException("Unable to close null|"+name+", nothing to close");
if (levels.current().getNamespace() != null || !levels.current().getName().equals(name))
throw new IOException("Unable to close null|"+name+", found "+levels.current().getNamespace()+"|"+levels.current().getName());
close();
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#close(java.lang.String, java.lang.String)
*/
public void close(String namespace, String name) throws IOException {
checkStarted();
if (levels.empty())
throw new IOException("Unable to close "+namespace+"|"+name+", nothing to close");
if (!levels.current().getNamespace().equals(namespace) || !levels.current().getName().equals(name))
throw new IOException("Unable to close "+namespace+"|"+name+", found "+levels.current().getNamespace()+"|"+levels.current().getName());
close();
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#closeToLevel(int)
*/
public void closeToLevel(int count) throws IOException {
while (levels.size() > count)
close();
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#close()
*/
public void close() throws IOException {
checkStarted();
if (levels.empty()) {
super.close();
} else {
if (pendingClose) {
write("/>");
writePendingComment();
pendingClose = false;
} else {
if (levels.current().hasChildren())
writePretty();
write("</");
if (levels.current().getNamespace() == null)
write(levels.current().getName());
else
write(getNSAbbreviation(levels.current().getNamespace())+levels.current().getName());
write('>');
}
levels.pop();
}
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#open(java.lang.String)
*/
public void open(String name) throws IOException {
open(null, name);
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#element(java.lang.String, java.lang.String, java.lang.String, boolean)
*/
public void element(String namespace, String name, String content, boolean onlyIfNotEmpty) throws IOException {
if (!onlyIfNotEmpty || content != null && !content.equals(""))
element(namespace, name, content);
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#element(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
*/
public void element(String namespace, String name, String content, String comment) throws IOException {
if (!XMLUtil.isNMToken(name))
throw new IOException("XML name "+name+" is not valid");
open(namespace, name, comment);
text(content);
close();
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#element(java.lang.String, java.lang.String, java.lang.String)
*/
public void element(String namespace, String name, String content) throws IOException {
if (!XMLUtil.isNMToken(name))
throw new IOException("XML name "+name+" is not valid");
open(namespace, name);
text(content);
close();
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#element(java.lang.String, java.lang.String, boolean)
*/
public void element(String name, String content, boolean onlyIfNotEmpty) throws IOException {
if (!onlyIfNotEmpty || content != null && !content.equals(""))
element(null, name, content);
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#element(java.lang.String, java.lang.String)
*/
public void element(String name, String content) throws IOException {
element(null, name, content);
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#text(java.lang.String)
*/
public void text(String content) throws IOException {
text(content, false);
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#text(java.lang.String, boolean)
*
* Replace escapeText()
*/
public void text(String content, boolean dontEscape) throws IOException {
checkInElement();
if (content != null) {
if (pendingClose) {
write(">");
writePendingComment();
pendingClose = false;
}
if (dontEscape)
write(content);
else
write(XMLUtil.escapeXML(content, charset, false));
}
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#cData(java.lang.String)
*/
public void cData(String text) throws IOException {
text("<![CDATA["+text+"]]>");
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#writeBytes(byte[])
*/
public void writeBytes(byte[] bytes) throws IOException {
checkInElement();
if (pendingClose) {
write(">");
writePendingComment();
pendingClose = false;
}
flush();
stream.write(bytes);
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#isPretty()
*/
public boolean isPretty() throws IOException {
return (levels == null || levels.empty()) ? prettyBase : levels.current().isPretty();
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#setPretty(boolean)
*/
public void setPretty(boolean pretty) throws IOException {
if (levels == null || levels.empty())
this.prettyBase = pretty;
else
levels.current().setPretty(pretty);
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#startCommentBlock()
*/
public void startCommentBlock() throws IOException {
if (levels.inComment())
throw new IOException("cannot nest comments");
levels.current().setInComment(true);
if (isPretty())
writePretty();
write("<!--");
if (isPretty())
writePretty();
}
/* (non-Javadoc)
* @see org.eclipse.ohf.utilities.xml.IXMLWriter#endCommentBlock()
*/
public void endCommentBlock() throws IOException {
if (!levels.inComment())
throw new IOException("cannot close a comment block when it is open");
if (!levels.current().isInComment())
throw new IOException("cannot close a comment block when it is open");
if (isPretty())
writePretty();
write("-->");
if (isPretty())
writePretty();
levels.current().setInComment(false);
}
public boolean isSortAttributes() {
return sortAttributes;
}
public void setSortAttributes(boolean sortAttributes) {
this.sortAttributes = sortAttributes;
}
public boolean isPrettyHeader() {
return prettyHeader;
}
public void setPrettyHeader(boolean pretty) {
this.prettyHeader = pretty;
}
public int writePretty() throws IOException {
return writePretty(true);
}
public int writePretty(boolean eoln) throws IOException {
if (isPretty()) {
if (eoln)
write(lineType == LINE_UNIX ? "\n" : "\r\n");
for (int i = 0; i < levels.size() - 1; i++)
write(" ");
return (levels.size() - 1) * 2;
} else
return 0;
}
public int getLineType() {
return lineType;
}
public void setLineType(int lineType) {
this.lineType = lineType;
}
public boolean isXmlHeader() {
return xmlHeader;
}
public void setXmlHeader(boolean xmlHeader) {
this.xmlHeader = xmlHeader;
}
public String[] getSpecialAttributeNames() {
return specialAttributeNames;
}
public void setSpecialAttributeNames(String[] specialAttributeNames) {
this.specialAttributeNames = specialAttributeNames;
}
public int getAttributeLineWrap() {
return attributeLineWrap;
}
public void setAttributeLineWrap(int attributeLineWrap) {
this.attributeLineWrap = attributeLineWrap;
}
public void escapedText(String content) throws IOException {
text("");
int i = content.length();
if (isPretty())
while (i > 0 && (content.charAt(i-1) == '\r' || content.charAt(i-1) == '\n'))
i--;
write(content.substring(0, i));
}
}