/**
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.xml;
import java.io.*;
import java.util.Iterator;
/**
* Used to write pretty-printed XML content.
* <p>
* Application writers should keep in mind that this class does not perform any sort
* of coherency check, and will not prevent them from closing elements they haven't opened yet,
* or any other thing that would make the XML output invalid.
* </p>
* @author Nicolas Rinaudo
*/
public class XmlWriter {
// - Constants -------------------------------------------------------
// -------------------------------------------------------------------
/** Number of space characters used for one level of indentation. */
private static final int OFFSET_INCREMENT = 4;
/** Identifier for publicly accessible objects. */
public static final String AVAILABILITY_PUBLIC = "PUBLIC";
/** Identifier for system resources. */
public static final String AVAILABILITY_SYSTEM = "SYSTEM";
/** Default output encoding. */
public static final String DEFAULT_ENCODING = "UTF-8";
// - XML standard entities -------------------------------------------
// -------------------------------------------------------------------
/** Forbiden XML characters. */
private final static String[] ENTITIES = new String[] {"&", "\"" , "'", "<", ">"};
/** What to replace forbidden XML characters with. */
private final static String[] ENTITY_REPLACEMENTS = new String[] {"&", """, "'", "<", ">"};
// - Instance fields -------------------------------------------------
// -------------------------------------------------------------------
/** Where to write the XML content to. */
private PrintWriter out;
/** Current indentation offset. */
private int offset;
/** Whether the next element opening or closing operation should be indented. */
private boolean printIndentation;
// - Initialisation --------------------------------------------------
// -------------------------------------------------------------------
/**
* Creates an <code>XmlWriter</code> that will write to the specified file.
* <p>
* This is a convenience constructor and is strictly equivalent to
* <code>{@link #XmlWriter(OutputStream,String) XmlWriter}(new FileOutputStream(file), {@link #DEFAULT_ENCODING})</code>.
* </p>
* @param file where to write XML output to.
* @throws FileNotFoundException if <code>file</code> could not be found.
* @throws IOException if an I/O error occurs.
*/
public XmlWriter(File file) throws IOException, FileNotFoundException {this(new FileOutputStream(file));}
/**
* Creates an <code>XmlWriter</code> that will write to the specified file using the specified encoding.
* <p>
* This is a convenience constructor and is strictly equivalent to
* <code>{@link #XmlWriter(OutputStream,String) XmlWriter}(new FileOutputStream(file), encoding)</code>.
* </p>
* @param file where to write XML output to.
* @param encoding encoding to use when writing the XML content.
* @throws FileNotFoundException if <code>file</code> could not be found.
* @throws UnsupportedEncodingException if <code>encoding</code> is not supported.
* @throws IOException if an I/O error occurs.
*/
public XmlWriter(File file, String encoding) throws IOException, FileNotFoundException, UnsupportedEncodingException {this(new FileOutputStream(file), encoding);}
/**
* Creates an <code>XmlWriter</code> that will write to the specified output stream.
* <p>
* This is a convenience constructor and is strictly equivalent to
* <code>{@link #XmlWriter(OutputStream,String) XmlWriter}(stream, {@link #DEFAULT_ENCODING})</code>.
* </p>
* @param stream where to write XML output to.
* @throws IOException if an I/O error occurs.
*/
public XmlWriter(OutputStream stream) throws IOException {init(new OutputStreamWriter(stream, DEFAULT_ENCODING), DEFAULT_ENCODING);}
/**
* Creates an <code>XmlWriter</code> that will write to the specified stream using the specified encoding.
* @param stream where to write XML output to.
* @param encoding encoding to use when writing the XML content.
* @throws UnsupportedEncodingException if <code>encoding</code> is not supported.
* @throws IOException if an I/O error occurs.
*/
public XmlWriter(OutputStream stream, String encoding) throws UnsupportedEncodingException, IOException {init(new OutputStreamWriter(stream, encoding), encoding);}
private void init(Writer writer, String encoding) throws IOException {
out = new PrintWriter(writer, true);
out.print("<?xml version=\"1.0\" encoding=\"");
out.print(encoding);
out.println("\"?>");
if(out.checkError())
throw new IOException();
}
// - Element operations --------------------------------------------------
// -------------------------------------------------------------------
/**
* Writes the document type declaration of the XML file.
* <p>
* For the generated XML content to be valid, application writers should make sure that this
* is the very first method they call. This class doesn't ensure coherency and won't
* complain if the <code>DOCTYPE</code> statement is the last in the file.
* </p>
* <p>
* Both <code>description</code> and <code>url</code> can be set to <code>null</code>.
* If so, they will just be ignored.
* </p>
* @param topElement label of the top element in the XML file.
* @param availability availability of the document (expected to be either {@link #AVAILABILITY_PUBLIC}
* or {@link #AVAILABILITY_SYSTEM}, but this is not enforced).
* @param description description of the file (see DOCTYPE specifications for more information).
* @param url URL at which the DTD of the XML file can be downloaded.
* @throws IOException if an I/O error occurs.
*/
public void writeDocType(String topElement, String availability, String description, String url) throws IOException {
// Writes the compulsory bits.
out.print("<!DOCTYPE ");
out.print(topElement);
out.print(' ');
out.print(availability);
// Writes the description if present.
if(description != null) {
out.print(' ');
out.print('\"');
out.print(description);
out.print('\"');
}
// Writes the DTD url if present.
if(url != null) {
out.print(' ');
out.print('\"');
out.print(url);
out.print('\"');
}
out.println('>');
if(out.checkError())
throw new IOException();
}
/**
* Writes an element opening sequence.
* <p>
* This is a convenience method and is strictly equivalent to calling
* <code>{@link #startElement(String,boolean) startElement}(name, false)</code>.
* </p>
* @param name name of the element to open.
* @throws IOException if an I/O error occurs.
* @see #startElement(String,XmlAttributes)
* @see #writeStandAloneElement(String)
* @see #writeStandAloneElement(String,XmlAttributes)
*/
public void startElement(String name) throws IOException {startElement(name, false, null, false);}
/**
* Writes an element opening sequence.
* <p>
* Elements opened using this method will not have any attribute, and will
* need to be closed using an {@link #endElement(String) endElement} call.
* </p>
* @param name name of the element to open.
* @param lineBreak if <code>true</code>, a line break will be printed after the element declaration.
* @throws IOException if an I/O error occurs.
* @see #startElement(String,XmlAttributes)
* @see #writeStandAloneElement(String)
* @see #writeStandAloneElement(String,XmlAttributes)
*/
public void startElement(String name, boolean lineBreak) throws IOException {startElement(name, false, null, lineBreak);}
/**
* Writes a stand-alone element.
* <p>
* Elements opened using this method will not have any attributes, and will be
* closed immediately.
* </p>
* <p>
* A line break will always be printed after a stand-alone element.
* </p>
* @param name name of the element to write.
* @throws IOException if an I/O error occurs.
* @see #startElement(String,XmlAttributes)
* @see #startElement(String)
* @see #writeStandAloneElement(String)
*/
public void writeStandAloneElement(String name) throws IOException {startElement(name, true, null, true);}
/**
* Writes a one-line comment.
*
* @param comment comment description.
* @throws IOException if an I/O error occurs.
*/
public void writeCommentLine(String comment) throws IOException {
out.print("<!-- " + comment + " -->");
println();
}
/**
* Writes an element opening sequence.
* <p>
* This is a covenience method and is stricly equivalent to calling
* <code>{@link #startElement(String,XmlAttributes,boolean) startElement}(name, attributes, false)</code>.
* </p>
* @param name name of the element to open.
* @throws IOException if an I/O error occurs.
* @param attributes attributes that this element will have.
* @see #startElement(String)
* @see #writeStandAloneElement(String)
* @see #writeStandAloneElement(String,XmlAttributes)
*/
public void startElement(String name, XmlAttributes attributes) throws IOException {startElement(name, false, attributes, false);}
/**
* Writes an element opening sequence.
* <p>
* Elements opened using this method will need to be closed using an {@link #endElement(String) endElement} call.
* </p>
* @param name name of the element to open.
* @param attributes attributes that this element will have.
* @param lineBreak if <code>true</code>, a line break will be printed after the element declaration.
* @throws IOException if an I/O error occurs.
* @see #startElement(String)
* @see #writeStandAloneElement(String)
* @see #writeStandAloneElement(String,XmlAttributes)
*/
public void startElement(String name, XmlAttributes attributes, boolean lineBreak) throws IOException {startElement(name, false, attributes, lineBreak);}
/**
* Writes a stand-alone element.
* <p>
* Elements opened using this method will not need to be closed
* </p>
* <p>
* A line break will always be printed after a stand-alone element.
* </p>
* @param name name of the element to write.
* @param attributes attributes that this element will be closed immediately.
* @throws IOException if an I/O error occurs.
* @see #startElement(String)
* @see #startElement(String,XmlAttributes)
* @see #writeStandAloneElement(String)
*/
public void writeStandAloneElement(String name, XmlAttributes attributes) throws IOException {startElement(name, true, attributes, true);}
/**
* Writes an element opening sequence.
* @param name name of the element to open.
* @param isStandAlone whether or not this element should be closed immediately.
* @param attributes XML attributes for this element.
* @throws IOException if an I/O error occurs.
*/
private void startElement(String name, boolean isStandAlone, XmlAttributes attributes, boolean lineBreak) throws IOException {
// Prints indentation if necessary.
indent();
// Opens the element.
out.print('<');
out.print(name);
// Writes attributes, if any.
if(attributes != null) {
Iterator<String> names;
String attName;
names = attributes.names();
while(names.hasNext()) {
attName = names.next();
out.print(' ');
out.print(attName);
out.print("=\"");
out.print(escape(attributes.getValue(attName)));
out.print("\"");
}
}
// Closes the element if necessary.
if(isStandAlone)
out.print('/');
else
offset += OFFSET_INCREMENT;
// Finishes the element opening sequence.
out.print('>');
// Stand-alone elements are followed by a line break.
if(lineBreak)
println();
if(out.checkError())
throw new IOException();
}
/**
* Writes an element closing sequence.
* @param name name of the element to close.
* @throws IOException if an I/O error occurs.
*/
public void endElement(String name) throws IOException {
// Updates the indentation, and prints it if necessary.
offset -= OFFSET_INCREMENT;
indent();
// Writes the element closing sequence.
out.print("</");
out.print(name);
out.print('>');
println();
if(out.checkError())
throw new IOException();
}
// - CDATA handling --------------------------------------------------
// -------------------------------------------------------------------
/**
* Writes the specified CDATA to the XML stream.
* @param cdata content to write to the XML stream.
* @throws IOException if an I/O error occurs.
*/
public void writeCData(String cdata) throws IOException {
indent();
out.print(escape(cdata));
if(out.checkError())
throw new IOException();
}
/**
* Escapes XML content, replacing special characters by their proper value.
* @param data data to escape.
* @return the escaped content.
* @throws IOException if an I/O error occurs.
*/
public String escape(String data) throws IOException {
int position;
for(int i = 0; i < ENTITIES.length; i++) {
position = 0;
while((position = data.indexOf(ENTITIES[i], position)) != -1) {
data = data.substring(0, position) + ENTITY_REPLACEMENTS[i] +
(position == data.length() -1 ? "" : data.substring(position + 1, data.length()));
position = position + ENTITY_REPLACEMENTS[i].length();
}
}
return data;
}
// - Indentation handling --------------------------------------------
// -------------------------------------------------------------------
/**
* Prints a line break.
* @throws IOException if an I/O error occurs.
*/
public void println() throws IOException {
out.println();
printIndentation = true;
if(out.checkError())
throw new IOException();
}
/**
* If necessary, prints indentation.
* @throws IOException if an I/O error occurs.
*/
private void indent() throws IOException {
if(printIndentation) {
for(int i = 0; i < offset; i++)
out.print(' ');
printIndentation = false;
}
if(out.checkError())
throw new IOException();
}
// - Misc. -----------------------------------------------------------
// -------------------------------------------------------------------
/**
* Closes the XML stream.
* @throws IOException if an I/O error occurs.
*/
public void close() throws IOException {out.close();}
}