/*

License $Id: SIXBSWriter.java,v 1.2 2001/05/11 16:45:16 Hendrik Exp $

Copyright (c) 2001 tagtraum industries.

The sixbs library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library 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
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

For LGPL see <http://www.fsf.org/copyleft/lesser.txt>

We encourage your feedback and suggestions and want to use your feedback to
improve the Software. Send all such feedback to:
feedback@tagtraum.com

For more information on tagtraum industries, Hendrik Schreiber and sixbs
please see <http://www.tagtraum.com/>.

*/

package com.tagtraum.sixbs;

import org.w3c.dom.*;
import java.util.*;
import java.io.*;
import java.net.URL;
import java.lang.reflect.*;
import com.tagtraum.framework.util.*;



/**
 * Serializes beans to XML.
 * 
 * @author 	Hendrik Schreiber
 * @version $Id: SIXBSWriter.java,v 1.2 2001/05/11 16:45:16 Hendrik Exp $
 */
public class SIXBSWriter {

    /**
     * Source-Version
     */
    public static String	vcid = 
	"$Id: SIXBSWriter.java,v 1.2 2001/05/11 16:45:16 Hendrik Exp $";
    
    private static final Null NULL = Null.UNIQUE_NULL;
    private static final String REFERENCE = Reference.class.getName();
    private static ResourceBundle localStrings = ResourceBundle.getBundle("com.tagtraum.sixbs.localStrings");
    private static final Object[] NOARGS = new Object[] {};
    private Object[] ONEARG = new Object[] {null};
    private Class[] ONECLASS = new Class[] {null};
    
    private Writer out;
    private ObjectIdentifier objectIdentifier;
    private AdapterFinder adapterFinder;
    private String encoding;
    private boolean append;
    
    /**
     * Uses default char encoding. The default AdapterFinder is used.
     *
     * @param out OutputStream to write objects to with the specified encoding.
     * @param append if true, no XML declaration (&lt;?xml version="1.0"?&gt;) and the sixbs tag aren't written .
     */
    public SIXBSWriter(OutputStream out, boolean append) throws IOException {
        this(new OutputStreamWriter(out), AdapterFinder.getDefaultAdapterFinder(), append);
    }
    
    /**
     * For performance reasons the OutputStream should be buffered. sixbs does <i>not</i>
     * buffer. The default AdapterFinder is used.
     *
     * @param out OutputStream to write objects to with the specified encoding.
     * @param append if true, no XML declaration (&lt;?xml version="1.0"?&gt;) and the sixbs tag aren't written .
     * @param encoding char set to be used
     */
    public SIXBSWriter(OutputStream out, boolean append, String encoding) throws IOException {
        this(new OutputStreamWriter(out, encoding), AdapterFinder.getDefaultAdapterFinder(), append);
    }
    
    /**
     * For performance reasons the Writer should be buffered. sixbs does <i>not</i>
     * buffer. sixbs will write with the systems default encoding or the encoding
     * set in the provided buffer. No XML declaration is written. The default AdapterFinder
     * is used. Append is set to false.
     *
     * @param out Writer to write objects to.
     */
    public SIXBSWriter(Writer out) throws IOException {
        this(out, AdapterFinder.getDefaultAdapterFinder(), false, null);
    }

    /**
     * For performance reasons the Writer should be buffered. sixbs does <i>not</i>
     * buffer. sixbs will write with the systems default encoding or the encoding
     * set in the provided buffer. 
     *
     * @param append if true, no XML declaration (&lt;?xml version="1.0"?&gt;) and the sixbs tag aren't written .
     * @param out Writer to write objects to.
     */
    public SIXBSWriter(Writer out, boolean append) throws IOException {
        this(out, AdapterFinder.getDefaultAdapterFinder(), append, null);
    }

    /**
     * For performance reasons the Writer should be buffered. sixbs does <i>not</i>
     * buffer. sixbs will write with the systems default encoding or the encoding
     * set in the provided buffer.
     *
     * @param out Writer to write objects to.
     * @param append if true, no XML declaration (&lt;?xml version="1.0"?&gt;) and the sixbs tag aren't written .
     */
    public SIXBSWriter(Writer out, AdapterFinder adapterFinder, boolean append) throws IOException {
        this(out, adapterFinder, append, null);
    }

