Rozszerzanie JAXB

Z ostatnich not zebrało mi się kilka obietnic. Jedną z nich było omówienie pluginów w wydaniu JAXB. Jako, że dzisiaj urządziłem sobie wolny dzień postanowiłem wywiązać się przynajmniej z części obowiązków z tym i z tych blogowych.

Przykład, na którym do tej pory zawsze bazowałem to dodanie wsparcia dla obsługi zdarzeń z użyciem PropertyChangeSupport. Jest to przykład o tyle wygodny, że pojawiają się przy nim nowe pole, przynajmniej dwie nowe metody. W grę wchodzi również wybiórcza modyfikacja metod, które zadeklarował sobie już JAXB. Z drugiej strony przykład ten jest o tyle niezręczny, że jest to już fragment jaxb-commons. Jakkolwiek ciężko mi wymyślić coś bardziej kreatywnego co by odnosiło się do ogółu generowanego kodu.

Mamy już pomysł, teraz zatem kolej na kod. Pluginy są podpinane przez XJC i są odpalane po procesie parsowania schematu XML. Krok po kroku co należy zrobić.

  • Stworzyć klasę rozszerzającą com.sun.tools.xjc.Plugin
  • Stworzyć jar, który będzie zawierał plugin (chociażby skryptem ant). Poniższy przykład pakuje wszystko z bin_plugin oraz katalog META-INF.
<project name="plugin" default="jar">
  <target name="jar">
    <delete file="plugin.jar" />
    <jar destfile="plugin.jar">
      <fileset dir="bin_plugin" />
      <fileset file="./*" excludes="*" includes="META-INF/**" />
    </jar>
  </target>
</project>
  • W paczce (jar) należy należy stworzyć plik META-INF/services/com.sun.tools.xjc.Plugin
  • W pliku tym należy umieścić listę kolejnych pluginów - to znaczy wymienić pełne nazwy klas, po kolei każdą w nowej linii
com.sun.tools.xjc.addon.locator.SourceLocationAddOn
com.sun.tools.xjc.addon.sync.SynchronizedMethodAddOn
com.sun.tools.xjc.addon.at_generated.PluginImpl
pl.dywicki.plugin.PropertyChangePlugin

Mając tak przygotowane archiwum uruchamiamy xjc:

xjc -cp nasz.jar -extension -nasz\_plugin schemat.xsd

Kilka słów wyjaśnienia -cp to class path, -extension włącza pluginy, a -nasz_plugin informuje xjc, że chcemy uruchomić plugin, który legitymuje się taką dyrektywą.

Po tych wszystkich karkołomnych zabiegach możemy spokojnie zabrać się za kodowanie..

package pl.dywicki.plugin;

import java.beans.PropertyChangeSupport;

import javax.xml.bind.annotation.XmlTransient;

import org.xml.sax.ErrorHandler;

import com.sun.codemodel.JBlock;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JFieldRef;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JInvocation;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JVar;
import com.sun.tools.xjc.Options;
import com.sun.tools.xjc.Plugin;
import com.sun.tools.xjc.outline.ClassOutline;
import com.sun.tools.xjc.outline.Outline;

/**
 * Plugin dorzucający do wygenerowanych klas obsługę powiadamiania o zmianach
 *
 * @author Łukasz Dywicki
 */
public class PropertyChangePlugin extends Plugin {

    @Override
    public String getOptionName() {
        // nasz plugin będzie uruchomiony poprzez dyrektywę -Xlistener
        return "Xlistener";
    }

    @Override
    public String getUsage() {
        // help dla użytkownika
        return " -Xlistener propagowanie zdarzeń przy pomocy standardów JavaBeans";
    }

    @Override
    public boolean run(Outline outline, Options opt, ErrorHandler errorHandler) {
        // a to jest najważniejsza część pluginu :)

        // przeglądamy wszystkie dostępne klasy
        for (ClassOutline co : outline.getClasses()) {
            // definicja pojedyńczej klasy, przy pomocy JDefinedClass
            // możemy dodawać nowe własności etc
            JDefinedClass clazz = co.implClass;

            addField(clazz);

            changeMethods(clazz);
        }
        return true;
    }

    /**
     * Dodanie pola do generowanej klasy.
     *
     * @param clazz Klasa do której dorzucamy pole
     */
     private void addField(JDefinedClass clazz) {
         // tworzymy w wygenerowanej klasie pole
         // private PropertyChangeSupport support = new PropertyChangeSupport(this);
         JFieldVar $field = clazz.field(JMod.PRIVATE | JMod.TRANSIENT, PropertyChangeSupport.class, "support");
         // inicjujemy pole
         JInvocation _new = JExpr._new($field.type());
         // dodajemy argument (this)
         _new.arg(JExpr._this());
         $field.init(\_new);
         // dodanie adnotacji @XmlTransient do pola, aby nie było ono
         // obsługiwane przez JAXB
         $field.annotate(XmlTransient.class);
     }

    /**
     * Modyfikacja metod set\*..
     *
     * @param clazz Klasa którą "przerabiamy".
     */
     private void changeMethods(JDefinedClass clazz) {
         for (JMethod method : clazz.methods()) {
             if (method.name().startsWith("set")) {
                 String tmp = method.name().substring(3);
                 String fieldName = tmp.substring(0, 1).toLowerCase() + tmp.substring(1);
                 addSupport(method, fieldName);
             }
         }
     }

    /**
     * Zmodyfikowanie istniejących metod.
     *
     * @param method Obiekt metody
     * @param fieldName Nazwa pola, które metoda obsługuje
     */
     private void addSupport(JMethod method, String fieldName) {
         // przeskakujemy do pierwszej linijki w metodzie
         JBlock body = method.body();
         body.pos(0);
         // odwoładnie do np this.userName
         JFieldRef ref = JExpr.ref(fieldName);
         // deklaracja zmiennej o typie pierwszego argumentu i nazwie temp
         JVar $temp = body.decl(method.listParams()[0].type(), "temp");
         // przypisanie do zmiennej temp wartości pola
         $temp.init(JExpr.ref(fieldName));

         // przeskoczenie na koniec metody
         body.pos(body.getContents().size());

         // odwołanie do pola this.support (stworzone w metodzie addField) i
         // wywołanie metody this.support.firePropertyChange()
         JInvocation invocation = JExpr.ref("support").invoke("firePropertyChange");
         // kolejne argumenty metody - nazwa property, stara wartość i nowa wartość
         invocation.arg(fieldName);
         invocation.arg($temp);
         invocation.arg(ref);

         // dorzucenie wywołania na końcu metody
         body.add(invocation);
     }

}

Kosztuje to troszkę pracy, ale koniec końców mamy wsparcie dla obsługi zdarzeń. Dla zainteresowanych dołączam jeszcze dwa pluginy - generujące metody hashCode i equals. Napisałem je, gdy nie miałem dostępu do internetu, jeśli chcecie ich używać - zalecam wcześniej wspomniane jaxb-commons.