/*
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program 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.
 *
 * Copyright (c) 2006 - 2009 Object Refinery Ltd, Pentaho Corporation and Contributors.  All rights reserved.
 */

package org.pentaho.reporting.libraries.xmlns.writer;

import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Properties;

import org.pentaho.reporting.libraries.base.util.FastStack;
import org.pentaho.reporting.libraries.base.util.ObjectUtilities;
import org.pentaho.reporting.libraries.base.util.StringUtils;
import org.pentaho.reporting.libraries.xmlns.common.AttributeList;

/**
 * A support class for writing XML files.
 *
 * @author Thomas Morgner
 */
public class XmlWriterSupport
{
  /**
   * An internal state-management class containing the state for nested
   * tags.
   */
  private static class ElementLevel
  {
    private String namespace;
    private String prefix;
    private String tagName;
    private DeclaredNamespaces namespaces;

    /**
     * Creates a new ElementLevel object.
     *
     * @param namespace  the namespace of the current tag (can be null).
     * @param prefix     the namespace prefix of the current tag (can be null).
     * @param tagName    the tagname (never null).
     * @param namespaces the collection of all currently known namespaces (never null).
     */
    protected ElementLevel(final String namespace,
                           final String prefix,
                           final String tagName,
                           final DeclaredNamespaces namespaces)
    {
      if (tagName == null)
      {
        throw new NullPointerException();
      }
      if (namespaces == null)
      {
        throw new NullPointerException();
      }
      this.prefix = prefix;
      this.namespace = namespace;
      this.tagName = tagName;
      this.namespaces = namespaces;
    }

    /**
     * Creates a new ElementLevel with no namespace information.
     *
     * @param tagName    the xml-tagname.
     * @param namespaces the currently known namespaces.
     */
    protected ElementLevel(final String tagName,
                           final DeclaredNamespaces namespaces)
    {
      if (tagName == null)
      {
        throw new NullPointerException();
      }
      if (namespaces == null)
      {
        throw new NullPointerException();
      }
      this.namespaces = namespaces;
      this.tagName = tagName;
    }

    /**
     * Returns the defined namespace prefix for this entry.
     *
     * @return the namespace prefix.
     */
    public String getPrefix()
    {
      return prefix;
    }

    /**
     * Returns the defined namespace uri for this entry.
     *
     * @return the namespace uri.
     */
    public String getNamespace()
    {
      return namespace;
    }

    /**
     * Returns the tagname for this entry.
     *
     * @return the tagname.
     */
    public String getTagName()
    {
      return tagName;
    }

    /**
     * Returns the map of defined namespace for this entry.
     *
     * @return the namespaces.
     */
    public DeclaredNamespaces getNamespaces()
    {
      return namespaces;
    }
  }


  /**
   * A constant for controlling the indent function.
   */
  public static final int OPEN_TAG_INCREASE = 1;

  /**
   * A constant for controlling the indent function.
   */
  public static final int CLOSE_TAG_DECREASE = 2;

  /**
   * A constant for controlling the indent function.
   */
  public static final int INDENT_ONLY = 3;

  /**
   * A constant for close.
   */
  public static final boolean CLOSE = true;

  /**
   * A constant for open.
   */
  public static final boolean OPEN = false;

  /**
   * A list of safe tags.
   */
  private TagDescription safeTags;

  /**
   * The indent level for that writer.
   */
  private FastStack openTags;

  /**
   * The indent string.
   */
  private String indentString;

  private boolean lineEmpty;
  private int additionalIndent;

  private boolean alwaysAddNamespace;
  private boolean assumeDefaultNamespace;
  private HashMap impliedNamespaces;
  private boolean writeFinalLinebreak;
  private boolean htmlCompatiblityMode;
  private String lineSeparator;
  private StringBuffer normalizeBuffer;

  /**
   * Default Constructor. The created XMLWriterSupport will not have no safe tags and starts with an indention level of
   * 0.
   */
  public XmlWriterSupport()
  {
    this(new DefaultTagDescription(), "  ");
  }


  /**
   * Creates a new support instance.
   *
   * @param safeTags     the tags that are safe for line breaks.
   * @param indentString the indent string.
   */
  public XmlWriterSupport(final TagDescription safeTags,
                          final String indentString)
  {
    this(safeTags, indentString, StringUtils.getLineSeparator());
  }