    /**
     * For performance reasons the Writer should be buffered. sixbs does <i>not</i>
     * buffer. sixbs will write with the systems default encoding or the encoding
     * set in the provided buffer.
     *
     * @param out Writer to write objects to with the specified encoding.
     * @param append if true, no XML declaration (&lt;?xml version="1.0"?&gt;) and the sixbs tag aren't written .
     * @param adapterFinder the adapterFinder to use
     */
    public SIXBSWriter(OutputStreamWriter out, AdapterFinder adapterFinder, boolean append) throws IOException {
        this(out, adapterFinder, append, out.getEncoding());
    }
    
    /**
     * For performance reasons the Writer should be buffered. sixbs does <i>not</i>
     * buffer. sixbs will write with the systems default encoding or the encoding
     * set in the provided buffer.
     *
     * @param out Writer to write objects to with the specified encoding.
     * @param append if true, no XML declaration (&lt;?xml version="1.0"?&gt;)is written.
     * @param adapterFinder the adapterFinder to use
     * @param encoding encoding that is used by the writer - set null, if unknwon
     */
    public SIXBSWriter(Writer out, AdapterFinder adapterFinder, boolean append, String encoding) throws IOException {
        this.out = out;
        objectIdentifier = new ObjectIdentifier();
        this.adapterFinder = adapterFinder;
        this.encoding = encoding;
        this.append = append;
        if (!append) {
            writeHeader();
        }
    }
    
    /**
     * Writes the xml declaration, and an opening &lt;sixbs&gt; tag. Is called, if
     * append to false.
     */
    protected void writeHeader() throws IOException {
        if (encoding == null) {
            out.write("<?xml version=\"1.0\"?>");
        }
        else {
            out.write("<?xml version=\"1.0\" encoding=\"");
            out.write(encoding);    
            out.write("\" ?>");
        }
        out.write("<sixbs version=\"1.0\">");
    }
    
    /**
     * Writes a closing &lt;/sixbs&gt; tag. Is called, if
     * append to false.
     */
    protected void writeFooter() throws IOException {
        out.write("</sixbs>");
    }
    
    public synchronized void writeObject(Object obj) throws IOException {
        if (obj == null) obj = NULL;
        String id = null;
        if ((id = objectIdentifier.getId(obj)) != null) {
            writeReference(id);
        }
        else if (obj.getClass().isArray()) {
            writeArray(obj);
        }
        else {
            Class adapterClass = null;
            if (obj instanceof Adapter || (adapterClass = adapterFinder.find(obj)) == null) {
                writePlainObject(obj);
            }
            else {
                writeAdapter(obj, adapterClass);
            }
        }
    }

    private void writePlainObject(Object obj) throws IOException {
        out.write("<");
        writeClass(obj);
        out.write(" id=\"");
        out.write(objectIdentifier.createId(obj));
        out.write("\">");
        Class klass = obj.getClass();
        if ((klass != Object.class)
            && (klass == String.class
            || klass.getSuperclass() == Number.class
            || klass == Character.class
            || klass == Boolean.class
            || klass == URL.class)) {
                out.write(encode(obj.toString()));
        } else {
            writeProperties(obj);
        }
        out.write("</");
        writeClass(obj);
        out.write(">");
    }

    private void writeReference(String id) throws IOException {
        out.write("<");
        out.write(REFERENCE);
        out.write(" id=\"");
        out.write(id);
        out.write("\"/>");
    }
    
    private void writeAdapter(Object obj, Class adapterClass) throws IOException {
        Object adapter = null;
        try {
            ONECLASS[0] = obj.getClass();
            Constructor constr = adapterClass.getConstructor(ONECLASS);
            ONEARG[0]=obj;
            adapter = constr.newInstance(ONEARG);
        }
        catch (Throwable t) {
            throw new SIXBSException(localStrings.getString("adapter_instantiation_failure") + " " + adapterClass.getName(), t);
        }
        writeObject(adapter);
    }
        
