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).

  1. package de.tasmiro.utils;
  2.  
  3. import java.io.File;
  4. import java.io.IOException;
  5. import java.io.InputStream;
  6. import java.io.OutputStream;
  7. import java.io.Reader;
  8. import java.lang.annotation.ElementType;
  9. import java.lang.annotation.Retention;
  10. import java.lang.annotation.RetentionPolicy;
  11. import java.lang.annotation.Target;
  12. import java.lang.reflect.Method;
  13. import java.lang.reflect.Modifier;
  14. import java.net.URL;
  15. import java.util.Map;
  16. import java.util.TreeMap;
  17.  
  18. import org.jdom2.Document;
  19. import org.jdom2.Element;
  20. import org.jdom2.JDOMException;
  21. import org.jdom2.input.SAXBuilder;
  22. import org.jdom2.output.XMLOutputter;
  23. import org.slf4j.Logger;
  24. import org.slf4j.LoggerFactory;
  25.  
  26. import com.thoughtworks.xstream.XStream;
  27. import com.thoughtworks.xstream.core.util.HierarchicalStreams;
  28. import com.thoughtworks.xstream.io.xml.JDom2Reader;
  29. import com.thoughtworks.xstream.io.xml.JDom2Writer;
  30.  
  31. /**
  32.  * Helps to maintain migrations.
  33.  *
  34.  * Inspired by com.pmease.commons.xmt. But totally rewritten
  35.  * to be much simpler and based on JDom2.
  36.  *
  37.  * In spite of xmt, the name of the migration method does not
  38.  * matter. Just put an @Migration(version) annotation before it,
  39.  * ensure it's static and it expects an Element parameter.
  40.  */
  41. public class XStreamMigrations {
  42.  
  43. private static final Logger LOGGER = LoggerFactory.getLogger(XStreamMigrations.class);
  44.  
  45. private final XStream xstream;
  46. private final XMLOutputter writer;
  47. private final SAXBuilder builder;
  48.  
  49. public XStreamMigrations() {
  50. this(new XStream());
  51. }
  52.  
  53. public XStreamMigrations(XStream xstream) {
  54. this(xstream, new SAXBuilder(), new XMLOutputter());
  55. }
  56.  
  57. public XStreamMigrations(XStream xstream, SAXBuilder builder, XMLOutputter writer) {
  58. this.xstream = xstream;
  59. this.writer = writer;
  60. this.builder = builder;
  61. }
  62.  
  63. public XStream getXStream() {
  64. return this.xstream;
  65. }
  66.  
  67. public XMLOutputter getWriter() {
  68. return this.writer;
  69. }
  70.  
  71. public SAXBuilder getBuilder() {
  72. return this.builder;
  73. }
  74.  
  75. public Object fromXML(File in) throws JDOMException, IOException {
  76. return fromXML(builder.build(in));
  77. }
  78.  
  79. public Object fromXML(URL in) throws JDOMException, IOException {
  80. return fromXML(builder.build(in));
  81. }
  82.  
  83. public Object fromXML(String in) throws JDOMException, IOException {
  84. return fromXML(builder.build(in));
  85. }
  86.  
  87. public Object fromXML(Reader in) throws JDOMException, IOException {
  88. return fromXML(builder.build(in));
  89. }
  90.  
  91. public Object fromXML(InputStream in) throws JDOMException, IOException {
  92. return fromXML(builder.build(in));
  93. }
  94.  
  95. public Object fromXML(Document doc) {
  96. return fromXML(doc.getRootElement());
  97. }
  98.  
  99. public Object fromXML(Element rootElement) {
  100. String version = rootElement.getAttributeValue("version", "0");
  101. JDom2Reader reader = new JDom2Reader(rootElement);
  102. Class<?> clazz = HierarchicalStreams.readClassType(reader, xstream.getMapper());
  103. migrate(clazz, rootElement, Integer.parseInt(version));
  104. return xstream.unmarshal(reader);
  105. }
  106.  
  107. public void toXML(Object obj, OutputStream out) throws IOException {
  108. Element rootElement = new Element("container");
  109. xstream.marshal(obj, new JDom2Writer(rootElement));
  110. rootElement.setAttribute("version", "" + getVersion(obj.getClass()));
  111. Document doc = new Document(rootElement.getChildren().get(0).detach());
  112. this.writer.output(doc, out);
  113. }
  114.  
  115. private int getVersion(Class<?> clazz) {
  116. int maxVersion = 0;
  117. for (Method m : clazz.getDeclaredMethods()) {
  118. Migration migration = m.getAnnotation(Migration.class);
  119. if (migration != null) {
  120. int version = migration.version();
  121. if ((m.getModifiers() & Modifier.STATIC) != 0) {
  122. Class<?>[] params = m.getParameterTypes();
  123. if (params.length == 1 && params[0].isAssignableFrom(Element.class)) {
  124. if (version > maxVersion) {
  125. maxVersion = version;
  126. }
  127. } else {
  128. LOGGER.warn("Ignoring @Migration(" + version + ") on method with wrong parameter count or type " + m.toGenericString());
  129. }
  130. } else {
  131. LOGGER.warn("Ignoring @Migration(" + version + ") on non-static method " + m.toGenericString());
  132. }
  133. }
  134. }
  135. return maxVersion;
  136. }
  137.  
  138. private void migrate(Class<?> clazz, Element rootElement, int version) {
  139. Map<integer, method=""> methods = new TreeMap<integer, method="">();
  140. for (Method m : clazz.getDeclaredMethods()) {
  141. if ((m.getModifiers() & Modifier.STATIC) != 0) {
  142. Migration migration = m.getAnnotation(Migration.class);
  143. if (migration != null) {
  144. int v = migration.version();
  145. if (v > version) {
  146. Class<?>[] params = m.getParameterTypes();
  147. if (params.length == 1 && params[0].isAssignableFrom(Element.class)) {
  148. Method oldMethod = methods.put(v, m);
  149. if (oldMethod != null) {
  150. throw new RuntimeException("In class " + clazz.getName() + ": Duplicate migration versions defined for " + m.toGenericString() + " and " + oldMethod.toGenericString());
  151. }
  152. }
  153. }
  154. }
  155. }
  156. }
  157.  
  158. // Invoke all methods
  159. for (Method m : methods.values()) {
  160. try {
  161. m.invoke(null, rootElement);
  162. } catch (RuntimeException e) {
  163. throw e;
  164. } catch (Exception e) {
  165. LOGGER.error("Failed to invoke migration " + m.toGenericString(), e);
  166. }
  167. }
  168. }
  169.  
  170. @Target(ElementType.METHOD)
  171. @Retention(RetentionPolicy.RUNTIME)
  172. public static @interface Migration {
  173. int version();
  174. }
  175. }

Add new comment