/*
 * 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.util.Collection;
import java.util.Iterator;
import java.util.Map;
import javolution.lang.Configurable;
import javolution.lang.Reflection;
import javolution.lang.Reusable;
import javolution.text.CharArray;
import javolution.text.TextBuilder;
import javolution.text.TextFormat;
import javolution.util.FastList;
import javolution.util.FastMap;
import javolution.util.FastSet;
import javolution.util.FastTable;
import javolution.xml.XMLFormat.InputElement;
import javolution.xml.XMLFormat.OutputElement;
import javolution.xml.stream.XMLStreamException;
import javolution.xml.stream.XMLStreamReader;
import javolution.xml.stream.XMLStreamWriter;

/**
 * <p> This class represents the binding between Java classes and 
 *     their XML representation ({@link XMLFormat}).</p>
 *     
 * <p> Custom XML bindings can also be used to alias class names and 
 *     ensure that the XML representation is:<ul>
 *     <li> Impervious to obfuscation.</li>
 *     <li> Unaffected by any class refactoring.</li>
 *     <li> Can be mapped to multiple implementations. For example:[code]
 *     
 *     // Creates a binding to serialize Swing components into high-level XML
 *     // and deserialize the same XML into SWT components.
 *     XMLBinding swingBinding = new XMLBinding();
 *     swingBinding.setAlias(javax.swing.JButton.class, "Button");
 *     swingBinding.setAlias(javax.swing.JTable.class, "Table");
 *     ...
 *     XMLBinding swtBinding = new XMLBinding();
 *     swtBinding.setAlias(org.eclipse.swt.widgets.Button.class, "Button");
 *     swtBinding.setAlias(org.eclipse.swt.widgets.Table.class, "Table");
 *     ...
 *     
 *     // Writes Swing Desktop to XML.
 *     XMLObjectWriter writer = new XMLObjectWriter().setBinding(swingBinding);
 *     writer.setOutput(new FileOutputStream("C:/desktop.xml"));
 *     writer.write(swingDesktop, "Desktop", SwingDesktop.class);
 *     writer.close();
 *
 *     // Reads back high-level XML to a SWT implementation!    
 *     XMLObjectReader reader = new XMLObjectReader().setXMLBinding(swtBinding);
 *     reader.setInput(new FileInputStream("C:/desktop.xml"));
 *     SWTDesktop swtDesktop = reader.read("Desktop", SWTDesktop.class);
 *     reader.close();
 *     [/code]</li>
 *     </ul></p>        
 *     
 * <p> More advanced bindings can also be created through sub-classing.[code]
 * 
 *     // XML binding using reflection.
 *     public ReflectionBinding extends XMLBinding {
 *         protected XMLFormat getFormat(Class forClass) {
 *             Field[] fields = forClass.getDeclaredFields();
 *             return new XMLReflectionFormat(fields);
 *         }
 *     }
 *     
 *     // XML binding read from DTD input source.
 *     public DTDBinding extends XMLBinding {
 *         public DTDBinding(InputStream dtd) {
 *             ...
 *         }
 *     }
 *     
 *     // XML binding overriding default formats.
 *     public MyBinding extends XMLBinding {
 *         // Non-static formats use unmapped XMLFormat instances.
 *         XMLFormat<String> myStringFormat = new XMLFormat<String>(null) {...}
 *         XMLFormat<Collection> myCollectionFormat = new XMLFormat<Collection>(null) {...}
 *         protected XMLFormat getFormat(Class forClass) throws XMLStreamException {
 *             if (String.class.equals(forClass))
 *                  return myStringFormat;
 *             if (Collection.class.isAssignableFrom(forClass))
 *                  return myCollectionFormat;
 *             return super.getFormat(cls);
 *         }
 *     }
 *     [/code]
 *      
 * <p> The default XML binding implementation supports all static XML formats 
 *     (static members of the classes being mapped) as well as the 
 *     following types:<ul>
 *        <li><code>java.lang.Object</code> (empty element)</li>
 *        <li><code>java.lang.Class</code></li>
 *        <li><code>java.lang.String</code></li>
 *        <li><code>java.lang.Appendable</code></li>
 *        <li><code>java.util.Collection</code></li>
 *        <li><code>java.util.Map</code></li>
 *        <li><code>java.lang.Object[]</code></li>
 *        <li> all primitive types wrappers (e.g. 
 *            <code>Boolean, Integer ...</code>)</li>
 *        </ul></p>
 *          
 * @author  <a href="mailto:jean-marie@dautelle.com">Jean-Marie Dautelle</a>
 * @version 5.4, December 1, 2009
 */
public class XMLBinding implements Reusable, XMLSerializable {

    /**
     * Holds the default instance used by readers/writers (thread-safe).
     */
    static final XMLBinding DEFAULT = new XMLBinding();

    /**
     * Holds the class attribute.
     */
    private QName _classAttribute = QName.valueOf("class");

