/* * 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 j2me.util.Collection; import j2me.util.Iterator; import j2me.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( j2me.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(j2me.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"); } } }; }