/*
 * $Id$
 *
 * Copyright 1996-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.javatest.util;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Writer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * A class to facilitate writing XML via a stream.
 */
public class XMLWriter
{
    /**
     * Create an XMLWriter object, using a default header.
     * @param out a Writer to which to write the generated XML
     * @throws IOException if there is a problem writing to the underlying stream
     */
    public XMLWriter(Writer out) throws IOException {
        this(out, "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
    }

    /**
     * Create an XMLWriter object, using a specifed doctype header.
     * @param out a Writer to which to write the generated XML
     * @param docType a string containing a doctype header for the XML to be generetaed
     * @throws IOException if there is a problem writing to the underlying stream
     */
    public XMLWriter(Writer out, String docType) throws IOException {
        if (out instanceof BufferedWriter)
            this.out = (BufferedWriter) out;
        else
            this.out = new BufferedWriter(out);
        this.out.write(docType);
        this.out.newLine();
    }

    /**
     * Create an XMLWriter object, using a specified bundle for l0calizing messages.
     * @param out a Writer to which to write the generated XML
     * @param i18n a resource bundle to use to localize messages
     * @throws IOException if there is a problem writing to the underlying stream
     */
    public XMLWriter(Writer out, I18NResourceBundle i18n) throws IOException {
        this(out);
        this.i18n = i18n;
    }


    /**
     * Create an XMLWriter object, using a specifed doctype header and
     * using a specified bundle for l0calizing messages.
     * @param out a Writer to which to write the generated XML
     * @param docType a string containing a doctype header for the XML to be generetaed
     * @param i18n a resource bundle to use to localize messages
     * @throws IOException if there is a problem writing to the underlying stream
     */
    public XMLWriter(Writer out, String docType, I18NResourceBundle i18n) throws IOException {
        this(out, docType);
        this.i18n = i18n;
    }

    /**
     * Set the reource bundle to be used for localizing messages.
     * @param i18n the resource bundle to be used for localizing messages
     */
    public void setI18NResourceBundle(I18NResourceBundle i18n) {
        this.i18n = i18n;
    }

    /**
     * Flush the stream, and the underlying output stream.
     * @throws IOException if there is a problem writing to the underlying stream
     */
    public void flush() throws IOException {
        out.flush();
    }

    /**
     * Close the stream, and the underlying output stream.
     * @throws IOException if there is a problem closing the underlying stream
     */
    public void close() throws IOException {
        out.close();
    }

    /**
     * Write a newline to the underlying output stream.
     * @throws IOException if there is a problem closing the underlying stream
     */
    public void newLine() throws IOException {
        if (state == IN_TAG) {
            out.write(">");
            state = IN_BODY;
        }
        out.newLine();
    }

    /**
     * Start an XML tag.  If a prior tag has been started, it will
     * be closed first. Once a tag has been opened, attributes for the
     * tag may be written out, followed by body content before finally
     * ending the tag.
     * @param tag the tag to be started
     * @throws IOException if there is a problem closing the underlying stream
     * @see #writeAttr
     * @see #write
     * @see #endTag
     */
    public void startTag(String tag) throws IOException {
        if (state == IN_TAG) {
            out.write(">");
            state = IN_BODY;
        }
        //newLine();
        out.write("<");
        out.write(tag);
        state = IN_TAG;
    }

    /**
     * Finish an XML tag. It is expected that a call to endTag will match
     * a corresponding earlier call to startTag, but there is no formal check
     * for this.
     * @param tag the tag to be closed.
     * @throws IOException if there is a problem closing the underlying stream
     */
    public void endTag(String tag) throws IOException {
        if (state == IN_TAG) {
            out.write("/>");
            state = IN_BODY;
        }
        else {
            out.write("</");
            out.write(tag);
            out.write(">");
        }
        //out.newLine();
    }

    /**
     * Write an attribute for a tag. A tag must previously have been started.
     * All tag attributes must be written before any body text is written.
     * The value will be quoted if necessary when writing it to the underlying
     * stream. No check is made that the attribute is valid for the current tag.
     * @param name the name of the attribute to be written
     * @param value the value of the attribute to be written
     * @throws IllegalStateException if the stream is not in a state to
     * write attributes -- e.g. if this call does not follow startTag or other
     * calls of writteAttr
     * @throws IOException if there is a problem closing the underlying stream
     */
    public void writeAttr(String name, String value) throws IOException {
        if (state != IN_TAG)
            throw new IllegalStateException();

        out.write(" ");
        out.write(name);
        out.write("=");
        out.write("\"");
        out.write(value);  // should check for " ?
        out.write("\"");
    }

    /**
     * Write a line of text, followed by a newline.
     * The text will be escaped as necessary.
     * @param text the text to be written.
     * @throws IOException if there is a problem closing the underlying stream
     */
    public void writeLine(String text) throws IOException {
        write(text);
        out.newLine();
    }

    /**
     * Write a formatted date.
     * @param millis date represented in milliseconds
     * @throws IOException if a exception occurs during writing
     * @see java.util.Date
     */
    public void writeDate(long millis) throws IOException {
        writeDate(new Date(millis));
    }

    /**
     * Write a formatted date surrounded by a markup tag.
     * @param tag tag to open and close before and after the date is inserted
     * @param millis date represented in milliseconds
     * @throws IOException if a exception occurs during writing
     * @see java.util.Date
     */
    public void writeDate(String tag, long millis) throws IOException {
        writeDate(tag, new Date(millis));
    }

    /**
     * Write a formatted date.
     * @param date the date to print
     * @throws IOException if a exception occurs during writing
     */
    public void writeDate(Date date) throws IOException {
        if (dateFormatter == null)
            dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
        write(dateFormatter.format(date));
    }

    /**
     * Write a formatted date surrounded by a markup tag.
     * @param tag tag to open and close before and after the date is inserted
     * @param date the date to print
     * @throws IOException if a exception occurs during writing
     * @see java.util.Date
     */
    public void writeDate(String tag, Date date) throws IOException {
        startTag(tag);
        writeDate(date);
        endTag(tag);
    }

    /**
     * Write body text, escaping it as necessary.
     * If this call follows a call of startTag, the open tag will be
     * closed -- meaning that no more attributes can be written until another
     * tag is started.
     * @param text the text to be written
     * @throws IOException if there is a problem closing the underlying stream
     */
    public void write(String text) throws IOException {
        if (text.length() == 0)
            return;

        if (state == IN_TAG) {
            out.write(">");
            state = IN_BODY;
        }

        // check to see if there are any special characters
        boolean specialChars = false;
        for (int i = 0; i < text.length() && !specialChars; i++) {
            switch (text.charAt(i)) {
            case '<': case '>': case '&':
                specialChars = true;
            }
        }

        // if there are special characters write the string character at a time;
        // otherwise, write it out as is
        if (specialChars) {
            for (int i = 0; i < text.length(); i++) {
                char c = text.charAt(i);
                switch (c) {
                case '<': out.write("&lt;"); break;
                case '>': out.write("&gt;"); break;
                case '&': out.write("&amp;"); break;
                default: out.write(c);
                }
            }
        }
        else
            out.write(text);
    }

    /**
     * Write a localized message, using a specified resource bundle.
     * @param i18n the resource bundle used to localize the message
     * @param key the key for the message to be localized
     * @throws IOException if there is a problem closing the underlying stream
     */
    public void write(I18NResourceBundle i18n, String key) throws IOException {
        write(i18n.getString(key));
    }

    /**
     * Write a localized message, using a specified resource bundle.
     * @param i18n the resource bundle used to localize the message
     * @param key the key for the message to be localized
     * @param arg an argument to be formatted into the localized message
     * @throws IOException if there is a problem closing the underlying stream
     */
    public void write(I18NResourceBundle i18n, String key, Object arg) throws IOException {
        write(i18n.getString(key, arg));
    }

    /**
     * Write a localized message, using a specified resource bundle.
     * @param i18n the resource bundle used to localize the message
     * @param key the key for the message to be localized
     * @param args arguments to be formatted into the localized message
     * @throws IOException if there is a problem closing the underlying stream
     */
    public void write(I18NResourceBundle i18n, String key, Object[] args) throws IOException {
        write(i18n.getString(key, args));
    }

    /**
     * Write a localized message, using the default resource bundle.
     * @param key the key for the message to be localized
     * @throws IOException if there is a problem closing the underlying stream
     */
    public void writeI18N(String key) throws IOException {
        write(i18n.getString(key));
    }

    /**
     * Write a localized message, using the default resource bundle.
     * @param key the key for the message to be localized
     * @param arg an argument to be formatted into the localized message
     * @throws IOException if there is a problem closing the underlying stream
     */
    public void writeI18N(String key, Object arg) throws IOException {
        write(i18n.getString(key, arg));
    }

    /**
     * Write a localized message, using the default resource bundle.
     * @param key the key for the message to be localized
     * @param args arguments to be formatted into the localized message
     * @throws IOException if there is a problem closing the underlying stream
     */
    public void writeI18N(String key, Object[] args) throws IOException {
        write(i18n.getString(key, args));
    }

    private BufferedWriter out;
    private int state;
    private I18NResourceBundle i18n;
    private DateFormat dateFormatter;
    private static final int IN_TAG = 1;
    private static final int IN_BODY = 2;
}
