/* * MIT License * * Copyright 2017-2018 Sabre GLBL Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.sabre.oss.conf4j.jaxb.converter; import com.sabre.oss.conf4j.converter.TypeConverter; import com.sabre.oss.conf4j.jaxb.converter.JaxbConverter.JaxbPool.Handle; import org.xml.sax.SAXException; import javax.xml.XMLConstants; 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.XmlRootElement; import javax.xml.bind.annotation.XmlSchema; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import java.io.ByteArrayInputStream; import java.io.StringWriter; import java.lang.ref.WeakReference; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Type; import java.net.URL; import java.util.ArrayDeque; import java.util.Deque; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import static java.lang.Thread.currentThread; import static java.util.Objects.requireNonNull; import static javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.Validate.notNull; /** * Type converter which supports converts object to/from xml. The object class must be properly annotated * with JAXB annotations. Only classes which has {@link XmlRootElement} applied are supported. * * @see XmlRootElement */ public class JaxbConverter<T> implements TypeConverter<T> { // Since there is only one instance of this object created, we want to be sure it is capable of handling all kinds of // Jaxb objects used in conf4j classes; thus there will be a separate JaxbPool for each distinct Jaxb class. private final ConcurrentMap<Class<T>, JaxbPool> jaxbPoolMap = new ConcurrentHashMap<>(); @Override public boolean isApplicable(Type type, Map<String, String> attributes) { requireNonNull(type, "type cannot be null"); return type instanceof Class && ((AnnotatedElement) type).getAnnotation(XmlRootElement.class) != null; } @Override public T fromString(Type type, String value, Map<String, String> attributes) { requireNonNull(type, "type cannot be null"); if (isBlank(value)) { return null; } @SuppressWarnings("unchecked") Class<T> targetClazz = (Class<T>) type; JaxbPool jaxbPool = getJaxbPool(targetClazz); try (Handle<Unmarshaller> handle = jaxbPool.borrowUnmarshaller()) { return targetClazz.cast(handle.get().unmarshal(new ByteArrayInputStream(value.getBytes()))); } catch (JAXBException e) { throw new IllegalArgumentException("Unable to convert xml to " + targetClazz + " using jaxb", e); } } @Override public String toString(Type type, T value, Map<String, String> attributes) { requireNonNull(type, "type cannot be null"); if (value == null) { return null; } @SuppressWarnings("unchecked") JaxbPool jaxbPool = getJaxbPool((Class<T>) type); try (Handle<Marshaller> handle = jaxbPool.borrowMarshaller()) { Marshaller marshaller = handle.get(); StringWriter stringWriter = new StringWriter(); marshaller.marshal(value, stringWriter); return stringWriter.toString(); } catch (JAXBException e) { throw new IllegalArgumentException("Unable to convert " + value.getClass().getName() + " to xml using jaxb", e); } } private JaxbPool getJaxbPool(Class<T> clazz) { return jaxbPoolMap.computeIfAbsent(clazz, JaxbPool::new); } /** * This class represents single JaxbPool, which is responsible for handling single Jaxb object */ static final class JaxbPool { interface Handle<T> extends AutoCloseable { T get(); @Override void close(); } private final JAXBContext context; private final Schema schema; private final Pool<Marshaller> marshallers = new Pool<>(new MarshallerSupplier()); private final Pool<Unmarshaller> unmarshallers = new Pool<>(new UnmarshallerSupplier()); JaxbPool(Class<?> clazz) { try { this.context = JAXBContext.newInstance(clazz); this.schema = readXsdSchema(clazz); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } /** * Returns XSD schema by annotation of given class or null when schemat doesn not exist within the class * * @param clazz class to read schema * @return XSD schema or null */ Schema readXsdSchema(Class<?> clazz) throws SAXException { XmlSchema xmlSchema = clazz.getPackage().getAnnotation(XmlSchema.class); String schemaLocation = xmlSchema != null ? xmlSchema.location() : null; if (schemaLocation == null) { return null; } SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); return schemaFactory.newSchema(getResource(schemaLocation)); } public JAXBContext getContext() { return context; } public Handle<Marshaller> borrowMarshaller() { return new HandleImpl<>(marshallers); } public Handle<Unmarshaller> borrowUnmarshaller() { return new HandleImpl<>(unmarshallers); } private static final class HandleImpl<E> implements Handle<E> { private final Pool<E> pool; private final AtomicReference<E> element = new AtomicReference<>(); private HandleImpl(Pool<E> pool) { this.pool = pool; } @Override public E get() { E e = pool.borrow(); element.set(e); return e; } @Override public void close() { E e = element.getAndSet(null); if (e != null) { pool.release(e); } } } private static final class Pool<E> { private final Supplier<E> supplier; private final Deque<WeakReference<E>> elements = new ArrayDeque<>(); private Pool(Supplier<E> supplier) { this.supplier = notNull(supplier, "supplier cannot be null"); } public E borrow() { E element = null; synchronized (elements) { while (!elements.isEmpty()) { WeakReference<E> reference = elements.removeLast(); element = reference.get(); if (element != null) { break; } } } if (element == null) { element = supplier.get(); } return element; } public void release(E element) { notNull(element, "element cannot be null"); synchronized (elements) { elements.addLast(new WeakReference<>(element)); } } } private final class MarshallerSupplier implements Supplier<Marshaller> { @Override public Marshaller get() { try { Marshaller marshaller = context.createMarshaller(); marshaller.setProperty(JAXB_FORMATTED_OUTPUT, Boolean.TRUE); marshaller.setSchema(schema); return marshaller; } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } } private final class UnmarshallerSupplier implements Supplier<Unmarshaller> { @Override public Unmarshaller get() { try { Unmarshaller unmarshaller = context.createUnmarshaller(); unmarshaller.setSchema(schema); return unmarshaller; } catch (JAXBException e) { throw new RuntimeException(e); } } } private static URL getResource(String resourceName) { ClassLoader loader = defaultIfNull(currentThread().getContextClassLoader(), JaxbPool.class.getClassLoader()); URL url = loader.getResource(resourceName); if (url == null) { throw new IllegalArgumentException("resource " + resourceName + " not found."); } return url; } } }