/* * Formatter.java July 2006 * * Copyright (C) 2006, Niall Gallagher <niallg@users.sf.net> * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. See the License for the specific language governing * permissions and limitations under the License. */ package org.simpleframework.xml.stream; import java.io.BufferedWriter; import java.io.Writer; /** * The <code>Formatter</code> object is used to format output as XML * indented with a configurable indent level. This is used to write * start and end tags, as well as attributes and values to the given * writer. The output is written directly to the stream with and * indentation for each element appropriate to its position in the * document hierarchy. If the indent is set to zero then no indent * is performed and all XML will appear on the same line. * * @see org.simpleframework.xml.stream.Indenter */ class Formatter { /** * Represents the prefix used when declaring an XML namespace. */ private static final char[] NAMESPACE = { 'x', 'm', 'l', 'n', 's' }; /** * Represents the XML escape sequence for the less than sign. */ private static final char[] LESS = { '&', 'l', 't', ';'}; /** * Represents the XML escape sequence for the greater than sign. */ private static final char[] GREATER = { '&', 'g', 't', ';' }; /** * Represents the XML escape sequence for the double quote. */ private static final char[] DOUBLE = { '&', 'q', 'u', 'o', 't', ';' }; /** * Represents the XML escape sequence for the single quote. */ private static final char[] SINGLE = { '&', 'a', 'p', 'o', 's', ';' }; /** * Represents the XML escape sequence for the ampersand sign. */ private static final char[] AND = { '&', 'a', 'm', 'p', ';' }; /** * This is used to open a comment section within the document. */ private static final char[] OPEN = { '<', '!', '-', '-', ' ' }; /** * This is used to close a comment section within the document. */ private static final char[] CLOSE = { ' ', '-', '-', '>' }; /** * Output buffer used to write the generated XML result to. */ private OutputBuffer buffer; /** * Creates the indentations that are used buffer the XML file. */ private Indenter indenter; /** * This is the writer that is used to write the XML document. */ private Writer result; /** * Represents the prolog to insert at the start of the document. */ private String prolog; /** * Represents the last type of content that was written. */ private Tag last; /** * Constructor for the <code>Formatter</code> object. This creates * an object that can be used to write XML in an indented format * to the specified writer. The XML written will be well formed. * * @param result this is where the XML should be written to * @param format this is the format object to use */ public Formatter(Writer result, Format format){ this.result = new BufferedWriter(result, 1024); this.indenter = new Indenter(format); this.buffer = new OutputBuffer(); this.prolog = format.getProlog(); } /** * This is used to write a prolog to the specified output. This is * only written if the specified <code>Format</code> object has * been given a non null prolog. If no prolog is specified then no * prolog is written to the generated XML. * * @throws Exception thrown if there is an I/O problem */ public void writeProlog() throws Exception { if(prolog != null) { write(prolog); write("\n"); } } /** * This is used to write any comments that have been set. The * comment will typically be written at the start of an element * to describe the purpose of the element or include debug data * that can be used to determine any issues in serialization. * * @param comment this is the comment that is to be written */ public void writeComment(String comment) throws Exception { String text = indenter.top(); if(last == Tag.START) { append('>'); } if(text != null) { append(text); append(OPEN); append(comment); append(CLOSE); } last = Tag.COMMENT; } /** * This method is used to write a start tag for an element. If a * start tag was written before this then it is closed. Before * the start tag is written an indent is generated and placed in * front of the tag, this is done for all but the first start tag. * * @param name this is the name of the start tag to be written * * @throws Exception thrown if there is an I/O exception */ public void writeStart(String name, String prefix) throws Exception{ String text = indenter.push(); if(last == Tag.START) { append('>'); } flush(); append(text); append('<'); if(!isEmpty(prefix)) { append(prefix); append(':'); } append(name); last = Tag.START; } /** * This is used to write a name value attribute pair. If the last * tag written was not a start tag then this throws an exception. * All attribute values written are enclosed in double quotes. * * @param name this is the name of the attribute to be written * @param value this is the value to assigne to the attribute * * @throws Exception thrown if there is an I/O exception */ public void writeAttribute(String name, String value, String prefix) throws Exception{ if(last != Tag.START) { throw new NodeException("Start element required"); } write(' '); write(name, prefix); write('='); write('"'); escape(value); write('"'); } /** * This is used to write the namespace to the element. This will * write the special attribute using the prefix and reference * specified. This will escape the reference if it is required. * * @param reference this is the namespace URI reference to use * @param prefix this is the prefix to used for the namespace * * @throws Exception thrown if there is an I/O exception */ public void writeNamespace(String reference, String prefix) throws Exception{ if(last != Tag.START) { throw new NodeException("Start element required"); } write(' '); write(NAMESPACE); if(!isEmpty(prefix)) { write(':'); write(prefix); } write('='); write('"'); escape(reference); write('"'); } /** * This is used to write the specified text value to the writer. * If the last tag written was a start tag then it is closed. * By default this will escape any illegal XML characters. * * @param text this is the text to write to the output * * @throws Exception thrown if there is an I/O exception */ public void writeText(String text) throws Exception{ writeText(text, Mode.ESCAPE); } /** * This is used to write the specified text value to the writer. * If the last tag written was a start tag then it is closed. * This will use the output mode specified. * * @param text this is the text to write to the output * * @throws Exception thrown if there is an I/O exception */ public void writeText(String text, Mode mode) throws Exception{ if(last == Tag.START) { write('>'); } if(mode == Mode.DATA) { data(text); } else { escape(text); } last = Tag.TEXT; } /** * This is used to write an end element tag to the writer. This * will close the element with a short <code>/></code> if the * last tag written was a start tag. However if an end tag or * some text was written then a full end tag is written. * * @param name this is the name of the element to be closed * * @throws Exception thrown if there is an I/O exception */ public void writeEnd(String name, String prefix) throws Exception { String text = indenter.pop(); if(last == Tag.START) { write('/'); write('>'); } else { if(last != Tag.TEXT) { write(text); } if(last != Tag.START) { write('<'); write('/'); write(name, prefix); write('>'); } } last = Tag.END; } /** * This is used to write a character to the output stream without * any translation. This is used when writing the start tags and * end tags, this is also used to write attribute names. * * @param ch this is the character to be written to the output */ private void write(char ch) throws Exception { buffer.write(result); buffer.clear(); result.write(ch); } /** * This is used to write plain text to the output stream without * any translation. This is used when writing the start tags and * end tags, this is also used to write attribute names. * * @param plain this is the text to be written to the output */ private void write(char[] plain) throws Exception { buffer.write(result); buffer.clear(); result.write(plain); } /** * This is used to write plain text to the output stream without * any translation. This is used when writing the start tags and * end tags, this is also used to write attribute names. * * @param plain this is the text to be written to the output */ private void write(String plain) throws Exception{ buffer.write(result); buffer.clear(); result.write(plain); } /** * This is used to write plain text to the output stream without * any translation. This is used when writing the start tags and * end tags, this is also used to write attribute names. * * @param plain this is the text to be written to the output * @param prefix this is the namespace prefix to be written */ private void write(String plain, String prefix) throws Exception{ buffer.write(result); buffer.clear(); if(!isEmpty(prefix)) { result.write(prefix); result.write(':'); } result.write(plain); } /** * This is used to buffer a character to the output stream without * any translation. This is used when buffering the start tags so * that they can be reset without affecting the resulting document. * * @param ch this is the character to be written to the output */ private void append(char ch) throws Exception { buffer.append(ch); } /** * This is used to buffer characters to the output stream without * any translation. This is used when buffering the start tags so * that they can be reset without affecting the resulting document. * * @param plain this is the string that is to be buffered */ private void append(char[] plain) throws Exception { buffer.append(plain); } /** * This is used to buffer characters to the output stream without * any translation. This is used when buffering the start tags so * that they can be reset without affecting the resulting document. * * @param plain this is the string that is to be buffered */ private void append(String plain) throws Exception{ buffer.append(plain); } /** * This method is used to write the specified text as a CDATA block * within the XML element. This is typically used when the value is * large or if it must be preserved in a format that will not be * affected by other XML parsers. For large text values this is * also faster than performing a character by character escaping. * * @param value this is the text value to be written as CDATA */ private void data(String value) throws Exception { write("<![CDATA["); write(value); write("]]>"); } /** * This is used to write the specified value to the output with * translation to any symbol characters or non text characters. * This will translate the symbol characters such as "&", * ">", "<", and """. This also writes any non text * and non symbol characters as integer values like "{". * * @param value the text value to be escaped and written */ private void escape(String value) throws Exception { int size = value.length(); for(int i = 0; i < size; i++){ escape(value.charAt(i)); } } /** * This is used to write the specified value to the output with * translation to any symbol characters or non text characters. * This will translate the symbol characters such as "&", * ">", "<", and """. This also writes any non text * and non symbol characters as integer values like "{". * * @param ch the text character to be escaped and written */ private void escape(char ch) throws Exception { char[] text = symbol(ch); if(text != null) { write(text); } else { write(ch); } } /** * This is used to flush the writer when the XML if it has been * buffered. The flush method is used by the node writer after an * end element has been written. Flushing ensures that buffering * does not affect the result of the node writer. */ public void flush() throws Exception{ buffer.write(result); buffer.clear(); result.flush(); } /** * This is used to convert the the specified character to unicode. * This will simply get the decimal representation of the given * character as a string so it can be written as an escape. * * @param ch this is the character that is to be converted * * @return this is the decimal value of the given character */ private String unicode(char ch) { return Integer.toString(ch); } /** * This method is used to determine if a root annotation value is * an empty value. Rather than determining if a string is empty * be comparing it to an empty string this method allows for the * value an empty string represents to be changed in future. * * @param value this is the value to determine if it is empty * * @return true if the string value specified is an empty value */ private boolean isEmpty(String value) { if(value != null) { return value.length() == 0; } return true; } /** * This is used to determine if the specified character is a text * character. If the character specified is not a text value then * this returns true, otherwise this returns false. * * @param ch this is the character to be evaluated as text * * @return this returns the true if the character is textual */ private boolean isText(char ch) { switch(ch) { case ' ': case '\n': case '\r': case '\t': return true; } if(ch > ' ' && ch <= 0x7E){ return ch != 0xF7; } return false; } /** * This is used to convert the specified character to an XML text * symbol if the specified character can be converted. If the * character cannot be converted to a symbol null is returned. * * @param ch this is the character that is to be converted * * @return this is the symbol character that has been resolved */ private char[] symbol(char ch) { switch(ch) { case '<': return LESS; case '>': return GREATER; case '"': return DOUBLE; case '\'': return SINGLE; case '&': return AND; } return null; } /** * This is used to enumerate the different types of tag that can * be written. Each tag represents a state for the writer. After * a specific tag type has been written the state of the writer * is updated. This is needed to write well formed XML text. */ private enum Tag { COMMENT, START, TEXT, END } }