  /**
   * Create a new XmlWriterSupport instance.
   *
   * @param safeTags      the tags that are safe for line breaks.
   * @param indentString  the indent string.
   * @param lineseparator the lineseparator that should be used for writing XML files.
   */
  public XmlWriterSupport(final TagDescription safeTags,
                          final String indentString,
                          final String lineseparator)
  {
    if (indentString == null)
    {
      throw new NullPointerException("IndentString must not be null");
    }
    if (safeTags == null)
    {
      throw new NullPointerException("SafeTags must not be null");
    }
    if (lineseparator == null)
    {
      throw new NullPointerException("LineSeparator must not be null");
    }

    this.normalizeBuffer = new StringBuffer(128);
    this.safeTags = safeTags;
    this.openTags = new FastStack();
    this.indentString = indentString;
    this.lineEmpty = true;
    this.writeFinalLinebreak = true;
    this.lineSeparator = lineseparator;

    addImpliedNamespace("http://www.w3.org/XML/1998/namespace", "xml");
  }

  /**
   * Checks, whether the HTML compatibility mode is enabled. In HTML compatibility
   * mode, closed empty tags will have a space between the tagname and the
   * close-indicator.
   *
   * @return true, if the HTML compatiblity mode is enabled, false otherwise.
   */
  public boolean isHtmlCompatiblityMode()
  {
    return htmlCompatiblityMode;
  }

  /**
   * Enables or disables the HTML Compatibility mode. In HTML compatibility
   * mode, closed empty tags will have a space between the tagname and the
   * close-indicator.
   *
   * @param htmlCompatiblityMode true, if the HTML compatiblity mode is enabled, false otherwise.
   */
  public void setHtmlCompatiblityMode(final boolean htmlCompatiblityMode)
  {
    this.htmlCompatiblityMode = htmlCompatiblityMode;
  }

  /**
   * Checks, whether the XML writer should always add a namespace prefix to
   * the attributes. The XML specification leaves it up to the application on
   * how to handle unqualified attributes. If this mode is enabled, all
   * attributes will always be fully qualified - which removed the ambugity
   * but may not be compatible with simple, non namespace aware parsers.
   *
   * @return true, if all attributes should be qualified, false otherwise.
   */
  public boolean isAlwaysAddNamespace()
  {
    return alwaysAddNamespace;
  }

  /**
   * Defines, whether the XML writer should always add a namespace prefix to
   * the attributes. The XML specification leaves it up to the application on
   * how to handle unqualified attributes. If this mode is enabled, all
   * attributes will always be fully qualified - which removed the ambugity
   * but may not be compatible with simple, non namespace aware parsers.
   *
   * @param alwaysAddNamespace set to true, if all attributes should be qualified, false otherwise.
   */
  public void setAlwaysAddNamespace(final boolean alwaysAddNamespace)
  {
    this.alwaysAddNamespace = alwaysAddNamespace;
  }

  /**
   * Returns the indent level that should be added to the automaticly
   * computed indentation.
   *
   * @return the indent level.
   */
  public int getAdditionalIndent()
  {
    return additionalIndent;
  }

  /**
   * Defines the indent level that should be added to the automaticly
   * computed indentation.
   *
   * @param additionalIndent the indent level.
   */
  public void setAdditionalIndent(final int additionalIndent)
  {
    this.additionalIndent = additionalIndent;
  }

  /**
   * Returns the line separator.
   *
   * @return the line separator.
   */
  public String getLineSeparator()
  {
    return lineSeparator;
  }

  /**
   * Writes an opening XML tag that has no attributes.
   *
   * @param w            the writer.
   * @param namespaceUri the namespace URI for the element.
   * @param name         the tag name.
   * @throws java.io.IOException if there is an I/O problem.
   */
  public void writeTag(final Writer w,
                       final String namespaceUri,
                       final String name)
      throws IOException
  {
    writeTag(w, namespaceUri, name, null, XmlWriterSupport.OPEN);
  }

