/* * 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 com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.xml.JDom2Reader; import com.thoughtworks.xstream.io.xml.JDom2Writer; import com.thoughtworks.xstream.mapper.CannotResolveClassException; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.jdom2.Element; 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; /** * A PojoConverter that uses XStream for marshalling and unmarshalling. * @author Gordon */ public class XStreamPojoConverter implements PojoConverter { private final XStream xstream; /** * Gets the XStream instance that performs the POJO mapping. * @return The XStream instance initialized with the POJO converter. */ public XStream getXStream(){ return xstream; } private final boolean isThreadSafe; private final ReadWriteLock lock; /** * Creates a new XStream instance with the default options. * This instance is not thread safe on serializing new objects, * since it relies on annotation processing. */ public XStreamPojoConverter(){ this.xstream = new XStream(); this.isThreadSafe = false; lock = new ReentrantReadWriteLock(); } /** * Creates a pojo converter that uses the given configured XStream instance. * <p/> * From the XStream documentation: * <pre> * The XStream instance is thread-safe. That is, once the XStream instance has been created and configured, * it may be shared across multiple threads allowing objects to be serialized/deserialized concurrently. * Note, that this only applies if annotations are not auto-detected on -the-fly. * </pre> * Therefore, if you are using annotations to control serialization, ensure * that you set "isThreadSafe" to false. * @param xstream */ public XStreamPojoConverter(XStream xstream, boolean isThreadSafe){ this.xstream = xstream; this.isThreadSafe = isThreadSafe; if(!isThreadSafe){ lock = new ReentrantReadWriteLock(); } else{ lock = null; } } @Override public ConversionService extend(ConversionService service) { return new XStreamConversionService(service); } private class XStreamConversionService implements ConversionService { ConversionService base; public XStreamConversionService(ConversionService base){ this.base = base; } @Override public boolean canConvert(Class<?> source, Class<?> target) { if(!base.canConvert(source, target)){ if(Element.class.equals(target)) return true; else if(Element.class.equals(source)){ return true; } else{ //can't convert return false; } } //else the base can convert return true; } @Override public <T> T convert(Object source, Class<T> target) throws ConversionException { try{ return base.convert(source, target); } catch(ConversionNotSupportedException ex){ if(source == null){ throw ex; } //the base class does not support the conversion - try to make converters if(Element.class.equals(target)) return makeConvertersAndRetry(source.getClass(), source, target); else if(Element.class.equals(source.getClass())){ return makeConvertersAndRetry(target, source, target); } else{ //can't convert throw ex; } } } @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 <T, U> T makeConvertersAndRetry(Class<U> clazz, Object source, Class<T> target) throws ConversionException { if(!isThreadSafe){ //need to synchronize lock.writeLock().lock(); try{ //doublecheck if another thread got here first if(!base.canConvert(clazz, Element.class)){ //any other threads will stall on the readlock base.addConverter(clazz, Element.class, threadSafeMarshallingConverter); XStreamThreadSafeUnmarshallingConverter<U> unmarshaller = new XStreamThreadSafeUnmarshallingConverter<>(clazz); base.addConverter(Element.class, clazz, unmarshaller); //process the annotations xstream.processAnnotations(clazz); } } finally{ lock.writeLock().unlock(); } //invoke the converters via the base return base.convert(source, target); } else{ //no need to synchronize, the XStream class is thread safe. //Since we've said thread-safe, we also have no need to process annotations. base.addConverter(clazz, Element.class, marshallingConverter); XStreamUnmarshallingConverter<U> unmarshaller = new XStreamUnmarshallingConverter<>(clazz); base.addConverter(Element.class, clazz, unmarshaller); if(Element.class.equals(target)) return (T)marshallingConverter.convert(source); else { return (T)unmarshaller.convert((Element)source); } } } } //marshalling singletons private final Converter<Object, Element> marshallingConverter = new XStreamMarshallingConverter(); private final Converter<Object, Element> threadSafeMarshallingConverter = new XStreamThreadSafeMarshallingConverter(); /** * A converter that performs marshalling to XML */ private class XStreamMarshallingConverter implements Converter<Object, Element>{ @Override public Element convert(Object source) throws ConversionException { JDom2Writer writer = new JDom2Writer(); try{ xstream.marshal(source, writer); } catch(Exception ex){ throw new ConversionException("Unable to marshal " + source.getClass() + " to xml", ex); } return writer.getTopLevelNode(); } } /** * Wraps the marshalling converter in a global readlock for the XStream instance, * since it is not thread safe when reading annotations. */ private class XStreamThreadSafeMarshallingConverter implements Converter<Object, Element>{ @Override public Element convert(Object source) throws ConversionException { lock.readLock().lock(); try{ return marshallingConverter.convert(source); } finally{ lock.readLock().unlock(); } } } /** * A converter that performs unmarshalling from XML * @param <T> */ private class XStreamUnmarshallingConverter<T> implements Converter<Element, T>{ Class<T> clazz; public XStreamUnmarshallingConverter(Class<T> clazz){ this.clazz = clazz; } @Override public T convert(Element source) throws ConversionException { JDom2Reader reader = new JDom2Reader(source); try{ T ret = (T)xstream.unmarshal(reader); return ret; } catch(Exception ex){ throw new ConversionException("Unable to unmarshal " + clazz + " from xml", ex); } } } /** * Wraps the unmarshalling converter in a global readlock for the XStream instance, * since it is not thread safe when reading annotations. * @param <T> */ private class XStreamThreadSafeUnmarshallingConverter<T> implements Converter<Element, T>{ Class<T> clazz; XStreamUnmarshallingConverter<T> innerConverter; public XStreamThreadSafeUnmarshallingConverter(Class<T> clazz){ this.clazz = clazz; this.innerConverter = new XStreamUnmarshallingConverter<>(clazz); } @Override public T convert(Element source) throws ConversionException { lock.readLock().lock(); try{ return innerConverter.convert(source); } finally{ lock.readLock().unlock(); } } } }