org.xflatdb.xflat.convert.converters.JAXBPojoConverter.java Source code

Java tutorial

Introduction

Here is the source code for org.xflatdb.xflat.convert.converters.JAXBPojoConverter.java

Source

/* 
*   Copyright 2013 Gordon Burgett and individual contributors
*
*   Licensed under the Apache License, Version 2.0 (the "License");
*   you may not use this file except in compliance with the License.
*   You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
*   Unless required by applicable law or agreed to in writing, software
*   distributed under the License is distributed on an "AS IS" BASIS,
*   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*   See the License for the specific language governing permissions and
*   limitations under the License.
*/
package org.xflatdb.xflat.convert.converters;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import org.apache.commons.logging.LogFactory;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.Namespace;
import org.jdom2.filter.Filters;
import org.jdom2.jaxb.JDOMStreamReader;
import org.jdom2.jaxb.JDOMStreamWriter;
import org.jdom2.output.XMLOutputter;
import org.jdom2.xpath.XPathExpression;
import org.jdom2.xpath.XPathFactory;
import org.xflatdb.xflat.convert.ConversionException;
import org.xflatdb.xflat.convert.ConversionNotSupportedException;
import org.xflatdb.xflat.convert.ConversionService;
import org.xflatdb.xflat.convert.Converter;
import org.xflatdb.xflat.convert.PojoConverter;
import org.xflatdb.xflat.db.IdAccessor;

/**
 * A PojoConverter that extends a ConversionService to convert all unknown
 * objects to and from Element using JAXB.
 * Any conversion which 
 * @author gordon
 */
public class JAXBPojoConverter implements PojoConverter {

    /**
     * Extends the given conversion service with a JAXB conversion service.
     * A JAXB Marshaller and Unmarshaller will be created for classes that cannot
     * be converted using the given conversion service.
     * @param service The service to extend
     * @return A new service that extends the functionality of the given service.
     */
    @Override
    public ConversionService extend(ConversionService service) {
        if (service instanceof JAXBConversionService) {
            return service;
        }

        return new JAXBConversionService(service);
    }

    private Map<Class<?>, XPathExpression<Object>> idSelectorCache = new ConcurrentHashMap<>();

    /**
     * Guesses an alternate ID selector for the class based on how JAXB would
     * map the ID property.  If the class has no ID property, a null expression
     * is returned.
     * @param clazz The class for which an alternate ID selector is required.
     * @return The alternate ID selector, or null if the class hass no ID property.
     */
    public XPathExpression<Object> idSelector(Class<?> clazz) {

        if (!idSelectorCache.containsKey(clazz)) {
            XPathExpression<Object> ret = makeIdSelector(clazz);
            idSelectorCache.put(clazz, ret);
            return ret;
        }
        return idSelectorCache.get(clazz);
    }

    private XPathExpression<Object> makeIdSelector(Class<?> clazz) {
        IdAccessor accessor = IdAccessor.forClass(clazz);

        if (!accessor.hasId()) {
            return null;
        }

        Namespace ns = null;
        StringBuilder ret = new StringBuilder(clazz.getSimpleName());

        XmlAttribute attribute = (XmlAttribute) accessor.getIdPropertyAnnotation(XmlAttribute.class);
        if (attribute != null) {
            ret.append("/@");
            if (attribute.namespace() != null) {
                ns = Namespace.getNamespace("id", attribute.namespace());
                ret.append(ns.getPrefix()).append(":");
            }
            if (attribute.name() != null) {
                ret.append(attribute.name());
            } else {
                ret.append(accessor.getIdPropertyName());
            }
        } else {
            ret.append("/");
            XmlElement element = (XmlElement) accessor.getIdPropertyAnnotation(XmlElement.class);
            if (element != null) {
                if (element.namespace() != null) {
                    ns = Namespace.getNamespace("id", attribute.namespace());
                    ret.append(ns.getPrefix()).append(":");
                }
                if (element.name() != null) {
                    ret.append(element.name());
                } else {
                    ret.append(accessor.getIdPropertyName());
                }
            } else {
                ret.append(accessor.getIdPropertyName());
            }
        }

        if (ns == null) {
            return XPathFactory.instance().compile(ret.toString());
        }
        return XPathFactory.instance().compile(ret.toString(), Filters.fpassthrough(), null, ns);
    }

    private static class JAXBConversionService implements ConversionService {

        ConversionService base;

        Set<Class<?>> cannotMap = new HashSet<>();

        public JAXBConversionService(ConversionService base) {
            this.base = base;
        }

        @Override
        public boolean canConvert(Class<?> source, Class<?> target) {
            if (base.canConvert(source, target)) {
                return true;
            }

            //are we converting to/from Element?
            if (!Element.class.equals(source) && !Element.class.equals(target)) {
                return false;
            }

            try {
                if (Element.class.equals(source)) {
                    return this.makeJaxbConverters(target);
                } else {
                    return this.makeJaxbConverters(source);
                }
            } catch (ConversionException ex) {
                LogFactory.getLog(getClass())
                        .trace("ConversionException encountered when attempting to map class with JAXB", ex);
                return false;
            }
        }

