Tagged “java”

Eclipse: Keine Bevormundung durch Ubuntu

Published by cybso on

This is a post from my original site, which was hosted by the former blog service of the University of Osnabrück. I have moved it to the new site for archiving. Pages linked in this article may no longer work today, and the blog comments under the article no longer exist. Opinions expressed in this article reflect the point of view of the time of publication and do not necessarily reflect my opinion today.

Unter Ubuntu fragt Eclipse beim Starten nicht, welcher Workspace verwendet werden soll, sondern wählt immer $HOME/workspace - selbst wenn in den Einstellungen explizit festgelegt wurde, dass er fragen soll.

Woran das genau liegt, kann ich nicht sagen, aber es hat wohl was mit der verwendeten Java-VM zu tun, der Standard ist java-gcj. Umgehen lässt sich dieses Problem, indem die Pakete sun-java6-jdk bzw. sun-java6-jre installiert werden und die Verwendung dieser mithilfe der Umgebungsvariablen JAVA_HOME erzwungen wird:

JAVA_HOME=/usr/lib/jvm/java-6-sun eclipse

Oder dauerhaft, indem man diese in die Datei $HOME/.profile einträgt:

export JAVA_HOME=/usr/lib/jvm/java-6-sun

Nun erscheint bei jedem Start von Eclipse wie gewohnt der "Workspace auswählen"-Dialog.

Nachtrag 10.12.2008:

Der richtige Ort, um diese Änderung global festzulegen, ist /etc/environment:

PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games"
JAVA_HOME=/usr/lib/jvm/java-6-sun

Zusätzlich gibt es unter https://help.ubuntu.com/community/Java eine kleine Anleitung, wie man die vom System verwendete Java-VM ändern kann.

Let XStream call the default constructor where possible

Published by cybso on

This is a post from my original site, which was hosted by the former blog service of the University of Osnabrück. I have moved it to the new site for archiving. Pages linked in this article may no longer work today, and the blog comments under the article no longer exist. Opinions expressed in this article reflect the point of view of the time of publication and do not necessarily reflect my opinion today.

XStream is a nice Java library for serializing and deserializing objects. One of it's advantages is that it does not require the deserialized class to have a default constructor. But sometimes this will be a problem. A simple real-life example:

public class Person {

    public transient final PropertyChangeSupport pcs = new PropertyChangeSupport(this);

    private String name = "";

    public String getName() {
        return name;
    }

    public void setPerson(Name name) {
        pcs.firePropertyChange("name", this.name, this.name = name);
    }
}

The PropertyChangeSupport object should be marked transient, otherwise serializing would include the whole object, including its listeners. But sadly, he following code won't work:

XStream xstream = new XStream();
Person p = new Person();
p.setName("Roland");
String serialized = xstream.toXML(p);
// ...
p = xstream.fromXML(serialized);
System.out.println(p.getName()); // prints "Roland"
p.setName("Cybso"); // Throws NullPointerException in Person.setName(Name)

The call to p.setName("Cybso") throws a NullPointerException because pcs has not been initialized.

There are two standard ways to work around this problem. The first is to initialize XStream using a PureJavaReflectionProvider-Instance:

XStream xstream = new XStream(new PureJavaReflectionProvider());

This would force XStream create new objects using Class.newInstance() - and prevents you from (de)serializing classes without default constructor. The other way is to implement a method called readResolve() which will be called after the object has been created:

public class Person {

    public transient final PropertyChangeSupport pcs;

    private String name;

    public Person() {
        readResolve();
    }

    public void readResolve() {
        pcs = new PropertyChangeSupport(this);
        name = "";
    }

    public String getName() {
        return name;
    }

    public void setPerson(Name name) {
        pcs.firePropertyChange("name", this.name, this.name = name);
    }
}

This means to abandon the using of final transient fields and in this special case it enforces the implementation of a getPCS() or delegation methods. So let me suggest another solution: Create a custom converter that feels responsible for all classes having a default constructor. This reduces the final transient problem to classes without a default constructor.

public static class DefaultConstructorConverter extends ReflectionConverter {
    public DefaultConstructorConverter(Mapper mapper, ReflectionProvider reflectionProvider) {
        super(mapper, reflectionProvider);
    }

    @Override
    public boolean canConvert(Class clazz) {
        for (Constructor c : clazz.getConstructors()) {
            if (c.getParameterTypes().length == 0) {
                return true;
            }
        }
        return false;
    }

