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

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:

    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:

    <xs:simpleType name="bytesType">
        <xs:restriction base="xs:string">
            <xs:pattern value="&#91;1-9&#93;\d*(B|kB|KiB|MB|MiB|GB|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:

    @RegExp(pattern="[1-9]\d*(B|kB|KiB|MB|MiB|GB|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