View Source

h2. Migrate super class and sub class separately

Continue with version "1" of the Task class defined in [Getting Started] chapter:
{code}
package com.pmease.commons.xmt.bean;

import java.util.Stack;
import org.dom4j.Element;
import com.pmease.commons.xmt.VersionedDocument;

public class Task {
enum Priority {HIGH, MEDIUM, LOW}

public Priority priority;

@SuppressWarnings("unused")
private void migrate1(VersionedDocument dom, Stack<Integer> versions) {
Element element = dom.getRootElement().element("prioritized");
element.setName("priority");
if (element.getText().equals("true"))
element.setText("HIGH");
else
element.setText("LOW");
}
}
{code}
Now we have class _CompileTask_ subclassing from Task class as below:
{code}
package example;

import java.util.List;

public class CompileTask extends Task {
public List<String> srcFiles;
}
{code}
Instance of _CompileTask_ can be serialized to XML with XMT as below:
{code}
package example;

import java.util.ArrayList;

import com.pmease.commons.xmt.VersionedDocument;

public class Test {
public static void main(String args[]) {
CompileTask task = new CompileTask();
task.priority = Task.Priority.HIGH;
task.srcFiles = new ArrayList<String>();
task.srcFiles.add("Class1.java");
task.srcFiles.add("Class2.java");
String xml = VersionedDocument.fromBean(task).toXML();
saveXMLToFileOrDatabase(xml);
}

private static void saveXMLToFileOrDatabase(String xml) {
// save XML to file or database here
}
}
{code}
The resulting XML will be:
{code}
<example.CompileTask version="1.0">
<priority>HIGH</priority>
<srcFiles>
<string>Class1.java</string>
<string>Class2.java</string>
</srcFiles>
</example.CompileTask>
{code}
Pay attention to version attribute of the root element: XMT examines the class hierarchy (except for class _java.lang.Object_) to get current version of each class, and concatenates them with period. Since class _Task_ is of version "1" and class _CompileTask_ is of version "0", the resulting version of the hierarchy (or composite version) is "1.0".
When deserializing the compile task object from XML, XMT splits this composite version to get XML version for each class in the hierarchy, and repeats the process described in [getting started] chapter for each of these classes. So if class _Task_ is evolved to use numeric priority field, we simply add migrate methods in _Task_ class, while keep _CompileTask_ class intacted. On the other hand, if we evolve class _CompileTask_ to include a _destDir_ field, we can define the migrate method in _CompileTask_ like below, while keep class _Task_ intacted:
{code}
package example;

import java.util.List;
import java.util.Stack;

public class CompileTask extends Task {
public List<String> srcFiles;

public String destDir = "classes";

@SuppressWarnings("unused")
private void migrate1(VersionedDocument dom, Stack<Integer> versions) {
dom.getRootElement().addElement("destDir").setText("classes");
}
}
{code}
This separation of concerns is very important since there might exist many sub classes, and you certainly do not want to modify those sub classes if super class is evolved, and vice versa.

h2. Address class hierarchy change problem

Now we introduce class _AbstractCompileTask_ in the middle of _Task_ and _CompileTask_ like below:
{code}
package example;

public abstract class AbstractCompileTask extends Task {
public String options = "-debug";
}
{code}
{code}
package example;

import java.util.List;
import java.util.Stack;

public class CompileTask extends AbstractCompileTask {
public List<String> srcFiles;

public String destDir = "classes";

@SuppressWarnings("unused")
private void migrate1(VersionedDocument dom, Stack<Integer> versions) {
dom.getRootElement().addElement("destDir").setText("classes");
}
}
{code}
When deserialize from aforementioned "1.0" XML, XMT does the following:
# Split "1.0" into a versions list containing element "0" and "1" (version of sub class stored as first element). Current class is set to _CompileTask_.
# Remove the first element in the versions list to get version "0", compare it with current version of current class. This results in execution of method _migrate1_, with _versions_ param pointing to current versions list. After this step, current class is switched to the super class _AbstractCompileTask_.
# XMT continues to remove the first element in versions list, and get value of the removed element, which is now "1", and compare it with current version of current class. This results in a class mismatch since version "1" recorded in XML is derived from class _Task_, but the current class is _AbstractCompileTask_.

This issue can be solved by introducing another migrate method into class _CompileTask_:
{code}
package example;

import java.util.List;
import java.util.Stack;

public class CompileTask extends AbstractCompileTask {
public List<String> srcFiles;

public String destDir = "classes";

@SuppressWarnings("unused")
private void migrate1(VersionedDocument dom, Stack<Integer> versions) {
dom.getRootElement().addElement("destDir").setText("classes");
}

@SuppressWarnings("unused")
private void migrate2(VersionedDocument dom, Stack<Integer> versions) {
versions.push(0);
dom.addElement("options").setText("-debug");
}
}
{code}
This new migrate method inserts version "0" (current version of class _AbstractCompileTask_) into versions list, and the list now contains element "0", and "1" to handle data migration of class _AbstractCompileTask_ and _Task_ respectively. It also adds "options" element so that compile task objects deserialized from XML of old versions has the default compile options value of "-debug".

Now that we've successfully handled the case of adding new class into the hierarchy, but how about removing an existing class? Let's assume that class _Task_ is removed and class _AbstractCompileTask_ needs to take care of the priority field. In order to deserialize from XML of old versions, we need to write _AbstractCompileTask_ as below (assume class _Task_ is at version "2" when it is removed):
{code}
package example;

import java.util.Stack;
import com.pmease.commons.xmt.VersionedDocument;
import com.pmease.commons.xmt.MigrationHelper;

public class AbstractCompileTask {
public int priority;

public String options = "-debug";

@SuppressWarnings("unused")
private void migrate1(VersionedDocument dom, Stack<Integer> versions) {
int taskVersion = versions.pop();
MigrationHelper.migrate(String.valueOf(taskVersion), TaskMigrator.class, dom);
}

private static class TaskMigrator {

@SuppressWarnings("unused")
private void migrate1(VersionedDocument dom, Stack<Integer> versions) {
Element element = dom.getRootElement().element("prioritized");
element.setName("priority");
if (element.getText().equals("true"))
element.setText("HIGH");
else
element.setText("LOW");
}

@SuppressWarnings("unused")
private void migrate2(VersionedDocument dom, Stack<Integer> versions) {
Element element = dom.getRootElement().element("priority");
if (element.getText().equals("HIGH"))
element.setText("10");
else if (element.getText().equals("MEDIUM"))
element.setText("5");
else
element.setText("1");
}
}
}
{code}
The newly added migrate method first removes the first element from _versions_ param, which is the version intended to

When migrate from aforementioned "1.0" XML
With this modification, XMT will then follow below procedure to migrate from the aforementioned XML:
# It splits version "0.0" recorded in XML as a list, with the first element being version of _CompileTask_ and second element being version of _Task_.
# It