    private void writeArray(Object obj) throws IOException {
        out.write("<");
        out.write(TypedArray.class.getName());
        out.write(" type=\"");
        //if (obj.getClass().getComponentType().isArray()) {
        out.write(obj.getClass().getComponentType().getName());
        out.write("\" array=\"");
        out.write(obj.getClass().getComponentType().isArray()?"true":"false");
        out.write("\">");
        if (obj.getClass().getComponentType().isPrimitive()) {
            for (int j=0; j<Array.getLength(obj); j++) {
                writeObject(Array.get(obj, j));
            }
        }
        else {
            Object[] array = (Object[])obj;
            for (int j=0; j<array.length; j++) {
                writeObject(array[j]);
            }
        }
        out.write("</");
        out.write(TypedArray.class.getName());
        out.write(">");
    }

    private void writeClass(Object obj) throws IOException {
        out.write(obj.getClass().getName());
    }
    
    private void writeProperties(Object obj) throws IOException {
        // Class Object has no properties.
        if (obj.getClass() == Object.class) return;
        try {
            HashMap readMethodMap = ReflectHelper.getReadMethods(obj.getClass());
            HashMap writeMethodMap = ReflectHelper.getWriteMethods(obj.getClass());
            Iterator readMethods = readMethodMap.keySet().iterator();
            while (readMethods.hasNext()) {
                String name = (String)readMethods.next();
                // write only if when can set the value, too (rik)
                if (writeMethodMap.containsKey(name) && writeProperty(obj, name)) {
                    Method readMethod = (Method)readMethodMap.get(name);
                    Object property = readMethod.invoke(obj, NOARGS);
                    if (property == null) property = NULL;
                    out.write("<");
                    out.write(name);
                    out.write(">");
                    writeObject(property);
                    out.write("</");
                    out.write(name);
                    out.write(">");
                }
            }
        }
        catch (IOException ioe) {
            throw ioe;
        }
        catch (IllegalAccessException iae) {
            throw new SIXBSException(localStrings.getString("property_read_invocation_failed"), iae);
        }
        catch (InvocationTargetException ite) {
            throw new SIXBSException(localStrings.getString("property_read_invocation_failed"), ite.getTargetException());
        }
    }
    
    private boolean writeProperty(Object obj, String propertyName) {
        if (obj instanceof SIXBSSerializable) {
            return ((SIXBSSerializable)obj).serializable(propertyName);
        }
        return true;
    }

    /**
     * Flush the stream.  If the stream has saved any characters from the
     * various write() methods in a buffer, write them immediately to their
     * intended destination.  Then, if that destination is another character or
     * byte stream, flush it.  Thus one flush() invocation will flush all the
     * buffers in a chain of Writers and OutputStreams.
     *
     * @exception  IOException  If an I/O error occurs
     */
    public void flush() throws IOException {
        out.flush();
    }

    /**
     * Close the stream, flushing it first.  Once a stream has been closed,
     * further write() or flush() invocations will cause an IOException to be
     * thrown.
     * If this writer does not operate in append-mode, a closing &lt;/sixbs&gt;
     * tag will be written.
     *
     * @exception  IOException  If an I/O error occurs
     * @see #writeHeader()
     * @see #writeFooter()
     */
    public void close() throws IOException {
        if (!append) {
            writeFooter();
        }
        objectIdentifier = null;
        out.close();
    }

    /**
     * Encodes the string value of an element (text node) to an
     * XML conform format, special characters such the ampersand (&)
     * are replaced by an entities (&amp;).
     *
     * @param elementValue the string value to be encoded
     * @return the XML compliant string value.
     */
    public static String encode(String elementValue) {
        StringBuffer str = new StringBuffer();
        int len = (elementValue != null ? elementValue.length() : 0);
        char[] cbuf = elementValue.toCharArray();
    
        for (int i = 0; i < len; i++) {
            switch (cbuf[i]) {
    
            case '<': {
            str.append("&lt;");
    
            break;
            }
    
            case '>': {
            str.append("&gt;");
    
            break;
            }
    
            case '&': {
            str.append("&amp;");
    
            break;
            }
    
            case '\'': {
            str.append("&apos;");
    
            break;
            }
    
            case '"': {
            str.append("&quot;");
    
            break;
            }
    
            default: {
            if ((int)cbuf[i] < 32 || (int)cbuf[i] > 127) {
                str.append("&#");
                str.append((int)cbuf[i]);
                str.append(';');
            }
            else {
                str.append(cbuf[i]);
            }
            }
            }
        }
    return str.toString();
    }


}