Plugin für XJC, Teil 2

Plugin für vereinfachte getter/setter in generierten JAXB Klassen

Problembeschreibung

Bei der Benutzung von generierten JAXB-Klassen kann es in einigen Situationen zu recht umständlichen getter/setter Konstruktionen kommen. Als Beispiel soll der folgende Auszug aus einer XSD dienen:

[code language=“xml“]
<xsd:complexType name="Bestellung">
<xsd:sequence>
<xsd:element name="BestellNummer" type="BestellNummer" />
<xsd:element name="ArtikelNummern">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="ArtikelNummer" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="ArtikelNummer">
<xsd:attribute name="value" type="xsd:string" />
</xsd:complexType>
[/code]

Das setzen der Artikelnummer gestaltete sich dann wie folgt:

[code language=“java“]
BestellNummer nummer = new BestellNummer();
nummer.setValue("foobar");
bestellung.setBestellNummer(nummer);
[/code]

und das Auslesen ist auch nicht einfacher:

[code language=“java“]
String bestNummer = null;
BestellNummer nummer = bestellung.getBestellNummer();
if (nummer != null) {
bestNummer = nummer.getValue();
}
[/code]

Schöner wäre es, wenn man den Wert direkt auslesen bzw. setzen kann:

[code language=“java“]
bestellung.setBestellNummer_("foobar");
bestNummer = bestellung.getBestellNummer_();
[/code]

Dafür müssen in der entsprechenden JAXB-Klasse folgenden Methoden vorhanden sein:

[code language=“java“]
public String getBestellNummer_() {
if (this.bestellNummer == null) {
return null;
} else {
return this.bestellNummer.getValue();
}
}
public void setBestellNummer_(String value) {
if (value == null) {
this.bestellNummer = null;
} else {
this.bestellNummer = new BestellNummer();
this.bestellNummer.setValue(value);
}
}
[/code]

Damit man diese Methoden nicht immer bei einer Schema-Generierung verloren gehen, werden sie von einem eigenen Plugin einfach mit generiert. Nach schöner wäre es, wenn wir die Erzeugung des Attributes plus Getter/Setter Methoden anpassen. Das XJC bietet dafür in Ansätzen einen Mechanismus, der aber ist leider nicht vernünftigt umgesetzt, denn die wichtigen Funktionen sind entweder private (z.b. das Erzeugen des richtigen getter Namens) oder aber wichtige Klassen haben nur package visibility und erzeugen schon im Konstruktor Code, sodass man nicht wirklich etwas anpassen kann. Bleibt nur noch die zusätzliche Erzeugung von gettern/settern (damit wir keinen Namenskonflikt bekommen, bekommen die Methoden ein „_“ ans Ende). Soweit die Vorrede.

Algorithmus

Für alle Attribute einer Klasse, die

  • als Typ eine von uns generierte Klasse haben (Target-Type)
  • deren Target-Typ genau ein Attribut (Target-Attribut) besitzt

generieren wir:

  • eine optimierte setter-Methode, wenn wir eine setter-Methode in der Target-Type-Klasse zum passenden Target-Attribut finden
  • eine optiierte getter-Methode, wenn wir eine getter-Methode in der Target-Type-Klasse zum passenden Target-Attribut finden

Helferlein

Als erstes nehmen wir das Plugin-Gerüst aus dem letzten Blog. Dann brauchen wir noch ein paar Helferlein, um unseren Algorithmus besser programmieren zu können.

Die folgende Methode berechnet das Prefix für die Getter-Methoden. Der Quelltext wurde inspiriert von der XJC Implementierung, sodass beide das gleiche Verhalten zeigen (schön wäre eine einheitliche Hilfsmethode gewesen, die konnte ich aber leider nicht finden).

