When you work with JAXB to generate Java code from XSD (or *ghasp* WSDL) files, you’re going to use the
XJC tool, which is shipped with the JDK (at least until JDK 9 – with Jigsaw, it will soon be externalised into its own external dependency).
Adding plugins to XJC when running it via Maven is fairly straightforward. There are a few useful plugins available for free from here:
https://github.com/highsource/jaxb2-basics and from other sources. But if you’re not entirely happy with the results, you might need to roll your own.
In this article, we’ll look into how we can write a simple plugin to generate custom renditions of these methods:
toString()
equals()
hashCode()
Set up a project
First, we need to set up a new Maven project, which contains the plugin code. This is quite straightforward. Just work with a single dependency and you’re done:
<project xmlns="https://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.jooq</groupId>
<artifactId>jooq-tools-xjc-plugin</artifactId>
<version>3.11.0-SNAPSHOT</version>
<name>jOOQ XJC Code Generation Plugin</name>
<dependencies>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-xjc</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
</project>
Now, add the plugin logic
An empty plugin essentially looks like this:
package org.jooq.xjc;
import org.xml.sax.ErrorHandler;
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;
/**
* @author Lukas Eder
*/
public class XJCPlugin extends Plugin {
@Override
public String getOptionName() {
return "Xjooq-equals-hashcode-tostring";
}
@Override
public int parseArgument(Options opt, String[] args, int i) {
return 1;
}
@Override
public String getUsage() {
return " -Xjooq-equals-hashcode-tostring : xjc plugin";
}
@Override
public boolean run(Outline model, Options opt, ErrorHandler errorHandler) {
return true;
}
}
The important parts in our case are the
getOptionName() method, which provides a flag that can be used from XJC code generation configuration, to activate our plugin, and the
run() method, which will contain our code, adding the three desired methods.
Let’s fill in some actual code!
package org.jooq.xjc;
import static com.sun.codemodel.JMod.FINAL;
import static com.sun.codemodel.JMod.PUBLIC;
import static com.sun.codemodel.JMod.STATIC;
import java.util.Map.Entry;
import org.xml.sax.ErrorHandler;
import com.sun.codemodel.JBlock;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JConditional;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JOp;
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;
/**
* @author Lukas Eder
*/
public class XJCPlugin extends Plugin {
@Override
public String getOptionName() {
return "Xjooq-equals-hashcode-tostring";
}
@Override
public int parseArgument(Options opt, String[] args, int i) {
return 1;
}
@Override
public String getUsage() {
return " -Xjooq-equals-hashcode-tostring : xjc example plugin";
}
@Override
public boolean run(Outline model, Options opt, ErrorHandler errorHandler) {
JCodeModel m = new JCodeModel();
for (ClassOutline o : model.getClasses()) {
// toString()
// ---------------------------------------------------------------------------
{
JMethod method = o.implClass.method(PUBLIC, String.class, "toString");
method.annotate(Override.class);
JBlock body = method.body();
JClass sbType = m.ref(StringBuilder.class);
JVar sb = body.decl(0, sbType, "sb", JExpr._new(sbType));
for (Entry<String, JFieldVar> e : o.implClass.fields().entrySet()) {
JFieldVar v = e.getValue();
if ((v.mods().getValue() & STATIC) == 0) {
body.invoke(sb, "append").arg("<" + e.getKey() + ">");
body.invoke(sb, "append").arg(v);
body.invoke(sb, "append").arg("</" + e.getKey() + ">");
}
}
body._return(JExpr.invoke(sb, "toString"));
}
// equals()
// ---------------------------------------------------------------------------
{
JMethod method = o.implClass.method(PUBLIC, boolean.class, "equals");
method.annotate(Override.class);
JVar that = method.param(Object.class, "that");
JBlock body = method.body();
body._if(JExpr._this().eq(that))
._then()._return(JExpr.lit(true));
body._if(that.eq(JExpr._null()))
._then()._return(JExpr.lit(false));
body._if(JExpr.invoke("getClass").ne(JExpr.invoke(that, "getClass")))
._then()._return(JExpr.lit(false));
JVar other = body.decl(0, o.implClass, "other", JExpr.cast(o.implClass, that));
for (Entry<String, JFieldVar> e : o.implClass.fields().entrySet()) {
JFieldVar v = e.getValue();
if ((v.mods().getValue() & STATIC) == 0) {
if (v.type().isPrimitive()) {
body._if(v.ne(other.ref(v)))
._then()._return(JExpr.lit(false));
}
else {
JConditional i = body._if(v.eq(JExpr._null()));
i._then()._if(other.ref(v).ne(JExpr._null()))
._then()._return(JExpr.lit(false));
i._elseif(v.invoke("equals").arg(other.ref(v)).not())
._then()._return(JExpr.lit(false));
}
}
}
body._return(JExpr.lit(true));
}
// hashCode()
{
JMethod method = o.implClass.method(PUBLIC, int.class, "hashCode");
method.annotate(Override.class);
JBlock body = method.body();
JVar prime = body.decl(FINAL, m.INT, "prime", JExpr.lit(31));
JVar result = body.decl(0, m.INT, "result", JExpr.lit(1));
for (Entry<String, JFieldVar> e : o.implClass.fields().entrySet()) {
JFieldVar v = e.getValue();
if ((v.mods().getValue() & STATIC) == 0) {
body.assign(result, prime.mul(result).plus(
v.type().isPrimitive()
? v
: JOp.cond(v.eq(JExpr._null()), JExpr.lit(0), v.invoke("hashCode"))
));
}
}
body._return(result);
}
}
return true;
}
}
The above logic generates an XML document fragment of the JAXB-annotated classes (without formatting) and an
equals()
and
hashCode()
implementation that is inspired by the generated code you would obtain from an IDE like Eclipse.
Some example output:
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "MappedTable", propOrder = {
})
public class MappedTable {
protected String input;
@XmlElement(type = String.class)
protected String inputExpression;
@XmlElement(required = true)
protected String output;
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("<input>");
sb.append(input);
sb.append("</input>");
sb.append("<inputExpression>");
sb.append(inputExpression);
sb.append("</inputExpression>");
sb.append("<output>");
sb.append(output);
sb.append("</output>");
return sb.toString();
}
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null) {
return false;
}
if (getClass()!= that.getClass()) {
return false;
}
MappedTable other = ((MappedTable) that);
if (input == null) {
if (other.input!= null) {
return false;
}
} else {
if (!input.equals(other.input)) {
return false;
}
}
if (inputExpression == null) {
if (other.inputExpression!= null) {
return false;
}
} else {
if (!inputExpression.equals(other.inputExpression)) {
return false;
}
}
if (output == null) {
if (other.output!= null) {
return false;
}
} else {
if (!output.equals(other.output)) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = ((prime*result)+((input == null)? 0 :input.hashCode()));
result = ((prime*result)+((inputExpression == null)? 0 :inputExpression.hashCode()));
result = ((prime*result)+((output == null)? 0 :output.hashCode()));
return result;
}
}
Don’t forget to register your plugin
The last step prior to building your plugin is to create a file in your project at:
src/main/resources/META-INF/services/com.sun.tools.xjc.Plugin
And put the qualified name of your plugin in it:
org.jooq.xjc.XJCPlugin
Done. Now install your plugin…
mvn clean install
… and use it from your code generation configuration as follows:
<plugin>
<groupId>org.jvnet.jaxb2.maven2</groupId>
<artifactId>maven-jaxb2-plugin</artifactId>
<version>0.13.1</version>
<executions>
<execution>
<id>codegen</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<!-- The usual configuration -->
<encoding>UTF-8</encoding>
<locale>us</locale>
<forceRegenerate>true</forceRegenerate>
<extension>true</extension>
<strict>false</strict>
<schemaDirectory>../jOOQ-meta/src/main/resources/xsd</schemaDirectory>
<bindingDirectory>../jOOQ-meta/src/main/resources/xjb/codegen</bindingDirectory>
<generateDirectory>../jOOQ-meta/src/main/java</generateDirectory>
<generatePackage>org.jooq.util.jaxb</generatePackage>
<schemaIncludes>
<include>jooq-codegen-3.11.0.xsd</include>
</schemaIncludes>
<!-- activate it with this line. Must match getOptionName() -->
<args>
<arg>-Xjooq-equals-hashcode-tostring</arg>
</args>
<plugins>
<!-- include it with these lines. -->
<plugin>
<groupId>org.jooq.trial</groupId>
<artifactId>jooq-tools-xjc-plugin</artifactId>
<version>3.11.0-SNAPSHOT</version>
</plugin>
</plugins>
</configuration>
</execution>
</executions>
</plugin>
Done!
Like this:
Like Loading...
Xjc? Hey, it is 2018! :) But it is a nice idea.
So? Still a very useful utility :)
Thx for the post! Maybe jaxb/xjc is not full appreciate…We also need to remember the possibilities of binding files: source injection, parent class, base interface, properties renaming, type converter, sync method…all with xjc compiler by xjb files using xpath!…
Sure, they can help, too. Although the declarative nature of xjb files is much more limited than the possibility of programmatically generating arbitrary source code…