    /**
     * Holds the class to alias (QName) mapping.
     */
    private final FastMap _classToAlias = new FastMap();

    /**
     * Holds the alias (QName) to class mapping.
     */
    private final FastMap _aliasToClass = new FastMap();

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

    /**
     * Sets the qualified alias for the specified class.
     * 
     * @param cls the class being aliased.
     * @param qName the qualified name.
     */
    public void setAlias(Class cls, QName qName) {
        _classToAlias.put(cls, qName);
        _aliasToClass.put(qName, cls);
    }

    /**
     * Convenient method equivalent to {@link #setAlias(Class, QName) 
     * setAlias(cls, QName.valueOf(alias))}.
     * 
     * @param cls the class being aliased.
     * @param alias the alias for the specified class.
     */
    public final void setAlias(Class cls, String alias) {
        setAlias(cls, QName.valueOf(alias));
    }

    /**
     * Sets the qualified name of the attribute holding the 
     * class identifier. If the local name is <code>null</code> the class 
     * attribute is never read/written (which may prevent unmarshalling).
     * 
     * @param classAttribute the qualified name of the class attribute or 
     *        <code>null<code>.
     */
    public void setClassAttribute(QName classAttribute) {
        _classAttribute = classAttribute;
    }

    /**
     * Convenience method equivalent to {@link #setClassAttribute(QName)
     * setClassAttribute(QName.valueOf(name))}.
     * 
     * @param name the name of the class attribute or <code>null<code>.
     */
    public final void setClassAttribute(String name) {
        setClassAttribute(name == null ? null : QName.valueOf(name));
    }

    /**
     * Returns the XML format for the specified class/interface.
     * The default implementation returns the {@link XMLFormat#getInstance}
     * for the specified class.
     * 
     * @param forClass the class for which the XML format is returned.
     * @return the XML format for the specified class (never <code>null</code>).
     * @throws XMLStreamException if there is no format for the
     *         specified class.
     */
    protected XMLFormat getFormat(Class forClass) throws XMLStreamException  {
         return XMLFormat.getInstance(forClass);
    }

    /**
     * Reads the class corresponding to the current XML element.
     * 
     * This method is called by {@link XMLFormat.InputElement#getNext()} 
     * {@link XMLFormat.InputElement#get(String)} and
     * {@link XMLFormat.InputElement#get(String, String)} to retrieve the 
     * Java class corresponding to the current XML element.
     * 
     * If <code>useAttributes</code> is set, the default implementation 
     * reads the class name from the class attribute; otherwise the class 
     * name (or alias) is read from the current element qualified name.
     * 
     * @param reader the XML stream reader.
     * @param useAttributes indicates if the element's attributes should be 
     *        used to identify the class (e.g. when the element name is 
     *        specified by the user then attributes have to be used).
     * @return the corresponding class.
     * @throws XMLStreamException 
     */
    protected Class readClass(XMLStreamReader reader, boolean useAttributes)
            throws XMLStreamException {
        QName classQName;
        if (useAttributes) {
            if (_classAttribute == null)
                throw new XMLStreamException(
                        "Binding has no class attribute defined, cannot retrieve class");
            classQName = QName.valueOf(reader.getAttributeValue(_classAttribute
                    .getNamespaceURI(), _classAttribute.getLocalName()));
            if (classQName == null)
                throw new XMLStreamException(
                        "Cannot retrieve class (class attribute not found)");
        } else {
            classQName = QName.valueOf(reader.getNamespaceURI(), reader
                    .getLocalName());
        }

        // Searches aliases with namespace URI.
        Class cls = (Class) _aliasToClass.get(classQName);
        if (cls != null)
            return cls;

        // Searches aliases without namespace URI.
        cls = (Class) _aliasToClass.get(QName.valueOf(classQName.getLocalName()));
        if (cls != null)
            return cls;

        // Finally convert the qualified name to a class (ignoring namespace URI).
        cls = Reflection.getInstance().getClass(classQName.getLocalName());
        if (cls == null)
            throw new XMLStreamException("Class " + classQName.getLocalName() + 
                    " not found (see javolution.lang.Reflection to support additional class loader)");
        _aliasToClass.put(classQName, cls);
        return cls;
    }

