/*
 * Javolution - Java(TM) Solution for Real-Time and Embedded Systems
 * Copyright (C) 2006 - Javolution (http://javolution.org/)
 * All rights reserved.
 * 
 * Permission to use, copy, modify, and distribute this software is
 * freely granted, provided that this notice is preserved.
 */
package javolution.xml;


import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;

import java.lang.IllegalStateException;
import javolution.context.ObjectFactory;
import javolution.lang.Reusable;
import javolution.xml.stream.XMLStreamException;
import javolution.xml.stream.XMLStreamWriter;
import javolution.xml.stream.XMLStreamWriterImpl;


/**
 * <p> This class takes an object and formats it to XML; the resulting 
 *     XML can be deserialized using a {@link XMLObjectReader}.</p>
 *     
 * <p> When an object is formatted, the {@link XMLFormat} of the 
 *     object's class as identified by the {@link XMLBinding} is used to
 *     write its XML representation.</p>
 *     
 * <p> Multiple objects can be written to the same XML output.
 *     For example:[code]
 *     XMLObjectWriter writer = XMLObjectWriter.newInstance(outputStream);
 *     while (true)) {
 *         Message message = ...
 *         writer.write(message, "Message", Message.class);
 *     }
 *     writer.close(); // Writer is recycled, the underlying stream is closed.
 *     [/code]</p>
 *     
 * @author  <a href="mailto:jean-marie@dautelle.com">Jean-Marie Dautelle</a>
 * @version 4.0, September 4, 2006
 */
public class XMLObjectWriter implements Reusable {

    /**
     * Holds the associated factory.
     */
    private static final ObjectFactory FACTORY = new ObjectFactory() {

        protected Object create() {
            return new XMLObjectWriter();
        } 
    };

    /**
     * Hold the xml element used when formatting.
     */
    private final XMLFormat.OutputElement _xml
        = new XMLFormat.OutputElement();

    /**
     * Holds writer if any.
     */
    private Writer _writer;

    /**
     * Holds input stream if any.
     */
    private OutputStream _outputStream;

    /**
     * Indicates if factory produced.
     */
    private boolean _isFactoryProduced;

    /**
     * Default constructor.
     */
    public XMLObjectWriter() {
    }

    /**
     * Returns a XML object writer (potentially recycled) having the specified
     * output stream as output.
     * 
     * @param out the output stream.
     */
    public static XMLObjectWriter newInstance(OutputStream out) throws XMLStreamException {
        XMLObjectWriter writer = (XMLObjectWriter) FACTORY.object();
        writer._isFactoryProduced = true;
        writer.setOutput(out);
        return writer;
    }

    /**
     * Returns a XML object writer (potentially recycled) having the specified
     * output stream/encoding as output.
     * 
     * @param out the output stream.
     * @param encoding the output stream encoding.
     */
    public static XMLObjectWriter newInstance(OutputStream out, String encoding) throws XMLStreamException {
        XMLObjectWriter writer = (XMLObjectWriter) FACTORY.object();
        writer._isFactoryProduced = true;
        writer.setOutput(out, encoding);
        return writer;
    }

    /**
     * Returns a XML object writer (potentially recycled) having the specified
     * writer as output.
     * 
     * @param out the writer output.
     */
    public static XMLObjectWriter newInstance(Writer out) throws XMLStreamException {
        XMLObjectWriter writer = (XMLObjectWriter) FACTORY.object();
        writer._isFactoryProduced = true;
        writer.setOutput(out);
        return writer;
    }

    /**
     * Returns the stream writer used by this object writer (it can be used 
     * to write prolog, write namespaces, etc). The stream writer is setup to 
     * automatically repair namespaces and to automatically output empty 
     * elements when a start element is immediately followed by matching end
     * element. 
     * 
     * @return the stream writer.
     */
    public XMLStreamWriter getStreamWriter() {
        return _xml._writer;
    }

    /**
     * Sets the output stream for this XML object writer.
     * 
     * @param  out the output stream destination.
     * @return <code>this</code>
     * @see    XMLStreamWriterImpl#setOutput(OutputStream)
     */
    public XMLObjectWriter setOutput(OutputStream out) throws XMLStreamException {
        if ((_outputStream != null) || (_writer != null))
            throw new IllegalStateException("Writer not closed or reset");
        _xml._writer.setOutput(out);
        _outputStream = out;
        _xml._writer.writeStartDocument();
        return this;
    }
    
    /**
     * Sets the output stream and encoding for this XML object writer.
     * 
     * @param  out the output stream destination.
     * @param  encoding the stream encoding.
     * @return <code>this</code>
     * @see    XMLStreamWriterImpl#setOutput(OutputStream, String)
     */
    public XMLObjectWriter setOutput(OutputStream out, String encoding) throws XMLStreamException {
        if ((_outputStream != null) || (_writer != null))
            throw new IllegalStateException("Writer not closed or reset");
        _xml._writer.setOutput(out, encoding);
        _outputStream = out;
        _xml._writer.writeStartDocument();
        return this;
    }
    