  /**
   * Writes a closing XML tag.
   *
   * @param w the writer.
   * @throws java.io.IOException if there is an I/O problem.
   */
  public void writeCloseTag(final Writer w)
      throws IOException
  {
    indentForClose(w);
    final ElementLevel level = (ElementLevel) openTags.pop();

    setLineEmpty(false);

    w.write("</");
    final String prefix = level.getPrefix();
    if (prefix != null)
    {
      w.write(prefix);
      w.write(":");
      w.write(level.getTagName());
    }
    else
    {
      w.write(level.getTagName());
    }
    w.write(">");
    doEndOfLine(w);
  }

  /**
   * Writes a linebreak to the writer.
   *
   * @param writer the writer.
   * @throws IOException if there is a problem writing to the character stream.
   */
  public void writeNewLine(final Writer writer)
      throws IOException
  {
    if (isLineEmpty() == false)
    {
      writer.write(lineSeparator);
      setLineEmpty(true);
    }
  }

  /**
   * Checks, whether the currently generated line of text is empty.
   *
   * @return true, if the line is empty, false otherwise.
   */
  public boolean isLineEmpty()
  {
    return lineEmpty;
  }

  /**
   * A marker flag to track, wether the current line is empty. This influences the indention.
   *
   * @param lineEmpty defines, whether the current line should be treated as empty line.
   */
  public void setLineEmpty(final boolean lineEmpty)
  {
    this.lineEmpty = lineEmpty;
  }

  /**
   * Writes an opening XML tag with an attribute/value pair.
   *
   * @param w              the writer.
   * @param namespace      the namespace URI for the element
   * @param name           the tag name.
   * @param attributeName  the attribute name.
   * @param attributeValue the attribute value.
   * @param close          controls whether the tag is closed.
   * @throws java.io.IOException if there is an I/O problem.
   */
  public void writeTag(final Writer w,
                       final String namespace,
                       final String name,
                       final String attributeName,
                       final String attributeValue,
                       final boolean close)
      throws IOException
  {
    if (attributeName != null)
    {
      final AttributeList attr = new AttributeList();
      attr.setAttribute(namespace, attributeName, attributeValue);
      writeTag(w, namespace, name, attr, close);
    }
    else
    {
      writeTag(w, namespace, name, null, close);
    }
  }

  /**
   * Adds an implied namespace to the document. Such a namespace is not explicitly declared, it is assumed that the
   * xml-parser knows the prefix by some other means. Using implied namespaces for standalone documents is almost always
   * a bad idea.
   *
   * @param uri    the uri of the namespace.
   * @param prefix the defined prefix.
   */
  public void addImpliedNamespace(final String uri, final String prefix)
  {
    if (openTags.isEmpty() == false)
    {
      throw new IllegalStateException("Cannot modify the implied namespaces in the middle of the processing");
    }

    if (prefix == null)
    {
      if (impliedNamespaces == null)
      {
        return;
      }
      impliedNamespaces.remove(uri);
    }
    else
    {
      if (impliedNamespaces == null)
      {
        impliedNamespaces = new HashMap();
      }
      impliedNamespaces.put(uri, prefix);
    }
  }

  /**
   * Copies all currently declared namespaces of the given XmlWriterSupport instance as new implied namespaces into this
   * instance.
   *
   * @param writerSupport the Xml-writer from where to copy the declared namespaces.
   */
  public void copyNamespaces(final XmlWriterSupport writerSupport)
  {
    if (openTags.isEmpty() == false)
    {
      throw new IllegalStateException("Cannot modify the implied namespaces in the middle of the processing");
    }

    if (impliedNamespaces == null)
    {
      impliedNamespaces = new HashMap();
    }

    if (writerSupport.openTags.isEmpty() == false)
    {
      final ElementLevel parent = (ElementLevel) writerSupport.openTags.peek();
      impliedNamespaces.putAll(parent.getNamespaces().getNamespaces());
    }

    if (writerSupport.impliedNamespaces != null)
    {
      impliedNamespaces.putAll(writerSupport.impliedNamespaces);
    }
  }