    /**
     * Writes the specified class to the current XML element attributes or to 
     * a new element if the element attributes cannot be used.
     * 
     * This method is called by 
     * {@link XMLFormat.OutputElement#add(Object)} and 
     * {@link XMLFormat.OutputElement#add(Object, String)} and 
     * {@link XMLFormat.OutputElement#add(Object, String, String)} to 
     * identify the Java class corresponding to the XML element.
     * 
     * 
     * @param cls the class to be written.
     * @param writer the XML stream writer.
     * @param useAttributes indicates if the element's attributes should be 
     *        used to identify the class (e.g. when the element name is 
     *        specified by the user then attributes have to be used).
     * @throws XMLStreamException 
     */
    protected void writeClass(Class cls, XMLStreamWriter writer,
            boolean useAttributes) throws XMLStreamException {
        QName qName = (QName) _classToAlias.get(cls);
        String name = qName != null ? qName.toString() : cls.getName();
        if (useAttributes) {
            if (_classAttribute == null)
                return;
            if (_classAttribute.getNamespaceURI() == null) {
                writer.writeAttribute(_classAttribute.getLocalName(),
                        QName.j2meToCharSeq(name));
            } else {
                writer.writeAttribute(_classAttribute.getNamespaceURI(),
                        _classAttribute.getLocalName(), QName.j2meToCharSeq(name));
            }
        } else {
            if (qName != null) {
            	if(qName.getNamespaceURI() == null) {
            		writer.writeStartElement(qName.getLocalName());
            	} else {
            		writer.writeStartElement(qName.getNamespaceURI(), qName.getLocalName());
            	}
            } else {
                writer.writeStartElement(QName.j2meToCharSeq(name));
            }
        }
    }

    // Implements Reusable.
    public void reset() {
        _classAttribute = QName.valueOf("class");
        _aliasToClass.reset();
        _classToAlias.reset();
    }

    //////////////////////////////////////////////////
    // PREDEFINED FORMATS (LOADED ONLY IF REQUIRED) //
    //////////////////////////////////////////////////

    /**
     * Holds the static XML format for <code>java.lang.Object.class</code> instances.
     * The XML representation consists of the text representation of the object
     * as a "value" attribute.
     */
    static final XMLFormat OBJECT_XML = new XMLFormat(Object.class) {
      public boolean isReferenceable() {
            return false; // Always by value (immutable).
        }

        public Object newInstance(Class cls,
                javolution.xml.XMLFormat.InputElement xml)
                throws XMLStreamException {
            TextFormat format = TextFormat.getInstance(cls);
            if (!format.isParsingSupported())
                throw new XMLStreamException("No XMLFormat or TextFormat (with parsing supported) for instances of " + cls);
            CharArray value = xml.getAttribute("value");
            if (value == null) throw new XMLStreamException("Missing value attribute (to be able to parse the instance of " + cls + ")");
            return format.parse(value);
        }

        public void read(InputElement xml, Object obj)
                throws XMLStreamException {
            // Do nothing.
        }

        public void write(Object obj, OutputElement xml)
                throws XMLStreamException {
            TextBuilder tmp = TextBuilder.newInstance();
            try {
                TextFormat.getInstance(obj.getClass()).format(obj, tmp);
                xml.setAttribute("value", tmp);
            } finally {
                TextBuilder.recycle(tmp);
            }
        }
    };


    /**
     * Holds the default XML representation for <code>java.util.Collection</code>
     * instances. This representation consists of nested XML elements one for
     * each element of the collection. The elements' order is defined by
     * the collection iterator order. Collections are deserialized using their
     * default constructor.
     */
    static final XMLFormat COLLECTION_XML = new XMLFormat(
            java.util.Collection.class) {

        public void read(InputElement xml, Object obj)
                throws XMLStreamException {
            Collection collection = (Collection) obj;
            while (xml.hasNext()) {
                collection.add(xml.getNext());
            }
        }

        public void write(Object obj, OutputElement xml)
                throws XMLStreamException {
            Collection collection = (Collection) obj;
            for (Iterator i = collection.iterator(); i.hasNext();) {
                xml.add(i.next());
            }
        }
    };

    /**
     * Holds the default XML representation for <code>java.util.Map</code>
     * instances. This representation consists of key/value pair as nested
     * XML elements. For example:[code]
     * <javolution.util.FastMap>
     *     <Key class="java.lang.String" value="ONE"/>
     *     <Value class="java.lang.Integer" value="1"/>
     *     <Key class="java.lang.String" value="TWO"/>
     *     <Value class="java.lang.Integer" value="2"/>
     *     <Key class="java.lang.String" value="THREE"/>
     *     <Value class="java.lang.Integer" value="3"/>
     * </javolution.util.FastMap>[/code]
     *
     * The elements' order is defined by the map's entries iterator order.
     * Maps are deserialized using their default constructor.
     */
    static final XMLFormat MAP_XML = new XMLFormat(java.util.Map.class) {

        public void read(InputElement xml, Object obj)
                throws XMLStreamException {
            final Map map = (Map) obj;
            while (xml.hasNext()) {
                Object key = xml.get("Key");
                Object value = xml.get("Value");
                map.put(key, value);
            }
        }

        public void write(Object obj, OutputElement xml)
                throws XMLStreamException {
            final Map map = (Map) obj;
            for (Iterator it = map.entrySet().iterator(); it.hasNext();) {
                Map.Entry entry = (Map.Entry) it.next();
                xml.add(entry.getKey(), "Key");
                xml.add(entry.getValue(), "Value");
            }
        }
    };

}