[code language=“java“]
protected String getterPrefix(JType type) {
boolean useIs = false;
if (options.enableIntrospection) {
useIs = type.isPrimitive() == true && codeModel.BOOLEAN.equals(type.boxify().getPrimitiveType());
} else {
useIs = codeModel.BOOLEAN.equals(type.boxify().getPrimitiveType());
}
return (useIs == true ? "is" : "get");
}
[/code]

Wir wollen für alle Klassen, die genau ein Attribut haben „vereinfachte“ getter/setter erstellen, sodass wir für eine generierte Klasse prüfen müssen, ob diese genau ein Feld besitzt. im XJC werden generierte Klassen durch JDefinedClass dargestellt. Damit ist es einfach die einzige definierte Klassen-Variable herauszubekommen. Sollte die Klasse mehrere Variablen beinhalten geben wir null zurück.

[code language=“java“]
protected JFieldVar getSingleField(JDefinedClass cls) {
final Map<String, JFieldVar> map = cls.fields();
if (map.size() != 1) {
return null;
} else {
return map.values().iterator().next();
}
}
[/code]

Als nächstes benötigen wir zu einem Attribute einer Klasse, den jeweiligen Typ des Attributes. In XJC werden die Attribute einer Klasse als FieldOutline dargestellt. Zu einem Field müssen wir nun den entsprechenden Java-Typ heraus finden. Dabei interessieren wir uns nur für von XJC erzeugte Klassen. Für alle anderen Klassen wollen wir den Zugriff nicht vereinfachen. Finden wir keine Klasse, geben wir wieder null zurück.

[code language=“java“]
protected JDefinedClass mapFieldToDefinedClass(FieldOutline field) {
if (field.getRawType() instanceof JDefinedClass) {
return (JDefinedClass) field.getRawType();
}
return null;
}
[/code]

Zu einem FieldOutline benötigen wir die zugehörige Definition in der erzeugten Java Klasse. Diese suchen wir über den Namen des Felds. Sollten wir keine zugehörige Field-Definition finden, oder der Type stimmt nicht überein, geben wir null zurück.

[code language=“java“]
protected JFieldVar mapToFieldVar(FieldOutline field) {
Map<String, JFieldVar> map = field.parent().implClass.fields();
JFieldVar fieldVar = map.get(field.getPropertyInfo().getName(false));
if (fieldVar.type().equals(field.getRawType()) == true) {
return fieldVar;
} else {
return null;
}
}
[/code]

Als letztes benötigen wir noch eine Methode mit der wir die getter bzw. setter zu einem Feld innerhalb einer Java-Klasse finden können. Zum Suchen benötigen wir die Klasse, in der wir suchen wollen, als JDefinedClass, ein Prefix mit der die zu suchende Methode anfangen soll, der Rückgabe-Type als JType und die Signatur als JType[]. Wir geben nur dann einen JMethod zurück, wenn wir genau eine passende Methode finden. Ansonsten geben wir null zurück.

[code language=“java“]
protected JMethod searchMethod(JDefinedClass cls, String prefix, JType returnType, JType[] signatur) {
JMethod found = null;
for (JMethod m : cls.methods()) {
if (m.name().startsWith(prefix) == true // assume only the starting name
&& m.type().equals(returnType) == true //
&& m.hasSignature(signatur) == true) {
if (found != null) {
return null; // if we can not find a unique getter for the field, should never happen
} else {
found = m;
}
}
} // for
return found;
}
[/code]

Herzstück

Jetzt haben wir alle Bausteine zusammen mit der wir unsere optimierten getter/setter-Methoden erzeugen können. Wir werden jetzt die run Methode erweitern, sodass wir versuchen für alle uns bekannten Felder die optimierten Methoden zu erzeugen. Dafür rufen wir die process-Methode auf allen Feldern aller Klassen auf.

