001/*
002 * (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nuxeo - initial API and implementation
018 *
019 * $Id$
020 */
021
022package org.nuxeo.common.xmap;
023
024import java.io.File;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.OutputStream;
028import java.lang.annotation.Annotation;
029import java.lang.reflect.AnnotatedElement;
030import java.lang.reflect.Field;
031import java.lang.reflect.Method;
032import java.net.URL;
033import java.util.ArrayList;
034import java.util.Collection;
035import java.util.Hashtable;
036import java.util.List;
037import java.util.Map;
038
039import javax.xml.parsers.DocumentBuilder;
040import javax.xml.parsers.DocumentBuilderFactory;
041import javax.xml.parsers.ParserConfigurationException;
042
043import org.nuxeo.common.utils.FileUtils;
044import org.nuxeo.common.xmap.annotation.XContent;
045import org.nuxeo.common.xmap.annotation.XContext;
046import org.nuxeo.common.xmap.annotation.XMemberAnnotation;
047import org.nuxeo.common.xmap.annotation.XNode;
048import org.nuxeo.common.xmap.annotation.XNodeList;
049import org.nuxeo.common.xmap.annotation.XNodeMap;
050import org.nuxeo.common.xmap.annotation.XObject;
051import org.nuxeo.common.xmap.annotation.XParent;
052import org.w3c.dom.Document;
053import org.w3c.dom.Element;
054import org.w3c.dom.Node;
055import org.xml.sax.SAXException;
056
057/**
058 * XMap maps an XML file to a java object.
059 * <p>
060 * The mapping is described by annotations on java objects.
061 * <p>
062 * The following annotations are supported:
063 * <ul>
064 * <li> {@link XObject} Mark the object as being mappable to an XML node
065 * <li> {@link XNode} Map an XML node to a field of a mappable object
066 * <li> {@link XNodeList} Map an list of XML nodes to a field of a mappable object
067 * <li> {@link XNodeMap} Map an map of XML nodes to a field of a mappable object
068 * <li> {@link XContent} Map an XML node content to a field of a mappable object
069 * <li> {@link XParent} Map a field of the current mappable object to the parent object if any exists The parent object
070 * is the mappable object containing the current object as a field
071 * </ul>
072 * The mapping is done in 2 steps:
073 * <ul>
074 * <li>The XML file is loaded as a DOM document
075 * <li>The DOM document is parsed and the nodes mapping is resolved
076 * </ul>
077 *
078 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
079 */
080@SuppressWarnings({ "SuppressionAnnotation" })
081public class XMap {
082
083    private static final DocumentBuilderFactory initFactory() {
084        Thread t = Thread.currentThread();
085        ClassLoader cl = t.getContextClassLoader();
086        t.setContextClassLoader(XMap.class.getClassLoader());
087        try {
088            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
089            factory.setNamespaceAware(true);
090            return factory;
091        } finally {
092            t.setContextClassLoader(cl);
093        }
094    }
095
096    public static DocumentBuilderFactory getFactory() {
097        return factory;
098    }
099
100    private static DocumentBuilderFactory factory = initFactory();
101
102    // top level objects
103    private final Map<String, XAnnotatedObject> roots;
104
105    // the scanned objects
106    private final Map<Class<?>, XAnnotatedObject> objects;
107
108    private final Map<Class<?>, XValueFactory> factories;
109
110    /**
111     * Creates a new XMap object.
112     */
113    public XMap() {
114        objects = new Hashtable<Class<?>, XAnnotatedObject>();
115        roots = new Hashtable<String, XAnnotatedObject>();
116        factories = new Hashtable<Class<?>, XValueFactory>(XValueFactory.defaultFactories);
117    }
118
119    /**
120     * Gets the value factory used for objects of the given class.
121     * <p>
122     * Value factories are used to decode values from XML strings.
123     *
124     * @param type the object type
125     * @return the value factory if any, null otherwise
126     */
127    public XValueFactory getValueFactory(Class<?> type) {
128        return factories.get(type);
129    }
130
131    /**
132     * Sets a custom value factory for the given class.
133     * <p>
134     * Value factories are used to decode values from XML strings.
135     *
136     * @param type the object type
137     * @param factory the value factory to use for the given type
138     */
139    public void setValueFactory(Class<?> type, XValueFactory factory) {
140        factories.put(type, factory);
141    }
142
143    /**
144     * Gets a list of scanned objects.
145     * <p>
146     * Scanned objects are annotated objects that were registered by this XMap instance.
147     */
148    public Collection<XAnnotatedObject> getScannedObjects() {
149        return objects.values();
150    }
151
152    /**
153     * Gets the root objects.
154     * <p>
155     * Root objects are scanned objects that can be mapped to XML elements that are not part from other objects.
156     *
157     * @return the root objects
158     */
159    public Collection<XAnnotatedObject> getRootObjects() {
160        return roots.values();
161    }
162
163    /**
164     * Registers a mappable object class.
165     * <p>
166     * The class will be scanned for XMap annotations and a mapping description is created.
167     *
168     * @param klass the object class
169     * @return the mapping description
170     */
171    public XAnnotatedObject register(Class<?> klass) {
172        XAnnotatedObject xao = objects.get(klass);
173        if (xao == null) { // avoid scanning twice
174            XObject xob = checkObjectAnnotation(klass);
175            if (xob != null) {
176                xao = new XAnnotatedObject(this, klass, xob);
177                objects.put(xao.klass, xao);
178                scan(xao);
179                String key = xob.value();
180                if (key.length() > 0) {
181                    roots.put(xao.path.path, xao);
182                }
183            }
184        }
185        return xao;
186    }
187
188    private void scan(XAnnotatedObject xob) {
189        scanClass(xob, xob.klass);
190    }
191
192    private void scanClass(XAnnotatedObject xob, Class<?> aClass) {
193        Field[] fields = aClass.getDeclaredFields();
194        for (Field field : fields) {
195            Annotation anno = checkMemberAnnotation(field);
196            if (anno != null) {
197                XAnnotatedMember member = createFieldMember(field, anno);
198                xob.addMember(member);
199            }
200        }
201
202        Method[] methods = aClass.getDeclaredMethods();
203        for (Method method : methods) {
204            // we accept only methods with one parameter
205            Class<?>[] paramTypes = method.getParameterTypes();
206            if (paramTypes.length != 1) {
207                continue;
208            }
209            Annotation anno = checkMemberAnnotation(method);
210            if (anno != null) {
211                XAnnotatedMember member = createMethodMember(method, anno, aClass);
212                xob.addMember(member);
213            }
214        }
215
216        // scan superClass annotations
217        if (aClass.getSuperclass() != null) {
218            scanClass(xob, aClass.getSuperclass());
219        }
220    }
221
222    /**
223     * Processes the XML file at the given URL using a default context.
224     *
225     * @param url the XML file url
226     * @return the first registered top level object that is found in the file, or null if no objects are found.
227     */
228    public Object load(URL url) throws IOException {
229        return load(new Context(), url.openStream());
230    }
231
232    /**
233     * Processes the XML file at the given URL and using the given contexts.
234     *
235     * @param ctx the context to use
236     * @param url the XML file url
237     * @return the first registered top level object that is found in the file.
238     */
239    public Object load(Context ctx, URL url) throws IOException {
240        return load(ctx, url.openStream());
241    }
242
243    /**
244     * Processes the XML content from the given input stream using a default context.
245     *
246     * @param in the XML input source
247     * @return the first registered top level object that is found in the file.
248     */
249    public Object load(InputStream in) throws IOException {
250        return load(new Context(), in);
251    }
252
253    /**
254     * Processes the XML content from the given input stream using the given context.
255     *
256     * @param ctx the context to use
257     * @param in the input stream
258     * @return the first registered top level object that is found in the file.
259     */
260    public Object load(Context ctx, InputStream in) throws IOException {
261        try {
262            DocumentBuilderFactory factory = getFactory();
263            DocumentBuilder builder = factory.newDocumentBuilder();
264            Document document = builder.parse(in);
265            return load(ctx, document.getDocumentElement());
266        } catch (ParserConfigurationException e) {
267            throw new IOException(e);
268        } catch (SAXException e) {
269            throw new IOException(e);
270        } finally {
271            if (in != null) {
272                try {
273                    in.close();
274                } catch (IOException e) {
275                    // do nothing
276                }
277            }
278        }
279    }
280
281    /**
282     * Processes the XML file at the given URL using a default context.
283     * <p>
284     * Returns a list with all registered top level objects that are found in the file.
285     * <p>
286     * If not objects are found, an empty list is returned.
287     *
288     * @param url the XML file url
289     * @return a list with all registered top level objects that are found in the file
290     */
291    public Object[] loadAll(URL url) throws IOException {
292        return loadAll(new Context(), url.openStream());
293    }
294
295    /**
296     * Processes the XML file at the given URL using the given context
297     * <p>
298     * Return a list with all registered top level objects that are found in the file.
299     * <p>
300     * If not objects are found an empty list is retoruned.
301     *
302     * @param ctx the context to use
303     * @param url the XML file url
304     * @return a list with all registered top level objects that are found in the file
305     */
306    public Object[] loadAll(Context ctx, URL url) throws IOException {
307        return loadAll(ctx, url.openStream());
308    }
309
310    /**
311     * Processes the XML from the given input stream using the given context.
312     * <p>
313     * Returns a list with all registered top level objects that are found in the file.
314     * <p>
315     * If not objects are found, an empty list is returned.
316     *
317     * @param in the XML input stream
318     * @return a list with all registered top level objects that are found in the file
319     */
320    public Object[] loadAll(InputStream in) throws IOException {
321        return loadAll(new Context(), in);
322    }
323
324    /**
325     * Processes the XML from the given input stream using the given context.
326     * <p>
327     * Returns a list with all registered top level objects that are found in the file.
328     * <p>
329     * If not objects are found, an empty list is returned.
330     *
331     * @param ctx the context to use
332     * @param in the XML input stream
333     * @return a list with all registered top level objects that are found in the file
334     */
335    public Object[] loadAll(Context ctx, InputStream in) throws IOException {
336        try {
337            DocumentBuilderFactory factory = getFactory();
338            DocumentBuilder builder = factory.newDocumentBuilder();
339            Document document = builder.parse(in);
340            return loadAll(ctx, document.getDocumentElement());
341        } catch (ParserConfigurationException e) {
342            throw new IOException(e);
343        } catch (SAXException e) {
344            throw new IOException(e);
345        } finally {
346            if (in != null) {
347                try {
348                    in.close();
349                } catch (IOException e) {
350                    // do nothing
351                }
352            }
353        }
354    }
355
356    /**
357     * Processes the given DOM element and return the first mappable object found in the element.
358     * <p>
359     * A default context is used.
360     *
361     * @param root the element to process
362     * @return the first object found in this element or null if none
363     */
364    public Object load(Element root) {
365        return load(new Context(), root);
366    }
367
368    /**
369     * Processes the given DOM element and return the first mappable object found in the element.
370     * <p>
371     * The given context is used.
372     *
373     * @param ctx the context to use
374     * @param root the element to process
375     * @return the first object found in this element or null if none
376     */
377    public Object load(Context ctx, Element root) {
378        // check if the current element is bound to an annotated object
379        String name = root.getNodeName();
380        XAnnotatedObject xob = roots.get(name);
381        if (xob != null) {
382            return xob.newInstance(ctx, root);
383        } else {
384            Node p = root.getFirstChild();
385            while (p != null) {
386                if (p.getNodeType() == Node.ELEMENT_NODE) {
387                    // Recurse in the first child Element
388                    return load((Element) p);
389                }
390                p = p.getNextSibling();
391            }
392            // We didn't find any Element
393            return null;
394        }
395    }
396
397    /**
398     * Processes the given DOM element and return a list with all top-level mappable objects found in the element.
399     * <p>
400     * The given context is used.
401     *
402     * @param ctx the context to use
403     * @param root the element to process
404     * @return the list of all top level objects found
405     */
406    public Object[] loadAll(Context ctx, Element root) {
407        List<Object> result = new ArrayList<Object>();
408        loadAll(ctx, root, result);
409        return result.toArray();
410    }
411
412    /**
413     * Processes the given DOM element and return a list with all top-level mappable objects found in the element.
414     * <p>
415     * The default context is used.
416     *
417     * @param root the element to process
418     * @return the list of all top level objects found
419     */
420    public Object[] loadAll(Element root) {
421        return loadAll(new Context(), root);
422    }
423
424    /**
425     * Same as {@link XMap#loadAll(Element)} but put collected objects in the given collection.
426     *
427     * @param root the element to process
428     * @param result the collection where to collect objects
429     */
430    public void loadAll(Element root, Collection<Object> result) {
431        loadAll(new Context(), root, result);
432    }
433
434    /**
435     * Same as {@link XMap#loadAll(Context, Element)} but put collected objects in the given collection.
436     *
437     * @param ctx the context to use
438     * @param root the element to process
439     * @param result the collection where to collect objects
440     */
441    public void loadAll(Context ctx, Element root, Collection<Object> result) {
442        // check if the current element is bound to an annotated object
443        String name = root.getNodeName();
444        XAnnotatedObject xob = roots.get(name);
445        if (xob != null) {
446            Object ob = xob.newInstance(ctx, root);
447            result.add(ob);
448        } else {
449            Node p = root.getFirstChild();
450            while (p != null) {
451                if (p.getNodeType() == Node.ELEMENT_NODE) {
452                    loadAll(ctx, (Element) p, result);
453                }
454                p = p.getNextSibling();
455            }
456        }
457    }
458
459    protected static Annotation checkMemberAnnotation(AnnotatedElement ae) {
460        Annotation[] annos = ae.getAnnotations();
461        for (Annotation anno : annos) {
462            if (anno.annotationType().isAnnotationPresent(XMemberAnnotation.class)) {
463                return anno;
464            }
465        }
466        return null;
467    }
468
469    protected static XObject checkObjectAnnotation(AnnotatedElement ae) {
470        return ae.getAnnotation(XObject.class);
471    }
472
473    private XAnnotatedMember createMember(Annotation annotation, XAccessor setter) {
474        XAnnotatedMember member = null;
475        int type = annotation.annotationType().getAnnotation(XMemberAnnotation.class).value();
476        if (type == XMemberAnnotation.NODE) {
477            member = new XAnnotatedMember(this, setter, (XNode) annotation);
478        } else if (type == XMemberAnnotation.NODE_LIST) {
479            member = new XAnnotatedList(this, setter, (XNodeList) annotation);
480        } else if (type == XMemberAnnotation.NODE_MAP) {
481            member = new XAnnotatedMap(this, setter, (XNodeMap) annotation);
482        } else if (type == XMemberAnnotation.PARENT) {
483            member = new XAnnotatedParent(this, setter);
484        } else if (type == XMemberAnnotation.CONTENT) {
485            member = new XAnnotatedContent(this, setter, (XContent) annotation);
486        } else if (type == XMemberAnnotation.CONTEXT) {
487            member = new XAnnotatedContext(this, setter, (XContext) annotation);
488        }
489        return member;
490    }
491
492    public final XAnnotatedMember createFieldMember(Field field, Annotation annotation) {
493        XAccessor setter = new XFieldAccessor(field);
494        return createMember(annotation, setter);
495    }
496
497    public final XAnnotatedMember createMethodMember(Method method, Annotation annotation, Class<?> klass) {
498        XAccessor setter = new XMethodAccessor(method, klass);
499        return createMember(annotation, setter);
500    }
501
502    // methods to serialize the map
503    public String toXML(Object object) throws IOException {
504        DocumentBuilderFactory dbfac = getFactory();
505        DocumentBuilder docBuilder;
506        try {
507            docBuilder = dbfac.newDocumentBuilder();
508        } catch (ParserConfigurationException e) {
509            throw new IOException(e);
510        }
511        Document doc = docBuilder.newDocument();
512        // create root element
513        Element root = doc.createElement("root");
514        doc.appendChild(root);
515
516        // load xml reprezentation in root
517        toXML(object, root);
518        return DOMSerializer.toString(root);
519    }
520
521    public void toXML(Object object, OutputStream os) throws IOException {
522        String xml = toXML(object);
523        os.write(xml.getBytes());
524    }
525
526    public void toXML(Object object, File file) throws IOException {
527        String xml = toXML(object);
528        FileUtils.writeFile(file, xml);
529    }
530
531    public void toXML(Object object, Element root) {
532        XAnnotatedObject xao = objects.get(object.getClass());
533        if (xao == null) {
534            throw new IllegalArgumentException(object.getClass().getCanonicalName() + " is NOT registred in xmap");
535        }
536        XMLBuilder.saveToXML(object, root, xao);
537    }
538
539}