    @Override
    protected Object instantiateNewInstance(HierarchicalStreamReader reader, UnmarshallingContext context) {
        try {
            Class clazz = Class.forName(reader.getNodeName());
            return clazz.newInstance();
        } catch (Exception e) {
            throw new ConversionException("Could not create instance of class " + reader.getNodeName(), e);
        }
    }
}

Using this converter the original code will work:

XStream xstream = new XStream();
xstream.registerConverter(new DefaultConstructorConverter(xstream.getMapper(), xstream.getReflectionProvider()));
Person p = new Person();
p.setName("Roland");
String serialized = xstream.toXML(p);
//...
p = xstream.fromXML(serialized);
System.out.println(p.getName()); // prints "Roland"
p.setName("Cybso"); // No exception thrown!
System.out.println(p.getName()); // prints "Cybso"

Happy hacking ;)

Java: Howto embed SWT widget into Swing JFrame

Published by cybso on

This is a post from my original site, which was hosted by the former blog service of the University of Osnabrück. I have moved it to the new site for archiving. Pages linked in this article may no longer work today, and the blog comments under the article no longer exist. Opinions expressed in this article reflect the point of view of the time of publication and do not necessarily reflect my opinion today.

Today I wanted to embed a SWT component (Browser) into an existing JFrame. This is the way it works:

(continue reading)

Java: Model-View-Controller without memory leaks

Published by cybso on

This is a post from my original site, which was hosted by the former blog service of the University of Osnabrück. I have moved it to the new site for archiving. Pages linked in this article may no longer work today, and the blog comments under the article no longer exist. Opinions expressed in this article reflect the point of view of the time of publication and do not necessarily reflect my opinion today.

When doing MVC programming in Java, there is a problem that most people don't know about. I've ignored it myself much too long. The problem is that when you bind a model class to an UI component you will get a giant memory leak.

What happens?

Well, imagine a model class supporting listening for property changes. A simple example might look like this (I extend from PropertyChangeSupport here so that I don't have to delegate all the methods, normally you wouldn't do so, of course):

public class Model extends PropertyChangeSupport {
    private String name;
    public Model(String name) {
        super(new Object()); // Only an example! Don't do this in RL!
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        firePropertyChange("name", this.name, this.name = name);
    }
}

Next, imagine a view for this model class. It has an input field that is binded in both direction with the model (very simplified example here, I normally don't code this way ;-)):

public class ModelView extends JPanel {
    public ModelView(final Model model) {
        super(new BorderLayout());
        add(new JLabel("Name: "), BorderLayout.WEST);

        final JTextField textfield = new JTextField(model.getName());
        add(textfield, BorderLayout.CENTER);

        // Bind textfield => model (harmless)
        textfield.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                model.setName(textfield.getText());
            }
        });

        // Bind model => textfield (introduces memory leak)
        model.addPropertyChangeListener("name", new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent e) {
                textfield.setText(model.getName());
            }
        });
    }
}

From now on we have a memory leak, because the JPanel, the JLabel and the JTextField (and all other referenced) objects will never be removed by the garbage collector as long as the model exists (which might be until the applications end of life). The reason is that the anonymous inner PropertyChangeListener instance has (and must have!) an implicit reference to the JPanel. And even if you replace it by a static class it does have to know the textfield which is a child object of the panel and has a reference back to it. So even if nobody else references the panel the garbage collector sees:

  Model => PropertyChangeListener => JPanel

There is a little known class called WeakReference (and his brother WeakHashMap). It contains an object, but the object could still be removed by the garbage collector as long as there is no other (non-weak) reference to it.

A naive idea is to just encapsulate the PropertyChangeListener within a WeakReference object:

public class WeakListener implements PropertyChangeListener {
    private final WeakReference<PropertyChangeListener> listener;
    public WeakListener(PropertyChangeListener listener) {
        this.listener = new WeakReference<PropertyChangeListener>(listener);
    }

    public void propertyChange(PropertyChangeEvent e) {
        PropertyChangeListener l = this.listener.get();
        if (l != null) {
            l.propertyChange(e);
        }
    }
}

Code in ModelView constructor:

