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