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