// Bind model => textfield (with little memory leak)
model.addPropertyChangeListener("name", new WeakListener(new PropertyChangeListener() {
    public void propertyChange(PropertyChangeEvent e) {
        textfield.setText(model.getName());
    }
}));

The idea is that you still have this simple and small WeakListener object within your model's listener list, but the really big part (the original listener, the panel, label, textfield, etc) could be free'd. Sadly, this will not work, because as soon as you gave the anonymous PropertyChangeListener to WeakListener's constructor nobody else does reference to it, so the garbage collector will remove the object immediately.

The trick is to create a single reference to it which belongs the parent object:

public class ModelView extends JPanel {
    private PropertyChangeListener modelTextfieldListener;

    public ModelView(final Model model) {
        super(new BorderLayout());
        add(new JLabel("Name: "), BorderLayout.WEST);

        final JTextField textfield = new JTextField(model.getName());
        add(textfield, BorderLayout.CENTER);

        // Bind textfield => model (harmless)
        textfield.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                model.setName(textfield.getText());
            }
        });

        // Bind model => textfield (with little memory leak)
        model.addPropertyChangeListener("name", new WeakListener(modelTextfieldListener = new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent e) {
                textfield.setText(model.getName());
            }
        }));
    }
}

That's it. To make this easier I've created a little support class WeakListenerSupport that's very helpful if you have to deal with multiple input-property-bindings. Also it tries to remove ("unlink") itself from the model if the listener doesn't exist any longer. It even allows to unlink all weak listeners from a single object.

With this class the above example would look like:

public class ModelView extends JPanel {
    private final WeakListenerSupport wls = new WeakListenerSupport();

    public ModelView(final Model model) {
        super(new BorderLayout());
        add(new JLabel("Name: "), BorderLayout.WEST);

        final JTextField textfield = new JTextField(model.getName());
        add(textfield, BorderLayout.CENTER);

        // Bind textfield => model (harmless)
        textfield.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                model.setName(textfield.getText());
            }
        });

        // Bind model => textfield (without memory leak)
        model.addPropertyChangeListener("name", wls.propertyChange(new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent e) {
                textfield.setText(model.getName());
            }
        }, model));
    }
}

JTextPane with background color

Published by cybso on

This is a post from my original site, which was hosted by the former blog service of the University of Osnabrück. I have moved it to the new site for archiving. Pages linked in this article may no longer work today, and the blog comments under the article no longer exist. Opinions expressed in this article reflect the point of view of the time of publication and do not necessarily reflect my opinion today.

JTextPane and his ancestors JEditorPane and JTextComponent won't respect the color defined with setBackground(Color) since they display a "styled document" and expect the background color to be defined in the content. You'll always see a white background.

To change the background color (without modifying the content) you have to define attributes for the document:

JTextPane textPane = new JTextPane();
textPane.setContentType("text/html"); // or any other styled content type
textPane.setText("White text on a red background");

textPane.setForeground(Color.white); // Works as expected
textPane.setBackground(Color.red); // Obsolete, no affect

// Define a default background color attribute
Color backgroundColor = Color.red;
SimpleAttributeSet background = new SimpleAttributeSet();
StyleConstants.setBackground(background, backgroundColor);
textPane.getStyledDocument().setParagraphAttributes(0,
        textPane.getDocument().getLength(), background, false);

// And remove default (white) margin
textPane.setBorder(BorderFactory.createEmptyBorder());

// Alternative: Leave a 2px border but draw it in the same color
textPane.setBorder(BorderFactory.createLineBorder(backgroundColor, 2));

Evaluate structured code in JasperReports

Published by cybso on

This is a post from my original site, which was hosted by the former blog service of the University of Osnabrück. I have moved it to the new site for archiving. Pages linked in this article may no longer work today, and the blog comments under the article no longer exist. Opinions expressed in this article reflect the point of view of the time of publication and do not necessarily reflect my opinion today.

JasperReports is a library which can be used to fill reports from Java applications or just create simple PDFs. It allows you to not only use static output strings but also Groovy expressions. Sadly, this is restricted to simple expressions that result in a value and don't generate multiple class files at compile time.

For example, you could use the following expression to print different values depending if your document has more or less than 10 pages:

$V{PAGE_COUNT} < 10 ? "foo" : "bar"

But when you have to loop over values you'll face a problem as you have no possibility to define own methods. Even the usage of closures (Groovy) or anonymous inner classes (Java) is prohibited as Jasper expects every expression to result in one single class file.

