001/*
002 * Copyright 2011 Atteo.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the License
010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
011 * or implied. See the License for the specific language governing permissions and limitations under
012 * the License.
013 */
014package org.atteo.evo.jaxb;
015
016import java.lang.reflect.Field;
017
018import javax.annotation.Nullable;
019import javax.xml.bind.Binder;
020import javax.xml.bind.annotation.XmlElementWrapper;
021
022import org.w3c.dom.Element;
023import org.w3c.dom.Node;
024import org.w3c.dom.NodeList;
025
026/**
027 * Utility class to iterate over JAXB bindings.
028 */
029// TODO: add iteration over attributes
030public class JaxbBindings {
031    public interface Runnable {
032        /**
033         * Executed for each found element/object JAXB binding.
034         * @param element DOM element
035         * @param object object which element was unmarshalled to.
036         * @param field field which this element was unmarshalled to, can be null
037         */
038        void run(Element element, Object object, @Nullable Field field);
039    }
040
041    @SuppressWarnings("rawtypes")
042    private Binder binder;
043    private Runnable runnable;
044
045    @SuppressWarnings("rawtypes")
046    private JaxbBindings(Binder binder, Runnable runnable) {
047        this.binder = binder;
048        this.runnable = runnable;
049    }
050
051    /**
052     * Iterates over pairs consisting of {@link Element} and an object to which it was unmarshalled.
053     * @param root root XML element
054     * @param binder binder with the binding info
055     * @param runnable runnable to run for each {@link Element}/{@link Object} pair.
056     */
057    @SuppressWarnings("rawtypes")
058    public static void iterate(Element root, Binder binder, Runnable runnable) {
059        @SuppressWarnings("unchecked")
060        Object object = binder.getJAXBNode(root);
061        JaxbBindings bindRecurse = new JaxbBindings(binder, runnable);
062        try {
063            bindRecurse.recurse(root, object, null);
064        } catch (IllegalAccessException e) {
065            throw new RuntimeException(e);
066        }
067    }
068
069    private void recurse(Element element, Object object, @Nullable Field field)
070            throws IllegalAccessException {
071        runnable.run(element, object, field);
072
073        NodeList nodes = element.getChildNodes();
074        for (int i = 0; i < nodes.getLength(); i++) {
075            Node node = nodes.item(i);
076            if (!(node instanceof Element)) {
077                continue;
078            }
079            Element e = (Element) node;
080            @SuppressWarnings("unchecked")
081            Object child = binder.getJAXBNode(node);
082            Field f;
083            if (child != null) {
084                f = findFieldByValue(object, child);
085            } else {
086                f = findFieldByXmlElementWrapper(object, e.getTagName());
087                if (f == null) {
088                    continue;
089                }
090                child = f.get(object);
091            }
092            recurse(e, child, f);
093        }
094    }
095
096    private static Field findFieldByValue(Object object, Object child)
097            throws IllegalAccessException {
098        Class<?> klass = object.getClass();
099        while (klass != Object.class) {
100            for (Field field : klass.getDeclaredFields()) {
101                field.setAccessible(true);
102                if (field.get(object) == child) {
103                    return field;
104                }
105            }
106            klass = klass.getSuperclass();
107        }
108        return null;
109    }
110
111    private static Field findFieldByXmlElementWrapper(Object object, String name)
112            throws IllegalAccessException {
113        Class<?> klass = object.getClass();
114        while (klass != Object.class) {
115            for (Field field : klass.getDeclaredFields()) {
116                field.setAccessible(true);
117                XmlElementWrapper annotation = field.getAnnotation(XmlElementWrapper.class);
118                // TODO: should also check namespace here
119                if (annotation != null && name.equals(annotation.name())) {
120                    return field;
121                }
122            }
123            klass = klass.getSuperclass();
124        }
125        return null;
126    }
127}