/*
 * 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.OutputStream;
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 OutputStream os;
	private static final String CRLF = System.getProperty("line.separator");
        private boolean writeStarted = false;
	/**
	 * 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;

	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(OutputStream 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(OutputStream 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(cData.getBytes(encoding)); } catch(Throwable t) {}
		if(cData.indexOf("\n")!=(-1)) {
			(stack.peek()).setValue(true);
		}
	}
        public void addComment(String comment) {
            writeStarted = true;
            flushAttributes(false);
            try { os.write("<!-- ".getBytes()); } catch(Throwable t) {}
            try { os.write(comment.getBytes(encoding)); } catch(Throwable t) {}
            try { os.write(" -->".getBytes()); } 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) {}
			}
			//print("</" + tag + ">");
			try { 
				os.write("</".getBytes());
				os.write(tag.getBytes(encoding));
				os.write(">".getBytes());
			} catch(Throwable t) {}
			if (indentLevel == 0) {
				//println();
				try { os.write(CRLF.getBytes()); } 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) {
				//String s = attributes.get(i).toString();
				//print((Attribute) attributes.get(i));
                                try { os.write(" ".getBytes()); } catch(Throwable t) {}
				try { os.write(attr.attName.getBytes(encoding)); } catch(Throwable t) {}
                                try { os.write("=\"".getBytes()); } catch(Throwable t) {}
                                try { os.write(attr.value.getBytes(encoding)); } catch(Throwable t) {}
                                try { os.write("\"".getBytes()); } catch(Throwable t) {}
			}
			// attributes.removeAllElements();
			attributesFlushed = true;
			try {
				if (close) {
					os.write("/>".getBytes());
				} else {
					os.write(">".getBytes());
				}
			} 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;
		try {
			os.write("<?xml version=\"1.0\" encoding=\"".getBytes());
			os.write(encoding.getBytes());
			os.write("\"?".getBytes());
		} 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("<".getBytes());
			os.write(tag.getBytes());
			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.getBytes());
		for (int i = 0; i < indentLevel; i++) {
			os.write(indentStr.getBytes());
		}
	}
}
