Java tutorial
/* * 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 } } } } }