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