[code language=“java“]
public boolean run(Outline outline, Options opt, ErrorHandler errorHandler) throws SAXException {
System.out.println("run " + getOptionName() + " …");
this.codeModel = outline.getCodeModel();
this.options = opt;
for (ClassOutline cls : outline.getClasses()) {
for (FieldOutline field : cls.getDeclaredFields()) {
process( field);
}
}
System.out.println("run " + getOptionName() + " … done");
return true;
}
[/code]

Die process-Methode fürht jetzt folgende Schritte durch:
1) wir suchen eine Feld-Definition für das übergeben Feld, sollten wir keine finden können wir keine Methoden erzeugen

[code language=“java“]
JFieldVar $property = mapToFieldVar(field);
if ($property == null) {
return; // stop if we could not find a local field for given field outline
}
[/code]

2) dann ermitteln wir den Typen des gefunden Felds. Wir können nur weiter arbeiten, wenn der Type eine von uns erzeugte Klasse ist.

[code language=“java“]
JDefinedClass jTargetCls = mapFieldToDefinedClass(field);
if (jTargetCls == null) {
return; // stop if target class is not a generated one
}
[/code]

3) dann überprüfen wir, ob der gefunden Typ genau eine lokale Variable enthält. Sollte das nicht der Fall sein, können keine optimierten getter/setter erzeugt werden:

[code language=“java“]
final JFieldVar jFieldVar = getSingleField(jTargetCls);
if (jFieldVar == null) {
return; // no optimized getter if we do not have a unique target field
}
[/code]

4) damit eine optimierte getter-Methode erzeugt werden kann, müssen wir die entsprechende getter Methode auf dem Typen des lokalen Properties finden. Find wir eine, werden wir eine optimierte getter-Methode erzeugen

[code language=“java“]
JMethod targetGetter = searchMethod(jTargetCls, getterPrefix(jFieldVar.type()), jFieldVar.type(), null);
if (targetGetter != null) {
// if( this.property == null ) {
// return null;
// } else {
// return this.property.getV();
// }
JType type = targetGetter.type();
JMethod getter = field.parent().implClass.method(JMod.PUBLIC, type, getterPrefix(jTargetCls)
+ field.getPropertyInfo().getName(true) + "_");

JConditional $if2 = getter.body()._if(JExpr._this().ref($property).eq(JExpr._null()));
$if2._then() //
._return(JExpr._null());
$if2._else() //
._return(JExpr._this().ref($property).invoke(targetGetter));
} // endif getter
[/code]

5) für die optimierte setter-MEthode machen wir das gleiche

[code language=“java“]
JMethod targetSetter = searchMethod(jTargetCls, "set", codeModel.VOID, new JType[] { jFieldVar.type() });
if (targetSetter != null) {
// if( value == null ) {
// this.propert = null;
// } else {
// this.property = new …()
// this.property.set….(value)
// }
JType type = targetSetter.params().get(0).type();
JMethod setter = field.parent().implClass.method(JMod.PUBLIC, Void.TYPE, "set"
+ field.getPropertyInfo().getName(true) + "_");

JVar $newValue = setter.param(type, "value");
JConditional $if = setter.body()._if($newValue.eq(JExpr._null()));
$if._then() //
.assign(JExpr._this().ref($property), JExpr._null());
$if._else() //
.assign(JExpr._this().ref($property), JExpr._new(field.getRawType())) //
.invoke(JExpr._this().ref($property), targetSetter).arg($newValue);
} // endif setter
[/code]

Die so generierten setter/getter erlauben einen direkten Zugriff auf das Klassen-Feld der Ziel-Klasse. Auch werden beim Setzen die entsprechenden „Zwischen-Klassen“ erzeugt bzw. gelöscht. Beim Zugrif auf Listen wird nur eine optimierte getter-Methode erzeugt.

Als kleinen Nebeneffekt lernt man mit diesem Beispiel noch das Erzeugen von Java-Quelltext mit dem internen Java CodeModel, welches in der JAXB-Distribution sehr gut per JavaDoc dokumentiert ist.

Weitere Referenzen:

Schreibe einen Kommentar