  /**
   * Checks, whether the given URI is defined as valid namespace.
   *
   * @param uri the uri of the namespace.
   * @return true, if there's a namespace defined, false otherwise.
   */
  public boolean isNamespaceDefined(final String uri)
  {
    if (impliedNamespaces != null)
    {
      if (impliedNamespaces.containsKey(uri))
      {
        return true;
      }
    }
    if (openTags.isEmpty())
    {
      return false;
    }
    final ElementLevel parent = (ElementLevel) openTags.peek();
    return parent.getNamespaces().isNamespaceDefined(uri);
  }

  /**
   * Checks, whether the given namespace prefix is defined.
   *
   * @param prefix the namespace prefix.
   * @return true, if the prefix is defined, false otherwise.
   */
  public boolean isNamespacePrefixDefined(final String prefix)
  {
    if (impliedNamespaces != null)
    {
      if (impliedNamespaces.containsValue(prefix))
      {
        return true;
      }
    }
    if (openTags.isEmpty())
    {
      return false;
    }
    final ElementLevel parent = (ElementLevel) openTags.peek();
    return parent.getNamespaces().isPrefixDefined(prefix);
  }

  /**
   * Returns all namespaces as properties-collection. This reflects the currently defined namespaces, therefore
   * calls to writeOpenTag(..) might cause this method to return different collections.
   *
   * @return the defined namespaces.
   */
  public Properties getNamespaces()
  {
    final Properties namespaces = new Properties();
    if (openTags.isEmpty())
    {
      if (impliedNamespaces != null)
      {
        //noinspection UseOfPropertiesAsHashtable
        namespaces.putAll(impliedNamespaces);
      }
      return namespaces;
    }

    final ElementLevel parent = (ElementLevel) openTags.peek();
    //noinspection UseOfPropertiesAsHashtable
    namespaces.putAll(parent.getNamespaces().getNamespaces());
    return namespaces;
  }

  /**
   * Computes the current collection of defined namespaces.
   *
   * @return the namespaces declared at this writing position.
   */
  protected DeclaredNamespaces computeNamespaces()
  {
    if (openTags.isEmpty())
    {
      final DeclaredNamespaces namespaces = new DeclaredNamespaces();
      if (impliedNamespaces != null)
      {
        return namespaces.add(impliedNamespaces);
      }
      return namespaces;
    }

    final ElementLevel parent = (ElementLevel) openTags.peek();
    return parent.getNamespaces();
  }

  /**
   * Writes an opening XML tag along with a list of attribute/value pairs.
   *
   * @param w            the writer.
   * @param namespaceUri the namespace uri for the element (can be null).
   * @param name         the tag name.
   * @param attributes   the attributes.
   * @param close        controls whether the tag is closed.
   * @throws java.io.IOException if there is an I/O problem.
   */
  public void writeTag(final Writer w,
                       final String namespaceUri,
                       final String name,
                       final AttributeList attributes,
                       final boolean close)
      throws IOException
  {
    if (name == null)
    {
      throw new NullPointerException();
    }

    indent(w);
    setLineEmpty(false);

    DeclaredNamespaces namespaces = computeNamespaces();

    if (attributes != null)
    {
      namespaces = namespaces.add(attributes);
    }

    w.write("<");

    if (namespaceUri == null)
    {
      w.write(name);
      openTags.push(new ElementLevel(name, namespaces));
    }
    else
    {
      final String nsPrefix = namespaces.getPrefix(namespaceUri);
      if (nsPrefix == null)
      {
        throw new IllegalArgumentException(
            "Namespace " + namespaceUri + " is not defined.");
      }
      if ("".equals(nsPrefix))
      {
        w.write(name);
        openTags.push(new ElementLevel(namespaceUri, null, name, namespaces));
      }
      else
      {
        w.write(nsPrefix);
        w.write(":");
        w.write(name);
        openTags.push(new ElementLevel(namespaceUri, nsPrefix, name, namespaces));
      }
    }

    if (attributes != null)
    {
      final AttributeList.AttributeEntry[] entries = attributes.toArray();
      for (int i = 0; i < entries.length; i++)
      {
        final AttributeList.AttributeEntry entry = entries[i];
        w.write(" ");

        buildAttributeName(entry, namespaces, w);
        w.write("=\"");
        writeTextNormalized(w, entry.getValue(), true);
        w.write("\"");
      }
    }

    if (close)
    {
      if (isHtmlCompatiblityMode())
      {
        w.write(" />");
      }
      else
      {
        w.write("/>");
      }

      openTags.pop();
      doEndOfLine(w);
    }
    else
    {
      w.write(">");
      doEndOfLine(w);
    }
  }

