package com.freeone3000.xml;
import java.io.*;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Stack;
/**
* This is an XML writer, more or less compliant with Microsoft's
* .NET framework's XML writer. Note that this class is *not*
* thread safe.
*
* @author freeone3000
* @version Aug 26, 2006
*/
public class XmlWriter extends Writer {
private final BufferedWriter write;
private Stack<String> openNodes = new Stack<String>();
private boolean documentIsOpen = false;
private boolean inTag = false;
private boolean inAttribute = false;
private Map<String, String> replacements = new LinkedHashMap<String, String>();
/**
* Opens a new XmlWriter to the filename given, wrapping in a buffered
* writer.
* @param fname The filename to open.
* @throws IOException If opening the stream causes an IOException.
* @throws FileNotFoundException If the filename cannot be found.
*/
public XmlWriter(String fname) throws IOException, FileNotFoundException {
this(new File(fname));
}
/**
* Opens a new XmlWriter to the File given, wrapping in a buffered
* writer.
* @param f The file to use.
* @throws IOException If opening the stream causes an IOException.
* @throws FileNotFoundException If the filename cannot be found.
*/
public XmlWriter(File f) throws IOException, FileNotFoundException {
this(new FileOutputStream(f));
}
/**
* Opens a new XmlWriter to the OutputStream given, wrapping in a
* buffered writer.
* @param str The stream to use.
*/
public XmlWriter(OutputStream str) {
this(new OutputStreamWriter(str));
}
/**
* Opens a new XmlWriter to the writer given, wrapping in a buffered
* writer.
* @param w The writer to use.
*/
public XmlWriter(Writer w) {
this.write = new BufferedWriter(w);
setupReplacements();
}
private void setupReplacements() {
replacements.put("&", "&");
replacements.put("<", "<");
replacements.put(">", ">");
}
/**
* Writes any number of raw strings to the document, with NO
* CHECKING of any kind. For a checked manner, please see writeString.
* @param str Any number of strings.
* @throws IOException If the underlying writer throws an exception.
* @see XmlWriter#writeString(java.lang.String)
*/
public void writeRaw(String ... str) throws IOException {
StringBuilder s = new StringBuilder();
for(String s2 : str)
s.append(s2);
write.write(s.toString());
}
/**
* Writes the start of an element. If a tag is open, it closes it.
* @param elem The name of the element to write.
* @throws IOException If the underlying writer throws an exception.
* @throws XmlFormattingException If closing the tag throws an exception.
*/
public void writeStartElement(String elem) throws IOException, XmlFormattingException {
if(inTag) {
if(inAttribute)
writeEndAttribute();
writeRaw(">");
}
inTag = true;
elem = replaceEntities(elem);
openNodes.push(elem);
writeRaw("<" + elem);
}
/**
* Ends one element, closing all other tags and attribuates if neccecary.
* If we are in a tag, the tag is self-closed, otherwise it is tag closed.
* @throws IOException If the writer throws an exception.
* @throws XmlFormattingException If all nodes are already closed.
*/
public void writeEndElement() throws IOException, XmlFormattingException {
if(openNodes.empty())
throw new XmlFormattingException("All nodes are closed!");
if(inAttribute)
writeEndAttribute();
if(inTag) {
writeRaw("/>");
inTag = false;
} else
writeRaw("</" + replaceEntities(openNodes.pop()) + ">");
}
/**
* If pubid and sysid are both non-null, it writes `PUBLIC "pubid" "sysid"'.
* If pubid is null but sysid is not null, it writes `SYSTEM "sysid"'.
* If subset is not null, it writes `[subset]'.
* This results in a Doctype conformant to the .NET implementation.
* @param name The name of the attribute to write. Must NOT be null or empty.
* @param pubid Public ID. May be null.
* @param sysid System ID. May be null.
* @param subset Subset. May be null. No clue what it does.
* @throws IOException If writing throws an exception.
* @throws XmlFormattingException If this results in invalid Xml.
*/
public void writeDocumentType(String name, String pubid, String sysid, String subset)
throws IOException, XmlFormattingException {
if(documentIsOpen)
throw new XmlFormattingException("Cannot write the document type after the document is open!");
writeRaw("<!DOCTYPE " + replaceEntities(name));
writeWhitespace(" ");
if(pubid!=null && sysid!=null)
writeRaw("PUBLIC \"" + pubid + "\" \"" + sysid + "\"");
else if(sysid!=null)
writeRaw("SYSTEM \"" + sysid + "\"");
writeWhitespace(" ");
if(subset!=null)
writeRaw("[" + subset + "] ");
writeRaw(">");
documentIsOpen = true;
}
/**
* Writes a comment literal with the specified contents. If there is a tag open, the
* tag is closed.
* @param comment The comment to write.
* @throws IOException If the underlying writing throws an exception.
* @throws XmlFormattingException If ending the element throws a formatting exception.
*/
public void writeComment(String comment) throws IOException, XmlFormattingException {
if(inTag)
writeEndElement();
writeRaw("<!-- ");
writeString(comment);
writeRaw(" -->");
}
/**
* Replaces all restricted characters in the string 's' with XML-complaint
* characters.
* @param s The string to replace.
* @return The string after replacement.
*/
private String replaceEntities(String s) {
int pos;
for(String str : replacements.keySet()) {
pos = s.indexOf(str);
if(pos!=-1) {
s = s.replaceAll(str, replacements.get(str));
}
}
return s;
}
/**
* Writes an amount of whitespace, which must conform to the regex
* "^\\s+$". It then uses writeRaw() to get around any other restrictions,
* because whitespace cannot invalidate documents. This should be preferred
* over writeString() to write whitespace.
* @param whitespace A string of whitespace to write.
* @throws IOException If writing throws an exception.
* @throws IllegalArgumentException If the argument given does not conform to the regex.
* @see XmlWriter#writeRaw(String...)
*/
public void writeWhitespace(String whitespace) throws IOException {
if(!whitespace.matches("^\\s+$"))
throw new IllegalArgumentException("The string given was not whitespace!");
writeRaw(whitespace);
}
/**
* Writes the start of an attribute, with the name attributeName.
* If it is not in a tag, it throws an XmlFormattingException, if it
* is already in an attribute, it closes the old one.
* @param attributeName The name of the attribute.
* @throws IOException If writing throws an exception.
* @throws XmlFormattingException If the XML becomes invalid.
*/
public void writeStartAttribute(String attributeName) throws IOException, XmlFormattingException {
if(inAttribute)
writeEndAttribute();
if(!inTag)
throw new XmlFormattingException("You're not in a tag!");
inAttribute = true;
writeWhitespace(" ");
writeRaw(replaceEntities(attributeName));
writeRaw("=\"");
}
/**
* Writes the end of an attribute. If there is no attribute open,
* it fails silently.
* @throws IOException If writing to the stream throws an exception.
*/
public void writeEndAttribute() throws IOException {
if(!inAttribute)
return;
writeRaw("\"");
inAttribute = false;
}
/**
* Writes a processing instruction.
* @param name The name of the instruction.
* @param parts An array, 1->2, 3->4... think perl.
* @throws IOException If writing throws an exception.
* @throws XmlFormattingException If an exception occors.
*/
public void writeProcessingInstruction(String name, String ... parts) throws IOException,
XmlFormattingException {
writeRaw("<?");
writeString(name);
inTag = true;
for(int i=0; i<parts.length; i+=2) {
writeAttribute(parts[i], parts[i+1]);
}
inTag = false;
writeRaw("?>");
}
/**
* Writes an attribute, all at once.
* @param name The name of the attribute.
* @param value The value to write.
* @throws IOException If writing throws an exception.
* @throws XmlFormattingException If any of the methods called would invlaidate
* the formatting.
* @see XmlWriter#writeStartAttribute(java.lang.String)
* @see XmlWriter#writeEndAttribute()
*/
public void writeAttribute(String name, String value) throws IOException, XmlFormattingException {
writeStartAttribute(name);
writeString(value);
writeEndAttribute();
}
/**
* Writes data inside a parser-free block. As such, it uses the writeRaw() method,
* as opposed to the writeString() method. If it is inside an element but not inside
* an attribute, it closes the attribute.
* @param text The text to write.
* @throws IOException If writing throws an exception.
* @throws XmlFormattingException If closing the tag would cause a formatting error.
*/
public void writeCData(String text) throws IOException, XmlFormattingException {
if(inTag && !inAttribute)
writeEndElement();
writeRaw("<![CDATA[");
writeRaw(text);
writeRaw("]]>");
}
/**
* Writes the start of an XML1 document.
* @throws IOException If the writer throws an exception.
* @throws XmlFormattingException If the document has already started.
*/
public void writeStartDocument() throws IOException, XmlFormattingException {
if(documentIsOpen)
throw new XmlFormattingException("The document is already started!");
writeProcessingInstruction("xml", "version", "1.0", "encoding", "UTF-16");
}
/**
* Closes each element in turn, then closes the stream. All
* XmlFormattingExceptions are nonhandled.
* @throws IOException If the writer throws an exception.
* @see XmlWriter#writeEndElement()
*/
@SuppressWarnings({"EmptyCatchBlock"})
public void writeEndDocument() throws IOException {
try {
while(!openNodes.empty())
writeEndElement();
} catch(XmlFormattingException e) {
}
close();
}
/**
* Safely writes a string, replacing all bad entities, or enclosing
* in a CDATA block, and making sure it's not a direct write inside
* an unclosed tag. If it would be, it automatically closes the tag.
* @param str The string to write.
* @throws IOException If writing throws an IOException.
* @throws XmlFormattingException If closing the current tag would cause a XmlFormattingException
*/
public void writeString(String str) throws IOException, XmlFormattingException {
str = replaceEntities(str);
if(inTag && !inAttribute)
writeEndElement();
writeRaw(str);
}
/**
* Creates a new String from the character array, and passes it to
* writeString.
* @param chars The characters to write.
* @param off The offset from the start.
* @param len The length of the array to write.
* @throws IOException If the writer throws an IOException.
* @see XmlWriter#writeString
* @see XmlWriter#writeRaw
*/
public void write(char[] chars, int off, int len) throws IOException {
char[] c2 = new char[len];
System.arraycopy(chars, off, c2, 0, len);
try {
writeString(new String(c2));
} catch(XmlFormattingException xfe) {
throw new IOException("XmlFormattingException: " + xfe.getMessage());
}
}
/**
* Flushes the internal writer.
* @throws IOException If the writer throws an exception.
*/
public void flush() throws IOException {
write.flush();
}
/**
* Closes the internal writer.
* @throws IOException If the writer throws an exception.
*/
public void close() throws IOException {
write.close();
}
}