Of course you could extend the class path or use Scriptlets for this, but this requires you to ship the compiled class together with the report library.

Using the power of groovy there is a way to work around this: compile your expression at runtime! The following code will calculate the faculty of the number of pages:

new GroovyClassLoader().parseClass('''
    def static fac(x) {
        def res = 1;
        1.upto(x) {
            res *= it
        }
        return res
    }
''').fac($V{PAGE_COUNT})

The first line creates a new instance of the GroovyClassLoader, parses the code given in the following multiline string expression and executes the method "fac(x)" defined statically before.

To prove the power of this method the following code will embed a recursive listing of your home directory in your report:

new GroovyClassLoader().parseClass('''
    def static list(path, prefix) {
        String result = ""
        path.listFiles().each() {
            result += prefix + it.name + "\\n"
            result += list(it, prefix + "  ")
        }
        return result
    }
''').list(new File(System.getProperty("user.home")), "")

Remember that you have to double-escape backslashes as the first escape will be handles by the Jasper compiler.

Another possibility (and the reason why I researched at this topic) is that this method allows you to generate images at runtime and use the full power of the JFreeChart library used by Jasper itself.

Java: process http.proxyUser and http.proxyPassword

Published by cybso on

This is a post from my original site, which was hosted by the former blog service of the University of Osnabrück. I have moved it to the new site for archiving. Pages linked in this article may no longer work today, and the blog comments under the article no longer exist. Opinions expressed in this article reflect the point of view of the time of publication and do not necessarily reflect my opinion today.

Some tutorials suggest to use the system properties http.proxyUser and http.proxyPassword to get proxy authentication, but that won't work since - in contrast to http.proxyHost and http.proxyPort - these properties will not be processed by Java's HttpURLConnection.

Other suggest to use a custom default Authenticator. But that's dangerous because this would send your password to anybody who asks.

The following snippet contains some code that uses an Authenticator to process http.proxyUser, but ensures that these information will be sent to the host that is defined by http.proxyHost:

// Java ignores http.proxyUser. Here come's the workaround.
Authenticator.setDefault(new Authenticator() {
    @Override
    protected PasswordAuthentication getPasswordAuthentication() {
        if (getRequestorType() == RequestorType.PROXY) {
            String prot = getRequestingProtocol().toLowerCase();
            String host = System.getProperty(prot + ".proxyHost", "");
            String port = System.getProperty(prot + ".proxyPort", "");
            String user = System.getProperty(prot + ".proxyUser", "");
            String password = System.getProperty(prot + ".proxyPassword", "");

            if (getRequestingHost().toLowerCase().equals(host.toLowerCase())) {
                if (Integer.parseInt(port) == getRequestingPort()) {
                    // Seems to be OK.
                    return new PasswordAuthentication(user, password.toCharArray());  
                }
            }
        }
        return null;
    }  
});

JasperReports: Append an In-Report ToC without Scriptlets

Published by cybso on

Whenever you want to add a Table of Contents to your Jasper Report you have a problem, because there is no build-in function to do this. Most solutions suggest to derive an own Scriptlet class, use subreports or provide a custom data source.

Let me add another quick & dirty solution that requires no external resources, not even the Groovy library. It works with pure embedded Java code, at least as long you're ok with the very simplified layout of this solution, and do not require to have the ToC at the beginning of your report. And furthermore, it is pretty simple.

First, create a new parameter of type java.lang.StringBuilder:

<parameter name="toc" class="java.lang.StringBuilder" isForPrompting="false">
	<defaultValueExpression><![CDATA[new StringBuilder()]]></defaultValueExpression>
</parameter>

This parameter called toc will be filled with the table of content during the rendering of the detail sections. You may add markup if you want to.

Next, on the summary page, print the content of this parameter. I'll use HTML formated strings, here:

<statictext>
	<reportElement x="0" y="0" width="540" height="20"/>
	<textElement verticalAlignment="Middle">
		<font size="14" isBold="true" isItalic="true"/>
	</textElement>
	<text><![CDATA[Table of Contents]]>
</staticText>
<textField isStretchWithOverflow="true">
	<reportElement x="28" y="30" width="471" height="20"/>
	<textElement markup="html">
		<font fontName="Monospaced" size="8"/>
	</textElement>
	<textFieldExpression><![CDATA[$P{toc}.toString()]]></textFieldExpression>