  /**
   * Conditionally writes an end-of-line character. The End-Of-Line is only written, if the tag description indicates
   * that the currently open element does not expect any CDATA inside. Writing a newline for CDATA-elements may have
   * sideeffects.
   *
   * @param w the writer.
   * @throws java.io.IOException if there is an I/O problem.
   */
  private void doEndOfLine(final Writer w)
      throws IOException
  {
    if (openTags.isEmpty())
    {
      if (isWriteFinalLinebreak())
      {
        writeNewLine(w);
      }
    }
    else
    {
      final ElementLevel level = (ElementLevel) openTags.peek();
      if (getTagDescription().hasCData
          (level.getNamespace(), level.getTagName()) == false)
      {
        writeNewLine(w);
      }
    }
  }

  /**
   * Processes a single attribute and searches for namespace declarations. If a namespace declaration is found, it is
   * returned in a normalized way. If namespace processing is active, the attribute name will be fully qualified with
   * the prefix registered for the attribute's namespace URI.
   *
   * @param entry      the attribute enty.
   * @param namespaces the currently known namespaces.
   * @param writer     the writer that should receive the formatted attribute name.
   * @throws IOException if an IO error occured.
   */
  private void buildAttributeName(final AttributeList.AttributeEntry entry,
                                  final DeclaredNamespaces namespaces,
                                  final Writer writer) throws IOException
  {
    final ElementLevel currentElement = (ElementLevel) openTags.peek();
    final String name = entry.getName();
    final String namespaceUri = entry.getNamespace();

    if (isAlwaysAddNamespace() == false &&
        ObjectUtilities.equal(currentElement.getNamespace(), namespaceUri))
    {
      writer.write(name);
      return;
    }

    if (namespaceUri == null)
    {
      writer.write(name);
      return;
    }

    if (AttributeList.XMLNS_NAMESPACE.equals(namespaceUri))
    {
      // its a namespace declaration.
      if ("".equals(name))
      {
        writer.write("xmlns");
        return;
      }

      writer.write("xmlns:");
      writer.write(name);
      return;
    }

    final String namespacePrefix = namespaces.getPrefix(namespaceUri);
    if (namespacePrefix != null && "".equals(namespacePrefix) == false)
    {
      writer.write(namespacePrefix);
      writer.write(':');
      writer.write(name);
    }
    else
    {
      writer.write(name);
    }
  }

  /**
   * Normalizes the given string using a shared buffer.
   *
   * @param s                the string that should be XML-Encoded.
   * @param transformNewLine a flag controling whether to transform newlines into character-entities.
   * @return the transformed string.
   */
  public String normalizeLocal(final String s,
                               final boolean transformNewLine)
  {
    if (s == null)
    {
      return "";
    }
    normalizeBuffer.delete(0, normalizeBuffer.length());

    final int len = s.length();
    for (int i = 0; i < len; i++)
    {
      final char ch = s.charAt(i);

      switch (ch)
      {
        case '<':
        {
          normalizeBuffer.append("&lt;");
          break;
        }
        case '>':
        {
          normalizeBuffer.append("&gt;");
          break;
        }
        case '&':
        {
          normalizeBuffer.append("&amp;");
          break;
        }
        case '"':
        {
          normalizeBuffer.append("&quot;");
          break;
        }
        case '\n':
        {
          if (transformNewLine)
          {
            normalizeBuffer.append("&#x000a;");
          }
          else
          {
            normalizeBuffer.append('\n');
          }
          break;
        }
        case '\r':
        {
          if (transformNewLine)
          {
            normalizeBuffer.append("&#x000d;");
          }
          else
          {
            normalizeBuffer.append('\r');
          }
          break;
        }
        case 0x09:
        {
          normalizeBuffer.append(ch);
          break;
        }
        default:
        {
          if (ch >= 0x20)
          {
            normalizeBuffer.append(ch);
          }
        }
      }
    }

    final String retval = normalizeBuffer.toString();
    normalizeBuffer.delete(0, normalizeBuffer.length());
    return retval;
  }


