/* * (C) Copyright 2006-2007 Nuxeo SAS (http://nuxeo.com/) and contributors. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser General Public License * (LGPL) version 2.1 which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl.html * * 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. * * Contributors: * Nuxeo - initial API and implementation * * $Id$ */ package org.nuxeo.common.xmap; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Hashtable; import java.util.List; import java.util.Map; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.nuxeo.common.utils.FileUtils; import org.nuxeo.common.xmap.annotation.XContent; import org.nuxeo.common.xmap.annotation.XContext; import org.nuxeo.common.xmap.annotation.XMemberAnnotation; import org.nuxeo.common.xmap.annotation.XNode; import org.nuxeo.common.xmap.annotation.XNodeList; import org.nuxeo.common.xmap.annotation.XNodeMap; import org.nuxeo.common.xmap.annotation.XObject; import org.nuxeo.common.xmap.annotation.XParent; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; /** * XMap maps an XML file to a java object. * <p> * The mapping is described by annotations on java objects. * <p> * The following annotations are supported: * <ul> * <li> {@link XObject} * Mark the object as being mappable to an XML node * <li> {@link XNode} * Map an XML node to a field of a mappable object * <li> {@link XNodeList} * Map an list of XML nodes to a field of a mappable object * <li> {@link XNodeMap} * Map an map of XML nodes to a field of a mappable object * <li> {@link XContent} * Map an XML node content to a field of a mappable object * <li> {@link XParent} * Map a field of the current mappable object to the parent object if any exists * The parent object is the mappable object containing the current object as a field * </ul> * * The mapping is done in 2 steps: * <ul> * <li> The XML file is loaded as a DOM document * <li> The DOM document is parsed and the nodes mapping is resolved * </ul> * * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> */ @SuppressWarnings({"SuppressionAnnotation"}) public class XMap { // top level objects private final Map<String, XAnnotatedObject> roots; // the scanned objects private final Map<Class, XAnnotatedObject> objects; private final Map<Class, XValueFactory> factories; /** * Creates a new XMap object. */ public XMap() { objects = new Hashtable<Class, XAnnotatedObject>(); roots = new Hashtable<String, XAnnotatedObject>(); factories = new Hashtable<Class, XValueFactory>(XValueFactory.defaultFactories); } /** * Gets the value factory used for objects of the given class. * <p> * Value factories are used to decode values from XML strings. * * @param type the object type * @return the value factory if any, null otherwise */ public XValueFactory getValueFactory(Class type) { return factories.get(type); } /** * Sets a custom value factory for the given class. * <p> * Value factories are used to decode values from XML strings. * * @param type the object type * @param factory the value factory to use for the given type */ public void setValueFactory(Class type, XValueFactory factory) { factories.put(type, factory); } /** * Gets a list of scanned objects. * <p> * Scanned objects are annotated objects that were registered * by this XMap instance. */ public Collection<XAnnotatedObject> getScannedObjects() { return objects.values(); } /** * Gets the root objects. * <p> * Root objects are scanned objects that can be mapped to XML elements * that are not part from other objects. * * @return the root objects */ public Collection<XAnnotatedObject> getRootObjects() { return roots.values(); } /** * Registers a mappable object class. * <p> * The class will be scanned for XMap annotations * and a mapping description is created. * * @param klass the object class * @return the mapping description */ public XAnnotatedObject register(Class klass) { XAnnotatedObject xao = objects.get(klass); if (xao == null) { // avoid scanning twice XObject xob = checkObjectAnnotation(klass); if (xob != null) { xao = new XAnnotatedObject(this, klass, xob); objects.put(xao.klass, xao); scan(xao); String key = xob.value(); if (key.length() > 0) { roots.put(xao.path.path, xao); } } } return xao; } private void scan(XAnnotatedObject xob) { Field[] fields = xob.klass.getDeclaredFields(); for (Field field : fields) { Annotation anno = checkMemberAnnotation(field); if (anno != null) { XAnnotatedMember member = createFieldMember(field, anno); xob.addMember(member); } } Method[] methods = xob.klass.getDeclaredMethods(); for (Method method : methods) { // we accept only methods with one parameter Class[] paramTypes = method.getParameterTypes(); if (paramTypes.length != 1) { continue; } Annotation anno = checkMemberAnnotation(method); if (anno != null) { XAnnotatedMember member = createMethodMember(method, anno, xob.klass); xob.addMember(member); } } } /** * Processes the XML file at the given URL using a default context. * * @param url the XML file url * @return the first registered top level object that is found in the file, * or null if no objects are found. */ public Object load(URL url) throws Exception { return load(new Context(), url.openStream()); } /** * Processes the XML file at the given URL and using the given contexts. * * @param ctx the context to use * @param url the XML file url * @return the first registered top level object that is found in the file. */ public Object load(Context ctx, URL url) throws Exception { return load(ctx, url.openStream()); } /** * Processes the XML content from the given input stream using a default context. * * @param in the XML input source * @return the first registered top level object that is found in the file. */ public Object load(InputStream in) throws Exception { return load(new Context(), in); } /** * Processes the XML content from the given input stream using the given context. * * @param ctx the context to use * @param in the input stream * @return the first registered top level object that is found in the file. */ public Object load(Context ctx, InputStream in) throws Exception { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(in); return load(ctx, document.getDocumentElement()); } finally { if (in != null) { try { in.close(); } catch (IOException e) { // do nothing } } } } /** * Processes the XML file at the given URL using a default context. * <p> * Returns a list with all registered top level objects that are found in the file. * <p> * If not objects are found, an empty list is returned. * * @param url the XML file url * @return a list with all registered top level objects that are found in the file */ public Object[] loadAll(URL url) throws Exception { return loadAll(new Context(), url.openStream()); } /** * Processes the XML file at the given URL using the given context * <p> * Return a list with all registered top level objects that are found in the file. * <p> * If not objects are found an empty list is retoruned. * * @param ctx the context to use * @param url the XML file url * @return a list with all registered top level objects that are found in the file */ public Object[] loadAll(Context ctx, URL url) throws Exception { return loadAll(ctx, url.openStream()); } /** * Processes the XML from the given input stream using the given context. * <p> * Returns a list with all registered top level objects that are found in the file. * <p> * If not objects are found, an empty list is returned. * * @param in the XML input stream * @return a list with all registered top level objects that are found in the file */ public Object[] loadAll(InputStream in) throws Exception { return loadAll(new Context(), in); } /** * Processes the XML from the given input stream using the given context. * <p> * Returns a list with all registered top level objects that are found in the file. * <p> * If not objects are found, an empty list is returned. * * @param ctx the context to use * @param in the XML input stream * @return a list with all registered top level objects that are found in the file */ public Object[] loadAll(Context ctx, InputStream in) throws Exception { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(in); return loadAll(ctx, document.getDocumentElement()); } finally { if (in != null) { try { in.close(); } catch (IOException e) { // do nothing } } } } /** * Processes the given DOM element and return the first mappable object * found in the element. * <p> * A default context is used. * * @param root the element to process * @return the first object found in this element or null if none */ public Object load(Element root) throws Exception { return load(new Context(), root); } /** * Processes the given DOM element and return the first mappable object * found in the element. * <p> * The given context is used. * * @param ctx the context to use * @param root the element to process * @return the first object found in this element or null if none */ public Object load(Context ctx, Element root) throws Exception { // check if the current element is bound to an annotated object String name = root.getNodeName(); XAnnotatedObject xob = roots.get(name); if (xob != null) { return xob.newInstance(ctx, root); } else { Node p = root.getFirstChild(); while (p != null) { if (p.getNodeType() == Node.ELEMENT_NODE) { // Recurse in the first child Element return load((Element) p); } p = p.getNextSibling(); } // We didn't find any Element return null; } } /** * Processes the given DOM element and return a list with all top-level * mappable objects found in the element. * <p> * The given context is used. * * @param ctx the context to use * @param root the element to process * @return the list of all top level objects found */ public Object[] loadAll(Context ctx, Element root) throws Exception { List<Object> result = new ArrayList<Object>(); loadAll(ctx, root, result); return result.toArray(); } /** * Processes the given DOM element and return a list with all top-level * mappable objects found in the element. * <p> * The default context is used. * * @param root the element to process * @return the list of all top level objects found */ public Object[] loadAll(Element root) throws Exception { return loadAll(new Context(), root); } /** * Same as {@link XMap#loadAll(Element)} but put collected objects in the * given collection. * * @param root the element to process * @param result the collection where to collect objects */ public void loadAll(Element root, Collection<Object> result) throws Exception { loadAll(new Context(), root, result); } /** * Same as {@link XMap#loadAll(Context, Element)} but put collected objects in the * given collection. * * @param ctx the context to use * @param root the element to process * @param result the collection where to collect objects */ public void loadAll(Context ctx, Element root, Collection<Object> result) throws Exception { // check if the current element is bound to an annotated object String name = root.getNodeName(); XAnnotatedObject xob = roots.get(name); if (xob != null) { Object ob = xob.newInstance(ctx, root); result.add(ob); } else { Node p = root.getFirstChild(); while (p != null) { if (p.getNodeType() == Node.ELEMENT_NODE) { loadAll(ctx, (Element) p, result); } p = p.getNextSibling(); } } } protected static Annotation checkMemberAnnotation(AnnotatedElement ae) { Annotation[] annos = ae.getAnnotations(); for (Annotation anno : annos) { if (anno.annotationType() .isAnnotationPresent(XMemberAnnotation.class)) { return anno; } } return null; } protected static XObject checkObjectAnnotation(AnnotatedElement ae) { return ae.getAnnotation(XObject.class); } private XAnnotatedMember createMember(Annotation annotation, XAccessor setter) { XAnnotatedMember member = null; int type = annotation.annotationType().getAnnotation(XMemberAnnotation.class).value(); if (type == XMemberAnnotation.NODE) { member = new XAnnotatedMember(this, setter, (XNode) annotation); } else if (type == XMemberAnnotation.NODE_LIST) { member = new XAnnotatedList(this, setter, (XNodeList) annotation); } else if (type == XMemberAnnotation.NODE_MAP) { member = new XAnnotatedMap(this, setter, (XNodeMap) annotation); } else if (type == XMemberAnnotation.PARENT) { member = new XAnnotatedParent(this, setter); } else if (type == XMemberAnnotation.CONTENT) { member = new XAnnotatedContent(this, setter, (XContent) annotation); } else if (type == XMemberAnnotation.CONTEXT) { member = new XAnnotatedContext(this, setter, (XContext) annotation); } return member; } public final XAnnotatedMember createFieldMember(Field field, Annotation annotation) { XAccessor setter = new XFieldAccessor(field); return createMember(annotation, setter); } public final XAnnotatedMember createMethodMember(Method method, Annotation annotation, Class klass) { XAccessor setter = new XMethodAccessor(method, klass); return createMember(annotation, setter); } // methods to serialize the map public String toXML(Object object) throws ParserConfigurationException, IOException{ DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder = dbfac.newDocumentBuilder(); Document doc = docBuilder.newDocument(); // create root element Element root = doc.createElement("root"); doc.appendChild(root); // load xml reprezentation in root toXML(object, root); return DOMSerializer.toString(root); } public void toXML(Object object, OutputStream os ) throws Exception{ String xml = toXML(object); os.write(xml.getBytes()); } public void toXML(Object object, File file) throws Exception{ String xml = toXML(object); FileUtils.writeFile(file, xml); } public void toXML(Object object, Element root){ XAnnotatedObject xao = objects.get(object.getClass()); if ( xao == null ){ throw new IllegalArgumentException(object.getClass().getCanonicalName() + " is NOT registred in xmap"); } XMLBuilder.saveToXML(object, root, xao); } }