content: /*
   * 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 com.aliyun.odps.simpleframework.xml.stream;
  
  import java.io.BufferedWriter;
  import java.io.Writer;
  import java.util.regex.Pattern;
  
  /**
   * 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 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 = { ' ', '-', '-', '>' };
  
     /**
      * This is used to determine whether a string contains escape
      * characters.
      */
     private static final Pattern XML_CHARS = Pattern.compile("[&<>]");
     
     /**
      * 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;
  
     /**
      * whether to wrap the element data with a CDATA block automatically.
      */
     private boolean autoCdata;
     
     /**
      * 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.autoCdata = format.getAutoCdata();
     }
  
     /**
      * 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('>');
        }
  
        boolean containsEscapeChars = XML_CHARS.matcher(text).find();
        boolean wrapWithCDATA = this.autoCdata && containsEscapeChars;
        if(mode == Mode.DATA || wrapWithCDATA) {
           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>/&gt;</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 "&amp;",
      * "&gt;", "&lt;", and "&quot;". This also writes any non text
      * and non symbol characters as integer values like "&#123;".
      *
      * @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 "&amp;",
      * "&gt;", "&lt;", and "&quot;". This also writes any non text
      * and non symbol characters as integer values like "&#123;".
      *
      * @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                
    }
  }
  