  /**
   * Normalizes the given string and writes the result directly to the stream.
   *
   * @param writer           the writer that should receive the normalized content.
   * @param s                the string that should be XML-Encoded.
   * @param transformNewLine a flag controling whether to transform newlines into character-entities.
   * @throws IOException if writing to the stream failed.
   */
  public void writeTextNormalized(final Writer writer,
                                  final String s,
                                  final boolean transformNewLine) throws IOException
  {
    if (s == null)
    {
      return;
    }

    final char[] data = s.toCharArray();
    final int len = data.length;
    int startIdx = 0;
    int length = 0;
    for (int i = 0; i < len; i++)
    {
      final char ch = data[i];

      switch (ch)
      {
        case '<':
        {
          if (length != 0)
          {
            writer.write(data, startIdx, length);
            length = 0;
          }
          writer.write("&lt;");
          startIdx = i + 1;
          continue;
        }
        case '>':
        {
          if (length != 0)
          {
            writer.write(data, startIdx, length);
            length = 0;
          }
          writer.write("&gt;");
          startIdx = i + 1;
          continue;
        }
        case '&':
        {
          if (length != 0)
          {
            writer.write(data, startIdx, length);
            length = 0;
          }
          writer.write("&amp;");
          startIdx = i + 1;
          continue;
        }
        case '"':
        {
          if (length != 0)
          {
            writer.write(data, startIdx, length);
            length = 0;
          }
          writer.write("&quot;");
          startIdx = i + 1;
          continue;
        }
        case '\n':
        {
          if (transformNewLine)
          {
            if (length != 0)
            {
              writer.write(data, startIdx, length);
              length = 0;
            }
            writer.write("&#x000a;");
            startIdx = i + 1;
            continue;
          }
          break;
        }
        case '\r':
        {
          if (transformNewLine)
          {
            if (length != 0)
            {
              writer.write(data, startIdx, length);
              length = 0;
            }
            writer.write("&#x000d;");
            startIdx = i + 1;
            continue;
          }
          break;
        }
        case 0x09: // tab
        {
          break;
        }
        default:
        {
          if (ch >= 0x20)
          {
            // anything above the control-character range is ok.
            break;
          }
          // skip ..
          if (length != 0)
          {
            writer.write(data, startIdx, length);
            length = 0;
          }
          startIdx = i + 1;
          continue;
        }
      }
      length += 1;
    }

    if (length != 0)
    {
      writer.write(data, startIdx, length);
    }
  }

  /**
   * Normalises a string, replacing certain characters with their escape sequences so that the XML text is not
   * corrupted.
   *
   * @param s                the string.
   * @param transformNewLine true, if a newline in the string should be converted into a character entity.
   * @return the normalised string.
   */
  public static String normalize(final String s,
                                 final boolean transformNewLine)
  {
    if (s == null)
    {
      return "";
    }
    final int len = s.length();
    final StringBuffer str = new StringBuffer(len);

    for (int i = 0; i < len; i++)
    {
      final char ch = s.charAt(i);

      switch (ch)
      {
        case '<':
        {
          str.append("&lt;");
          break;
        }
        case '>':
        {
          str.append("&gt;");
          break;
        }
        case '&':
        {
          str.append("&amp;");
          break;
        }
        case '"':
        {
          str.append("&quot;");
          break;
        }
        case '\n':
        {
          if (transformNewLine)
          {
            str.append("&#x000a;");
          }
          else
          {
            str.append('\n');
          }
          break;
        }
        case '\r':
        {
          if (transformNewLine)
          {
            str.append("&#x000d;");
          }
          else
          {
            str.append('\r');
          }
          break;
        }
        case 0x09: // tab
        {
          str.append((char) 0x09);
        }
        default:
        {
          if (ch >= 0x20)
          {
            str.append(ch);
          }
        }
      }
    }

    return (str.toString());
  }

