Some of posts from this blog has been moved to dywicki.pl. You will be automatically redirected to new blog if you would submit comment.
New posts are published on dywicki.pl, this blog contains old content and it is not continued.

Niektóre posty z tego bloga zostały przeniesione do dywicki.pl. Zostaniesz automatycznie przekierowany jeśli bedzięsz chciał dodać komentarz.
Nowe posty sa publikowane na dywicki.pl, ten blog zawiera stare treści i nie jest kontynuowany.

Rozszerzanie JAXB

Filed under Java,JAXB,XML by

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.
1
2
3
4
5
6
7
8
9
<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
1
2
3
4
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:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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.

4 responses so far

4 Responses to “Rozszerzanie JAXB”

  1. faramir says:

    a da się stworzyć taki plugin, który by mówił xjc, w jakiej stronie kodowej (encoding) tworzył pliki? U mnie na Windowsie niestety cp1250 wykorzystuje mimo zmiany JAVA_OPTS i innych…
    Męczę się z polskimi literami z jaxb i z mavenem razem.. :/

  2. Sposób na kodowanie w samym jaxb:
    Marshaller.html#JAXB_ENCODING

    Jeśli idzie o Mavena to:

    1
    MAVEN_OPTS=-Dfile.encoding=utf-8
  3. Krzysiek Marcinowicz says:

    Witam,
    ten przykład dał mi nadzieję, że uda się napisać plugin, który doda adnotacje pomagające przy walidacji wygenerowanych beanow. Już wyjaśniam o co chodzi, pisząc schema’e często używamy prostych typów, ale z restrykcjami, jak poniżej:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <xs:simpleType name="bytesType">
        <xs:restriction base="xs:string">
            <xs:pattern value="&#91;1-9&#93;\d*(B|kB|KiB|<acronym title="Megabyte">MB</acronym>|MiB|<acronym title="Gigabyte">GB</acronym>|GiB)?" />
        </xs:restriction>
    </xs:simpleType>
     
    <xs:simpleType name="levelType">
        <xs:restriction base="xs:unsignedByte">
            <xs:minInclusive value="1" />
            <xs:maxInclusive value="5" />
        </xs:restriction>
    </xs:simpleType>
     
    <xs:element name="traffic" type="bytesType" />
    <xs:element name="level" type="levelType" />

    Gdyby wygenerowany bean wygladal tak:

    1
    2
    3
    4
    5
    6
    7
    8
    @RegExp(pattern="[1-9]\d*(B|kB|KiB|<acronym title="Megabyte">MB</acronym>|MiB|<acronym title="Gigabyte">GB</acronym>|GiB)?")
    @... inne
    String traffic;
     
    @Min(value="1")
    @Max(value="5")
    @... inne
    int level;

    Można by użyć wygenerowanych adnotacji do walidacji – na takiej zasadzie działa Bean Validation Framework (implementacja jsr303). Czyli do odpowiedniego pola trzeba dodać odpowiednią adnotacje a jej wartość wydobyć ze schemy – i z tym mam problem. Może jest gotowa opowiedz, że przy pomocy JAXB nie można się do tego dobrać (mi się do tej pory nie udało), ale może ktoś (najbardziej liczę na autora bloga) mógł by podrzucić jakieś wskazówki.

  4. Już śpieszę z odpowiedzią – możliwe jest skorzystanie z gotowego projektu JAXB Validation.

    Jak głosi opis na stronie projektu:
    Z jaxbvalidation, będziesz dokładnie wiedział jaki problem, gdzie wystąpił. Zgłoszone błędy mogą być obsłużone programowo bądź zgłoszone ludzkim błędem (łącznie ze wsparciem internacjonalizacji).

    Myślę, że to będzie rozwiązanie, którego szukasz. :)
    Pozdrawiam,
    Łukasz

Leave a Reply

You must be logged in to post a comment.