    /**
     * Sets the output writer for this XML object writer.
     * 
     * @param  out the writer destination.
     * @return <code>this</code>
     * @see    XMLStreamWriterImpl#setOutput(Writer)
     */
    public XMLObjectWriter setOutput(Writer out) throws XMLStreamException {
        if ((_outputStream != null) || (_writer != null))
            throw new IllegalStateException("Writer not closed or reset");
        _xml._writer.setOutput(out);
        _writer = out;
        _xml._writer.writeStartDocument();
        return this;
    }
    
    /**
     * Sets the XML binding to use with this object writer.
     * 
     * @param binding the XML binding to use.
     * @return <code>this</code>
     */
    public XMLObjectWriter setBinding(XMLBinding binding) {
        _xml.setBinding(binding);
        return this;
    }

    /**
     * Sets the indentation to be used by this writer (no indentation 
     * by default).
     * 
     * @param indentation the indentation string.
     * @return <code>this</code>
     */
    public XMLObjectWriter setIndentation(String indentation) {
        _xml._writer.setIndentation(indentation);
        return this;
    }

    /**
     * Sets the XML reference resolver to use with this object writer 
     * (the same reference resolver can be used accross multiple writers).
     * 
     * @param referenceResolver the XML reference resolver.
     * @return <code>this</code>
     */
    public XMLObjectWriter setReferenceResolver(XMLReferenceResolver referenceResolver) {
        _xml.setReferenceResolver(referenceResolver);
        return this;
    }

    /**
     * Writes the specified object as an anonymous nested element of 
     * unknown type. This result in the actual type of the object being
     * identified by the element name.
     *
     * @param obj the object written as nested element or <code>null</code>.
     * @see   XMLFormat.OutputElement#add(Object)
     */
    public void write(Object obj) throws XMLStreamException {
        _xml.add(obj);
    }

    /**
     * Writes the specified object as a named nested element of unknown type
     * (<code>null</code> objects are ignored). The nested XML element
     * may contain a class attribute identifying the object type.
     *
     * @param obj the object added as nested element or <code>null</code>.
     * @param name the name of the nested element.
     * @see   XMLFormat.OutputElement#add(Object, String)
     */
    public void write(Object obj, String name) throws XMLStreamException {
        _xml.add(obj, name);
    }

    /**
     * Writes the specified object as a fully qualified nested element of 
     * unknown type (<code>null</code> objects are ignored). 
     * The nested XML element may contain a class attribute identifying
     * the object type.
     *
     * @param obj the object added as nested element or <code>null</code>.
     * @param localName the local name of the nested element.
     * @param uri the namespace URI of the nested element.
     * @see   XMLFormat.OutputElement#add(Object, String, String)
     */
    public void write(Object obj, String localName, String uri)
            throws XMLStreamException {
        _xml.add(obj, localName, uri);
    }

    /**
     * Writes the specified object as a named nested element of actual type
     * known (<code>null</code> objects are ignored). 
     *
     * @param obj the object added as nested element or <code>null</code>.
     * @param name the name of the nested element.
     * @param cls the non-abstract class identifying the XML format to use.
     * @see   XMLFormat.OutputElement#add(Object, String, Class)
     */
    public  <T>  void write( T  obj, String name, Class <T>  cls)
            throws XMLStreamException {
        _xml.add(obj, name, cls);
    }

    /**
     * Writes the specified object as a fully qualified nested element of
     *  actual type known (<code>null</code> objects are ignored). 
     *
     * @param obj the object added as nested element or <code>null</code>.
     * @param localName the local name of the nested element.
     * @param uri the namespace URI of the nested element.
     * @param cls the class identifying the XML format to use.
     * @see   XMLFormat.OutputElement#add(Object, String, String, Class)
     */
    public  <T>  void write( T  obj, String localName, String uri, Class <T>  cls)
            throws XMLStreamException {
        _xml.add(obj, localName, uri, cls);
    }
    
    /**
     * Flushes the output stream of this writer (automatically done 
     * when {@link #close() closing}).
     */
    public void flush() throws XMLStreamException {
        _xml._writer.flush();
    }
    
    /**
     * Ends document writting, closes this writer and its underlying 
     * output then {@link #reset reset} this Writer for potential reuse.
     */
    public void close() throws XMLStreamException {
        try {
            if (_outputStream != null) {
                _xml._writer.writeEndDocument();
                _xml._writer.close();
                _outputStream.close();
                reset();
            } else if (_writer != null) {
                _xml._writer.writeEndDocument();
                _xml._writer.close();
                _writer.close();
                reset();
            }
            if (_isFactoryProduced) {
                FACTORY.recycle(this);
            }
            
        } catch (IOException e) {
            throw new XMLStreamException(e);
        }
    }

    /**
     * Resets this object writer for reuse.
     */
    public void reset() {
        _xml.reset();
        _outputStream = null;
        _writer = null;
    }
}