</textField>

Maybe you noticed that I use a monospace font here. This will be explained later.

Whereever you want to have a new entry (e.g. in the detail band or within a group header) add a new text field and put the following expression into it:

$P{toc}.append(
	$F{section}.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
		+ "<font color='#999999'>"
			/* Fill the string with up to 80 dots */
			+ new String(new char[80 - Math.min($F{section}.length(), 80)]).replaceAll(".", ".")
		+ "</font>"
		+ $V{PAGE_NUMBER}
		+ "<br>"
) == null ? "" : ""

This works since StringBuilder.append returns an object (itself). It won't be visible since the expression will result in an empty string in any case.

In this example, I want to use the field "section" as an index. Because the resulting string will be formatted using html markup we have to escape "<" as "&lt;", ">" as "&gt;" and "&" as "&amp;". In the next lines, you see the reason why I decided to use a monospace font for the ToC: the section name should be followed by up to 80 grey dots and the page number.

Since you cannot use loops within JasperReport's expressions (at least not without Groovy) I used the trick to create a new String containing the required number of 0x0 characters and replace all characters (the first parameter of String.replaceAll is a regular expression) with dots.

Annotation based migrations for XStream

Published by cybso on

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;
    }
    
    public Object fromXML(File in) throws JDOMException, IOException {
        return fromXML(builder.build(in));
    }
    
    public Object fromXML(URL in) throws JDOMException, IOException {
        return fromXML(builder.build(in));
    }
    
    public Object fromXML(String in) throws JDOMException, IOException {
        return fromXML(builder.build(in));
    }
    
    public Object fromXML(Reader in) throws JDOMException, IOException {
        return fromXML(builder.build(in));
    }
    
    public Object fromXML(InputStream in) throws JDOMException, IOException {
        return fromXML(builder.build(in));
    }
    
    public Object fromXML(Document doc) {
        return fromXML(doc.getRootElement());
    }
    
    public Object fromXML(Element rootElement) {
        String version = rootElement.getAttributeValue("version", "0");
        JDom2Reader reader = new JDom2Reader(rootElement);
        Class<?> clazz = HierarchicalStreams.readClassType(reader, xstream.getMapper());
        migrate(clazz, rootElement, Integer.parseInt(version));
        return xstream.unmarshal(reader);
    }
    
    public void toXML(Object obj, OutputStream out) throws IOException {
        Element rootElement = new Element("container");
        xstream.marshal(obj, new JDom2Writer(rootElement));
        rootElement.setAttribute("version", "" + getVersion(obj.getClass()));
        Document doc = new Document(rootElement.getChildren().get(0).detach());
        this.writer.output(doc, out);
    }

    private int getVersion(Class<?> clazz) {
        int maxVersion = 0;
        for (Method m : clazz.getDeclaredMethods()) {
            Migration migration = m.getAnnotation(Migration.class);
            if (migration != null) {
                int version = migration.version();
                if ((m.getModifiers() & Modifier.STATIC) != 0) {
                    Class<?>[] params = m.getParameterTypes();
                    if (params.length == 1 && params[0].isAssignableFrom(Element.class)) {
                        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;
    }

    private void migrate(Class<?> clazz, Element rootElement, int version) {
        Map<integer, method=""> methods = new TreeMap<integer, method="">();
        for (Method m : clazz.getDeclaredMethods()) {
            if ((m.getModifiers() & Modifier.STATIC) != 0) {
                Migration migration = m.getAnnotation(Migration.class);
                if (migration != null) {
                    int v = migration.version();
                    if (v > version) {
                        Class<?>[] params = m.getParameterTypes();
                        if (params.length == 1 && params[0].isAssignableFrom(Element.class)) {
                            Method oldMethod = methods.put(v, m);
                            if (oldMethod != null) {
                                throw new RuntimeException("In class " + clazz.getName() + ": Duplicate migration versions defined for " + m.toGenericString() + " and " + oldMethod.toGenericString());
                            }
                        }
                    }
                }
            }
        }
        
        // Invoke all methods
        for (Method m : methods.values()) {
            try {
                m.invoke(null, rootElement);
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                LOGGER.error("Failed to invoke migration " + m.toGenericString(), e);
            }
        }
    }
    
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public static @interface Migration {
        int version();
    }
}