Annotation based migrations for XStream
While looking for a simple way to migrate outdated Java models serialized with XStream I found XMT from Robin Shine, described at Migrate Serialized Java Objects with XStream and XMT. Hell, this was exactly what I was looking for! Instead of fiddling with multiple outdate Java classes that only exists for legacy reasons, just modify the DOM document before unmarshaling the thing with XStream!
But Robin's implementation uses Strings instead of Streams, which I dislike for it's memory overhead, and depends on the very old Dom4j framework (current version from 2005). Additionally, it uses private methods to define the migrations, which is very hard to test.
So, I decided to rewrite the thing in a modern implementation using JDom2 and Annotations. And I omitted the migration listener because I didn't need it (but it would be easy to add).
Just use this class in the same way as you would use XStream. If you change your models class in an incompatible way, add a static method expecting a JDom2 Element, and annotate it with "@Migrate(version=X)", where X is the current version number of the class. Within the method, change the structure of the DOM, so it matches the current representation of your class (see the links above for an example how to do it, just adapt it from Dom4j to JDom2).
package de.tasmiro.utils; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.URL; import java.util.Map; import java.util.TreeMap; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; import org.jdom2.input.SAXBuilder; import org.jdom2.output.XMLOutputter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.core.util.HierarchicalStreams; import com.thoughtworks.xstream.io.xml.JDom2Reader; import com.thoughtworks.xstream.io.xml.JDom2Writer; /** * Helps to maintain migrations. * * Inspired by com.pmease.commons.xmt. But totally rewritten * to be much simpler and based on JDom2. * * In spite of xmt, the name of the migration method does not * matter. Just put an @Migration(version) annotation before it, * ensure it's static and it expects an Element parameter. */ public class XStreamMigrations { private static final Logger LOGGER = LoggerFactory.getLogger(XStreamMigrations.class); private final XStream xstream; private final XMLOutputter writer; private final SAXBuilder builder; public XStreamMigrations() { this(new XStream()); } public XStreamMigrations(XStream xstream) { this(xstream, new SAXBuilder(), new XMLOutputter()); } public XStreamMigrations(XStream xstream, SAXBuilder builder, XMLOutputter writer) { this.xstream = xstream; this.writer = writer; this.builder = builder; } public XStream getXStream() { return this.xstream; } public XMLOutputter getWriter() { return this.writer; } public SAXBuilder getBuilder() { return this.builder; } return fromXML(builder.build(in)); } return fromXML(builder.build(in)); } return fromXML(builder.build(in)); } return fromXML(builder.build(in)); } return fromXML(builder.build(in)); } return fromXML(doc.getRootElement()); } JDom2Reader reader = new JDom2Reader(rootElement); Class<?> clazz = HierarchicalStreams.readClassType(reader, xstream.getMapper()); return xstream.unmarshal(reader); } xstream.marshal(obj, new JDom2Writer(rootElement)); rootElement.setAttribute("version", "" + getVersion(obj.getClass())); this.writer.output(doc, out); } private int getVersion(Class<?> clazz) { int maxVersion = 0; Migration migration = m.getAnnotation(Migration.class); if (migration != null) { int version = migration.version(); Class<?>[] params = m.getParameterTypes(); if (version > maxVersion) { maxVersion = version; } } else { LOGGER.warn("Ignoring @Migration(" + version + ") on method with wrong parameter count or type " + m.toGenericString()); } } else { LOGGER.warn("Ignoring @Migration(" + version + ") on non-static method " + m.toGenericString()); } } } return maxVersion; } Map<integer, method=""> methods = new TreeMap<integer, method="">(); Migration migration = m.getAnnotation(Migration.class); if (migration != null) { int v = migration.version(); if (v > version) { Class<?>[] params = m.getParameterTypes(); if (oldMethod != null) { throw new RuntimeException("In class " + clazz.getName() + ": Duplicate migration versions defined for " + m.toGenericString() + " and " + oldMethod.toGenericString()); } } } } } } // Invoke all methods try { m.invoke(null, rootElement); throw e; LOGGER.error("Failed to invoke migration " + m.toGenericString(), e); } } } @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public static @interface Migration { int version(); } }