        @Override
        public <T> T convert(Object source, Class<T> target) throws ConversionException {

            try {
                return base.convert(source, target);
            } catch (ConversionNotSupportedException ex) {
                //the base class does not support the conversion - try to make JAXB converters

                Class<?> pojoClass;
                if (Element.class.equals(target)) {
                    if (source == null) {
                        throw ex;
                    }

                    pojoClass = source.getClass();
                } else {
                    pojoClass = target;
                }

                if (!this.makeJaxbConverters(pojoClass)) {
                    throw new ConversionNotSupportedException(
                            "Unable to make JAXB converters for target " + pojoClass);
                }

                //try again now that we successfully made JAXB converters
                return base.convert(source, target);
            }
        }

        @Override
        public <S, T> void addConverter(Class<S> sourceType, Class<T> targetType,
                Converter<? super S, ? extends T> converter) {
            base.addConverter(sourceType, targetType, converter);
        }

        @Override
        public void removeConverter(Class<?> sourceType, Class<?> targetType) {
            base.removeConverter(sourceType, targetType);
        }

        private boolean makeJaxbConverters(Class<?> target) throws ConversionNotSupportedException {
            //are the classes non-primitive Objects?
            if (target.isPrimitive() || target.isEnum() || target.isAnnotation() || target.isInterface()) {
                return false;
            }

            //is it a class we previously tried and failed to map?
            if (cannotMap.contains(target)) {
                return false;
            }

            try {
                JAXBContext context = JAXBContext.newInstance(target);

                addJAXBConverters(context, target, base);
                return true;
            } catch (JAXBException ex) {
                //failure to map - add to the set so we don't retry
                this.cannotMap.add(target);
                throw new ConversionNotSupportedException("Cannot create JAXB binding for " + target, ex);
            }
        }

    }

    /**
     * Adds converters to and from JDOM {@link Element} for the given class.
     * This gives you more control over the {@link JAXBContext} than just allowing the 
     * JAXBPojoConverter to create a default JAXBContext for your pojos.
     * @param <T> The generic type of the pojo which should be convertible.
     * @param context The JAXBContext created to map the pojo to XML.
     * @param baseClass The pojo class which should be convertible.
     * @param registerTo The conversion service to which the converters should be added.
     * @throws JAXBException If an error occurs when creating the marshaller or unmarshaller.
     */
    public static <T> void addJAXBConverters(JAXBContext context, Class<T> baseClass, ConversionService registerTo)
            throws JAXBException {
        Converter<T, Element> marshaller = new JAXBMarshallingConverter(baseClass, context);
        Converter<Element, T> unmarshaller = new JAXBUnmarshallingConverter<>(baseClass, context);

        registerTo.addConverter(baseClass, Element.class, marshaller);
        registerTo.addConverter(Element.class, baseClass, unmarshaller);
    }

    private static class JAXBMarshallingConverter<T> implements Converter<T, Element> {
        XMLInputFactory factory = XMLInputFactory.newFactory();

        ThreadLocal<Marshaller> marshaller;
        JAXBContext context;
        Class<T> clazz;

        public JAXBMarshallingConverter(final Class<T> clazz, final JAXBContext context) {
            this.clazz = clazz;
            this.marshaller = new ThreadLocal<>();
            this.context = context;
        }

        @Override
        public Element convert(T source) throws ConversionException {

            Marshaller marshaller = this.marshaller.get();
            if (marshaller == null) {
                try {
                    this.marshaller.set(marshaller = context.createMarshaller());
                } catch (JAXBException ex) {
                    throw new ConversionException("Unable to create marshaller for class " + clazz, ex);
                }
            }

            try {
                Document doc;
                JDOMStreamWriter out = new JDOMStreamWriter();
                try {
                    marshaller.marshal(source, out);

                    doc = out.getDocument();
                } finally {
                    out.close();
                }

                return doc.detachRootElement();

            } catch (JAXBException | XMLStreamException ex) {
                throw new ConversionException("Unable to convert", ex);
            }
        }
    }

    private static class JAXBUnmarshallingConverter<T> implements Converter<Element, T> {

        org.jdom2.output.XMLOutputter outputter = new XMLOutputter();
        ThreadLocal<Unmarshaller> unmarshaller;
        JAXBContext context;
        Class<T> clazz;

        public JAXBUnmarshallingConverter(final Class<T> clazz, final JAXBContext context) throws JAXBException {
            this.clazz = clazz;
            this.unmarshaller = new ThreadLocal<>();
            this.context = context;
        }

        @Override
        public T convert(Element source) throws ConversionException {

            Unmarshaller unmarshaller = this.unmarshaller.get();
            if (unmarshaller == null) {
                try {
                    this.unmarshaller.set(unmarshaller = context.createUnmarshaller());
                } catch (JAXBException ex) {
                    throw new ConversionException("Unable to create unmarshaller for class " + clazz);
                }
            }

            Document doc = new Document();
            doc.setRootElement(source.detach());

            JDOMStreamReader in = new JDOMStreamReader(doc);
            try {
                T ret = (T) unmarshaller.unmarshal(in);
                return ret;
            } catch (JAXBException | ClassCastException ex) {
                throw new ConversionException("Unable to convert element to " + clazz, ex);
            } finally {
                try {
                    in.close();
                } catch (XMLStreamException ex) {
                    //ignore
                }
            }
        }

    }
}