  /**
   * Indent the line. Called for proper indenting in various places.
   *
   * @param writer the writer which should receive the indentention.
   * @throws java.io.IOException if writing the stream failed.
   */
  public void indent(final Writer writer)
      throws IOException
  {
    if (openTags.isEmpty())
    {
      for (int i = 0; i < additionalIndent; i++)
      {
        writer.write(this.indentString);
      }
      return;
    }

    final ElementLevel level = (ElementLevel) openTags.peek();
    if (getTagDescription().hasCData(level.getNamespace(),
        level.getTagName()) == false)
    {
      doEndOfLine(writer);

      for (int i = 0; i < this.openTags.size(); i++)
      {
        writer.write(this.indentString);
      }

      for (int i = 0; i < additionalIndent; i++)
      {
        writer.write(this.indentString);
      }
    }
  }

  /**
   * Indent the line. Called for proper indenting in various places.
   *
   * @param writer the writer which should receive the indentention.
   * @throws java.io.IOException if writing the stream failed.
   */
  public void indentForClose(final Writer writer)
      throws IOException
  {
    if (openTags.isEmpty())
    {
      for (int i = 0; i < additionalIndent; i++)
      {
        writer.write(this.indentString);
      }
      return;
    }

    final ElementLevel level = (ElementLevel) openTags.peek();
    if (getTagDescription().hasCData(level.getNamespace(),
        level.getTagName()) == false)
    {
      doEndOfLine(writer);

      for (int i = 1; i < this.openTags.size(); i++)
      {
        writer.write(this.indentString);
      }
      for (int i = 0; i < additionalIndent; i++)
      {
        writer.write(this.indentString);
      }
    }
  }

  /**
   * Returns the list of safe tags.
   *
   * @return The list.
   */
  public TagDescription getTagDescription()
  {
    return this.safeTags;
  }

  /**
   * Writes a comment into the generated xml file.
   *
   * @param writer  the writer.
   * @param comment the comment text
   * @throws IOException if there is a problem writing to the character stream.
   */
  public void writeComment(final Writer writer, final String comment)
      throws IOException
  {
    if (openTags.isEmpty() == false)
    {
      final ElementLevel level = (ElementLevel) openTags.peek();
      if (getTagDescription().hasCData
          (level.getNamespace(), level.getTagName()) == false)
      {
        indent(writer);
      }
    }

    setLineEmpty(false);

    writer.write("<!-- ");
    writeTextNormalized(writer, comment, false);
    writer.write(" -->");
    doEndOfLine(writer);
  }

  /**
   * Checks, whether attributes of the same namespace as the current element should be written without a prefix.
   * Attributes without a prefix are considered to be not in any namespace at all. How to treat such attributes is
   * implementation dependent. (Appendix A; Section 6.2 of the XmlNamespaces recommendation)
   *
   * @return true, if attributes in the element's namespace should be written without a prefix, false to write all
   *         attributes with a prefix.
   */
  public boolean isAssumeDefaultNamespace()
  {
    return assumeDefaultNamespace;
  }

  /**
   * Defines, whether attributes of the same namespace as the current element should be written without a prefix.
   * Attributes without a prefix are considered to be not in any namespace at all. How to treat such attributes is
   * implementation dependent. (Appendix A; Section 6.2 of the XmlNamespaces recommendation)
   *
   * @param assumeDefaultNamespace true, if attributes in the element's namespace should be written without a prefix,
   *                               false to write all attributes with a prefix.
   */
  public void setAssumeDefaultNamespace(final boolean assumeDefaultNamespace)
  {
    this.assumeDefaultNamespace = assumeDefaultNamespace;
  }

  /**
   * Returns the current indention level.
   *
   * @return the indention level.
   */
  public int getCurrentIndentLevel()
  {
    return additionalIndent + openTags.size();
  }

  /**
   * Defines, whether the written XML file should end with an empty line.
   *
   * @param writeFinalLinebreak true, if an linebreak should be added at the
   *                            end of the file, false otherwise.
   */
  public void setWriteFinalLinebreak(final boolean writeFinalLinebreak)
  {
    this.writeFinalLinebreak = writeFinalLinebreak;
  }

  /**
   * Checks, whether the written XML file should end with an empty line.
   *
   * @return true, if an linebreak should be added at the
   *         end of the file, false otherwise.
   */
  public boolean isWriteFinalLinebreak()
  {
    return writeFinalLinebreak;
  }
}
