package basics.xml.writer;

import java.io.IOException;
import java.io.Writer;
import java.util.Stack;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * @author NWiechmann
 */
public class XMLSerializer extends DefaultHandler
{
private final StringBuilder chars             = new StringBuilder();
private final Writer        w;
private final boolean       newline;
private final String        indent;
private boolean             isDocumentStarted = false;
@SuppressWarnings("unchecked")
private final Stack         isStartTagClosed  = new Stack();

/**
 * Storage optimized xml serializer. Serializes all into one line.
 * @param w the writer target
 */
public XMLSerializer(Writer w)
{
   this.w = w;
   this.indent = null;
   this.newline = false;
}

/**
 * Constructor for xml serializer optimized for reading. Serializes each xml tag into new line and
 * applies specified indentation. A value of null suppresses indentation.
 * @param w the writer target
 * @param indent number of whitespaces to use for indentation
 */
public XMLSerializer(Writer w, int indent)
{
   this.w = w;
   this.newline = true;
   if((indent > 0) && (indent < 100))
   {
      StringBuilder b = new StringBuilder(indent);
      while(indent-- > 0)
         b.append(' ');
      this.indent = b.toString();
   }
   else
   {
      this.indent = null;
   }
}

/**
 * @see org.xml.sax.ContentHandler#startDocument()
 */
public void startDocument() throws SAXException
{
   this.appendXMLDeclaration();
}

/**
 * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String,
 * java.lang.String, org.xml.sax.Attributes)
 */
@SuppressWarnings("unchecked")
public void startElement(String uri, String localName, String qName, Attributes attr)
   throws SAXException
{
   this.appendXMLDeclaration();
   try
   {
      if(!this.isStartTagClosed.isEmpty()
         && !((Boolean)this.isStartTagClosed.peek()).booleanValue())
      {
         this.w.write(">");
         if(this.newline)
            this.w.write('\n');
         this.isStartTagClosed.setElementAt(Boolean.TRUE, this.isStartTagClosed.size() - 1);
      }
      if(this.indent != null)
      {
         for(int i = 0; i < this.isStartTagClosed.size(); i++)
            this.w.write(this.indent);
      }
      this.w.write("<");
      this.w.write(qName);
      for(int i = 0; i < attr.getLength(); i++)
      {
         this.w.write(" ");
         this.w.write(attr.getQName(i));
         this.w.write("=\"");
         this.w.write(attr.getValue(i));
         this.w.write("\"");
      }
      this.chars.setLength(0);
      this.isStartTagClosed.push(Boolean.FALSE);
   }
   catch(IOException e)
   {
      throw new SAXException(e);
   }
}

/**
 * @see org.xml.sax.ContentHandler#characters(char[], int, int)
 */
public void characters(char [] ch, int start, int length) throws SAXException
{
   chars.append(ch, start, length);
}

/**
 * @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
 */
public void endElement(String uri, String localName, String qName) throws SAXException
{
   try
   {
      if(chars.length() > 0)
      {
         if(!((Boolean)this.isStartTagClosed.peek()).booleanValue())
            this.w.write(">");
         if((chars.indexOf("<") == -1) && (chars.indexOf(">") == -1))
         {
            this.w.write(chars.toString());
         }
         else
         {
            this.w.write("<![CDATA[");
            this.w.write(chars.toString());
            this.w.write("]]>");
         }
         this.w.write("</");
         this.w.write(qName);
         this.w.write(">");
         this.chars.setLength(0);
      }
      else
         if(!((Boolean)this.isStartTagClosed.peek()).booleanValue())
         {
            // empty element
            this.w.write("/>");
         }
         else
         {
            // has childreen
            if(this.indent != null)
            {
               for(int i = 1; i < this.isStartTagClosed.size(); i++)
                  this.w.write(this.indent);
            }
            this.w.write("</");
            this.w.write(qName);
            this.w.write(">");
         }
      if(this.newline)
         this.w.write('\n');
      this.isStartTagClosed.pop();
   }
   catch(IOException e)
   {
      throw new SAXException(e);
   }
}

/**
 * @see org.xml.sax.ContentHandler#endDocument()
 */
public void endDocument() throws SAXException
{
   try
   {
      this.w.flush();
   }
   catch(IOException e)
   {
      throw new SAXException(e);
   }
}

/**
 * Reset the serializer for reuse. Don't serviceId while processing events isn't finished!
 */
public void reset()
{
   this.isDocumentStarted = false;
   this.chars.setLength(0);
}

/**
 * Appends the xml declaration to the writer. Does it the first time only.
 * @throws SAXException
 */
private void appendXMLDeclaration() throws SAXException
{
   if(!this.isDocumentStarted)
   {
      try
      {
         this.w.write("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
         if(this.newline)
            this.w.write('\n');
         this.isDocumentStarted = true;
      }
      catch(IOException e)
      {
         throw new SAXException(e);
      }
   }
}
/*
 * public static void main(String[] args) throws SAXException { StringWriter w = new StringWriter();
 * StringBuilder b = w.getBuffer(); XMLSerializer s = new XMLSerializer(w, 2); AttributesImpl attr =
 * new AttributesImpl(); attr.addAttribute("", "id", "id", "CDATA", "fooid"); s.startElement("",
 * "product", "product", attr); System.out.print(b.toString()); b.setLength(0);
 * attr.removeAttribute(0); s.startElement("", "title", "title", attr);
 * System.out.print(b.toString()); b.setLength(0); s.characters("kaufmich".toCharArray(), 0,
 * "kaufmich".length()); System.out.print(b.toString()); b.setLength(0); s.endElement("", "title",
 * "title"); System.out.print(b.toString()); b.setLength(0); s.startElement("", "description",
 * "description", attr); System.out.print(b.toString()); b.setLength(0); s.startElement("",
 * "description2", "description2", attr); System.out.print(b.toString()); b.setLength(0);
 * s.endElement("", "description2", "description2"); System.out.print(b.toString()); b.setLength(0);
 * s.endElement("", "description", "description"); System.out.print(b.toString()); b.setLength(0);
 * s.endElement("", "product", "product"); System.out.print(b.toString()); b.setLength(0); }
 */
}
