/*
 * Copyright 
 *   2005 axYus - www.axyus.com
 *   2005 C.Marchand - christophe.marchand@axyus.com
 * 
 * This file is part of XEMELIOS.
 * 
 * XEMELIOS is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * XEMELIOS 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with XEMELIOS; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package fr.gouv.finances.cp.utils.xml.marshal;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.Stack;
import java.util.Vector;

/**
 * This class defines an xml writer.
 * <p>License : LGPL
 * @author: Christophe MARCHAND
 */
public class XmlOutputter {

    private static final String DEFAULT_ENCODING = "UTF-8";
    private String encoding;
    private OutputStreamWriter os;
    private static final String CRLF = System.getProperty("line.separator");
    private boolean writeStarted = false;
    Charset cs = null;
    /**
     * Indent level. Incremented by <code>startTag(String)</code>, decremented by
     * <code>endTag(String)</code>.
     */
    private int indentLevel = 0;
    /**
     * Indent string.
     */
    public static final transient String INDENT_STR = " ";
    /**
     * A vector to store attributes.
     */
    private Vector<Attribute> attributes = null;
    /**
     * to store if attributes have been written or not.
     */
    private boolean attributesFlushed = false;
    /**
     * Indent string.
     */
    private String indentStr = INDENT_STR;
    /**
     * a stack to remember if top-element as child or not.
     */
    private Stack<MutableBoolean> stack = null;
    private CharacterEscaper escaper = null;

    class Attribute extends java.lang.Object {

        private String attName = null;
        private String value = null;

        public Attribute(String attName, String value) {
            this.attName = attName;
            this.value = value;
        }

        @Override
        public String toString() {
            StringBuffer sb = new StringBuffer();
            sb.append(" ").append(attName).append("=\"");
            if (value != null) {
                sb.append(value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;"));
            }
            sb.append("\"");
            return sb.toString();
        }
    }

    /**
     * Constructs a XmlOutputter object based on a <code>OutputStream</code>.
     * 
     * @param out the outputstream to write to
     * @param encoding The encoding of xml output
     * @see java.io.OutputStream
     */
    public XmlOutputter(OutputStreamWriter out, String encoding) {
        super();
        this.os = out;
        this.encoding = encoding;
        init();
    }

    /**
     * Constructs a XmlOutputter object based on a <code>OutputStream</code>, with the default encoding (UTF-8).
     * @param out the outputstream to write to
     */
    public XmlOutputter(OutputStreamWriter out) {
        this(out, DEFAULT_ENCODING);
    }

    /**
     * Writes a int attribute.
     * @param tag java.lang.String
     * @param value int
     */
    public void addAttribute(String attr, int value) {
        addAttribute(attr, Integer.toString(value));
    }

    /**
     * Writes a long attribute.
     * @param tag java.lang.String
     * @param value long
     */
    public void addAttribute(String attr, long value) {
        addAttribute(attr, Long.toString(value));
    }

    /**
     * Writes an attribute.
     * @param tag java.lang.String
     * @param value java.lang.String
     */
    public void addAttribute(String attr, String value) {
        attributes.add(new Attribute(attr, value));
    }

    /**
     * Writes a BigDecimal attribute.
     * @param tag java.lang.String
     * @param value java.math.BigDecimal
     */
    public void addAttribute(String attr, java.math.BigDecimal value) {
        addAttribute(attr, value.toString());
    }

    /**
     * Writes a boolean attribute.
     * @param tag java.lang.String
     * @param value boolean
     */
    public void addAttribute(String attr, boolean value) {
        addAttribute(attr, value ? "true" : "false");
    }

    /**
     * Adds character data (#CDATA) to encapsulated Writer.
     * @param cData java.lang.String
     */
    public void addCharacterData(String cData) {
        flushAttributes(false);
        try {
            os.write(escaper.escapeString(cData));
        } catch (Throwable t) {
        }
        if (cData.indexOf("\n") != (-1)) {
            (stack.peek()).setValue(true);
        }
    }

    public void addComment(String comment) {
        writeStarted = true;
        flushAttributes(false);
        try {
            os.write("<!-- ");
            os.write(comment);
            os.write(" -->");
        } catch (Throwable t) {
        }
    }

    /**
     * Close the stack-on-top element. If <code>tag</code> is different of stack-on-top,
     * throws an <code>InvalidParameterException</code>.
     * @param tag java.lang.String
     * @exception java.security.InvalidParameterException If tag name
     * doesn't match the previously opened one.
     */
    public void endTag(String tag) throws java.security.InvalidParameterException {
        if (attributesFlushed) {
            indentLevel--;
            MutableBoolean bool = (MutableBoolean) stack.pop();
            if (bool.booleanValue()) {
                try {
                    writeIndent();
                } catch (Throwable t) {
                }
            }
            try {
                os.write("</");
                os.write(tag);
                os.write(">");
            } catch (Throwable t) {
            }
            if (indentLevel == 0) {
                try {
                    os.write(CRLF);
                } catch (Throwable t) {
                }
            }
        } else {
            flushAttributes(true);
            indentLevel--;
        }
    }

    /**
     * writes attributes to writer. Adds <code>/&gt;</code> if <code>close</code>,
     * and just <code>&gt;</code> otherwise.
     * @param close boolean
     */
    private void flushAttributes(boolean close) {
        if (!attributesFlushed) {
            for (Attribute attr : attributes) {
                try {
                    os.write(" ");
                    os.write(attr.attName);
                    os.write("=\"");
                    if(attr.value!=null)
                        os.write(escaper.escapeString(attr.value));
                    os.write("\"");
                } catch (Throwable t) {
                }
            }
            attributesFlushed = true;
            try {
                if (close) {
                    os.write("/>");
                } else {
                    os.write(">");
                }
            } catch (Throwable t) {
            }
        }
    }

    /**
     * Class initialization method. Called by all constructors.
     * If you override this method, do not forget to add <code>super.init()</code>
     * in your code.
     */
    private void init() {
        attributes = new Vector<Attribute>();
        stack = new Stack<MutableBoolean>();
        writeStarted = true;
        escaper = CharacterEscaperFactory.getCharacterEscaper(encoding);
        try {
            os.write("<?xml version=\"1.0\" encoding=\"");
            os.write(encoding);
            os.write("\"?");
        } catch (Throwable t) {
        }
    }

    /**
     * An easy way to define an custom indent string.
     * @param s java.lang.String
     */
    public void setIndentStr(String s) {
        indentStr = s;
    }

    /**
     * Opens a new Tag.
     * @param tag java.lang.String
     */
    public void startTag(String tag) {
        flushAttributes(false);
        attributesFlushed = false;
        attributes.removeAllElements();
        try {
            writeIndent();
            os.write("<");
            os.write(tag);
            indentLevel++;
            if (!stack.isEmpty()) {
                ((MutableBoolean) stack.peek()).setValue(true);
            }
        } catch (Throwable t) {
        }
        stack.push(new MutableBoolean(false));
    }

    /**
     * Writes the needed indentation.
     */
    public void writeIndent() throws IOException {
        os.write(CRLF);
        for (int i = 0; i < indentLevel; i++) {
            os.write(indentStr);
        }
    }

    public void close() throws IOException {
        os.flush();
        os.close();
    }
    
    public OutputStreamWriter getWriter() { return os; }
}
