Nov 28 2007
Rozszerzanie JAXB
Filed under Java,JAXB,XML by Łukasz Dywicki
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.
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.. :/
Sposób na kodowanie w samym jaxb:
Marshaller.html#JAXB_ENCODING
Jeśli idzie o Mavena to:
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:
Gdyby wygenerowany bean wygladal tak:
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.
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