Skip to content
Robin Drew edited this page Nov 16, 2023 · 14 revisions

Welcome to my Java code generator project. There are three steps to generating code:

  • Writing the setup file
  • Writing the source files
  • Running the code generator

The Setup File

The first step is to write a setup file. You only write one per code generator. The purpose of this file is to:
  • Specify all code generation source files (xml)
  • Specify the target directories for the generated code
<Setup>

  <Source name="user.xml" type="RESOURCE" />
  <Source name="data.xml" type="RESOURCE" />

  <Target name="USER" directory="target/test/java/user" />
  <Target name="DATA" directory="target/test/java/data" />

</Setup>

The source files contain the generated code definitions. They are read as FILE or RESOURCE in the usual way. The target directories are possible destinations for generated code. Each source file specifies the target to use (by its name).

The Source Files

The source files are xml files containing any number of bean, enum, interface, etc... definitions as you wish. The definitions are loaded in such a way that the order they reference each other does not matter, even between separate source files. The root element in each xml file is called Model, and has the following attributes:
  • The id attribute associates a unique id with each object generated, and should be significantly different in each source file
  • The package attribute is applied to all definitions in the file
  • The target attribute must reference a target named in the setup file, and specifies the directory to write code to
<Model id="2000" package="com.generated.data" target="DATA">

... beans, enums, interfaces, etc ...

</Model>

Running the Code Generator

Running the code generator is a short piece of code. It requires just two arguments, the setup file and an ExecutorService. The generator can perform a number of operations in parallel, so it is worth providing a degree of concurrency.
ISetup setup = new SimpleResourceReader().read("setup.xml", Setup.class);
ExecutorSerivce service = Executors.newFixedThreadPool(10);

JavaModelGenerator generator = new JavaModelGenerator(setup, service);
generator.generate();

Generated Types

The following types can be generated by this API:
  • Beans - plain old java objects
  • Enums - enum objects, with support for simple fields
  • Interfaces - standard interfaces
  • Aliases - alias fully qualified class names to simple names for clarity
  • Validators - provide validation for arguments and fields
  • Comparators - generate comparators to compare one object to another
  • Adaptors - generate adaptors to copy one object to another
  • Executable Beans - beans that represent remote methods
  • Data Stores - database and memory storage and access

There are a few important notes regarding the usage of this API:

  • Generics - simple usage of generics is fully supported
  • Naming - all types and aliases must be uniquely named

Beans

A Basic Bean

The bean is a simple object with fields, contructors, getters and setters.

XML Source

<Bean name="Person">
  <Field name="name" type="String" />
  <Field name="dateOfBirth" type="Date" />
</Bean>

Generated Code

public class Person implements IPerson {

   private String name = null;
   private Date dateOfBirth = null;
   
   public Person() {
   }

   public Person(String name, Date dateOfBirth) {
      setName(name);
      setDateOfBirth(dateOfBirth);
   }

   public Person(IPerson clone) {
      setName(clone.getName());
      setDateOfBirth(clone.getDateOfBirth());
   }

   public String getName() {
      return name;
   }

   public Date getDateOfBirth() {
      return dateOfBirth;
   }

   public void setName(String name) {
      this.name = name;
   }

   public void setDateOfBirth(Date dateOfBirth) {
      this.dateOfBirth = dateOfBirth
   }
}

Equals, HashCode & ToString

All beans also contain a simple implementation of the equals(), hashCode() and toString() methods.

Generated Code

@Override
public int hashCode() {
   HashCodeBuilder builder = new HashCodeBuilder();
   builder.append(getName());
   builder.append(getDateOfBirth());
   return builder.toHashCode();
}

@Override
public boolean equals(Object object) {
   if (object == this) {
      return true;
   }
   if (!this.getClass().equals(object.getClass())) {
      return false;
   }
   IPerson compare = (IPerson) object;
   EqualsBuilder builder = new EqualsBuilder();
   builder.append(this.getName(), compare.getName());
   builder.append(this.getDateOfBirth(), compare.getDateOfBirth());
   return builder.isEquals();
}

@Override
public String toString() {
   ToStringBuilder builder = new new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
   builder.append(getName());
   builder.append(getDateOfBirth());
   return builder.toString();
} 

A Comparable Bean

By default the bean will be made Comparable - adding a compareTo() method.

Generated Code

@Override
public int compareTo(IPerson compare) {
   CompareToBuilder builder = new CompareToBuilder();
   builder.append(this.getName(), compare.getName());
   builder.append(this.getDateOfBirth(), compare.getDateOfBirth());
   return builder.toComparison();
}

Disable comparable by adding an attribute.

XML Source

<Bean name="Person" comparable="false">
   ...
</Bean>

An Immutable Bean

The bean can be made immutable by adding the attribute immutable="true". All fields are final and only set from constructors. Setters are generated and return a copy of the object with the setter field modified. Just add the immutable attribute to make a bean immutable!

XML Source

<Bean name="Person" comparable="false">
  <Field name="name" type="String" />
  <Field name="dateOfBirth" type="Date" />
</Bean>

Generated Code

public class Person implements IPerson {

   private final String name;
   private final Date dateOfBirth;

   public Person(String name, Date dateOfBirth) {
      this.name = name;
      this.dateOfBirth = dateOfBirth;
   }

   public Person(IPerson clone) {
      this.name = clone.getName();
      this.dateOfBirth = clone.getDateOfBirth();
   }

   public String getName() {
      return name;
   }

   public Date getDateOfBirth() {
      return dateOfBirth;
   }
}

Implementing Interfaces

The bean can implement any number of interfaces. The generator does its best to match method signatures and generates a stub method for any that are not already part of the bean. Add the extends element to extend a class or implement an interface.
<Bean name="Person">
   <Extends type="INamedPerson" />
   <Field name="name" type="String" />
   <Field name="dateOfBirth" type="Date" />
</Bean>

<Interface name="INamedPerson">
   <Method name="getName" returnType="String" />
</Interface>

Enums

A Basic Enum

An enum is a basic type that provides a strictly defined set of enumerated values. The following is a simple example of an enum representing the various HTTP methods:

XML Source

<Enum name="HttpMethod">
   <Constant name="GET" />
   <Constant name="POST" />
   <Constant name="OPTIONS" />
   <Constant name="HEAD" />
   <Constant name="PUT" />
   <Constant name="DELETE" />
   <Constant name="TRACE" />
   <Constant name="CONNECT" />
   <Constant name="PATCH" />
</Enum>

Generated Code

public enum HttpMethod {
   /** The GET constant. */
   GET,
   /** The POST constant. */
   POST,
   /** The OPTIONS constant. */
   OPTIONS,
   /** The HEAD constant. */
   HEAD,
   /** The PUT constant. */
   PUT,
   /** The DELETE constant. */
   DELETE,
   /** The TRACE constant. */
   TRACE,
   /** The CONNECT constant. */
   CONNECT,
   /** The PATCH constant. */
   PATCH;
}

Constant Fields

In addition to basic constants, there is support for fields in enums. This can be helpful, especially when implementing interfaces. When implementing interfaces, stub methods will not be generated. This means that it is only worth implementing interfaces with existing enum methods (e.g. name) or no methods.

Enums can extend interfaces, existing or generated.

XML Source

<Enum name="HttpStatusCode">
   <Extends type="IHttpStatusCode" />
   <Constant name="OK">
      <Field name="code" type="int" value="200" />
   </Constant>
   <Constant name="NOT_FOUND">
      <Field name="code" type="int" value="404" />
   </Constant>
   <Constant name="INTERNAL_SERVER_ERROR">
      <Field name="code" type="int" value="500" />
   </Constant>
</Enum>

<Interface name="IHttpStatusCode">
   <Method name="getName" returnType="int" />
</Interface>

Generated Code

public enum HttpStatusCode implements IHttpStatusCode {

   /** The OK constant. */
   OK(200),
   /** The NOT_FOUND constant. */
   NOT_FOUND(404),
   /** The INTERNAL_SERVER_ERROR constant. */
   INTERNAL_SERVER_ERROR(500);

   /** The code field. */
   private final int code;

   private HttpStatusCode(int code) {
      this.code = code;
   }

   /**
    * Getter for the code field.
    * @return the value of the code field.
    */
   public int getCode() {
      return code;
   }
}

public interface IHttpStatusCode {

   int getCode();
}

Interfaces

A Basic Interface

Interfaces are automatically generated for beans and other types, however it will often be helpful to specify and generate interfaces independently. Most definitions can implement an interface using the Extends element.

You can even use basic generics in definitions!

XML Source

<Inteface name="ICache{K, V}">
   <Method name="size" returnType="int" />
   <Method name="isEmpty" returnType="boolean" />
   <Method name="put" returnType="int">
      <Parameter name="key" type="K" />
      <Parameter name="value" type="V" />
   </Method>
   <Method name="get" returnType="V">
      <Parameter name="key" type="K" />
   </Method>
</Interface>

Generated Code

public interface ICache<K, V> {
   int size();
   boolean isEmpty();
   boolean put(K key, V value);
   V get(K key);
}

Generics

In the example above I have introduced generics. They are supported in the API using the curly braces {} in place of the usual angle brackets <> as these are prohibited in XML. Basic usage of generics has been tested for most types, however you may have issues with more complicated usage.

Aliases

An alias provides a simplified way of referencing classes in the definitions. Usually you would have to reference the fully qualified class name, however an alias enables you to map it to a more friendly (shorter!) name.

We can avoiding the need for fully qualified names in definitions:

XML Source

<Bean name="Time">
   <Field name="unit" type="java.util.concurrent.TimeUnit" />
   <Field name="value" type="long" />
</Bean>

By adding an alias, we can shorten all references to TimeUnit:

XML Source

<Alias name="TimeUnit" type="java.util.concurrent.TimeUnit" />

<Bean name="Time">
   <Field name="unit" type="TimeUnit" />
   <Field name="value" type="long" />
</Bean>

Naming Types

Aliases highlight an important feature and restriction of the API. Each bean, enum, interface, alias, etc.. must have a unique name. It can not be declared more than once, regardless of package or source file in which it is declared. This guarantees that any references to other definitions or aliases can be made by the simple name, and package is never required. This is a restriction that may cause some inconvenience as the number of definitions grows. However the clarity provided by every name being unique is well worth it.

Validators

A validator names and assigns restrictions to a data type. The validator can be referenced by name in any field definition (e.g. fields in a bean). By referencing the validator, checks are automatically generated in the methods associated with that field. The following validations are currently available:
  • not null on any object (defaults to true if not specified - null is evil!)
  • size of collection, list, set, map
  • size of string
  • pattern of string
  • range of Byte, Short, Integer, Long, Float, Double
  • range of byte, short, int, long, float, double

Some basic validator examples:

XML Source

<Validator name="id" type="int" min="1" />
<Validator name="name" type="String" min="6" max="255/>
<Validator name="description" type="String" pattern="[a-zA-Z]+" />
<Validator name="age" type="short" min="0" max="120" />

Comparators

Comparing Classes

As the compare method on beans is automatically generated, it is often desirable to select an alternative way to compare two beans. It can also be applied to any two classes with getter methods. Every comparator contains two flags to manipulate the ordering. The reverse flag reverses the order in which the fields are compared. The swap flag swaps the natural ordering for each field.

XML Source

<Comparator name="PersonComparator" type="Person">
   <Field name="dateOfBirth" />
   <Field name="name" />
</Comparator>

Generated Code

@Override
public int compare(Person compare1, Person compare2) {
   CompareToBuilder builder = new CompareToBuilder();
   if (reverse) {
      if (swap) {
         builder.append(compare2.getName(), compare1.getName();)
         builder.append(compare2.getDateOfBirth(), compare1.getDateOfBirth();)
      } else {
         builder.append(compare1.getName(), compare2.getName();)
         builder.append(compare1.getDateOfBirth(), compare2.getDateOfBirth();)
      }
   } else {
      if (swap) {
         builder.append(compare2.getDateOfBirth(), compare1.getDateOfBirth();)
         builder.append(compare2.getName(), compare1.getName();)
      } else {
         builder.append(compare1.getDateOfBirth(), compare2.getDateOfBirth();)
         builder.append(compare1.getName(), compare2.getName();)
      }
   }
   return builder.toComparison();
}

Adaptors

Adapting Classes

Occasionally it is necessary to create one class from the state of another. An adaptor can set the appropriate fields from one class to another.

XML Source

<Adaptor name="PersonAdaptor" from="Person" to="FacebookUser">
   <Field from="name" />
   <Field from="email" />
</Adaptor>

Generated Code

@Override
public FacebookUser adapt(Person from) {
   FacebookUser to = new FacebookUser();
   to.setName(from.getName());
   to.setEmail(from.getEmail());
   return to; 
}

Executable Beans

An Executable Bean

Most services will have a requirement to handle requests for data, or make requests to other services for data. The executable bean provides a layer to facilitate this process. An executable bean effectively represents a method call.
  • The bean fields represent the parameters to the method call, and are typically basic types.
  • The bean implements the IExecutableBean<R> interface, with R representing the return type of the method call. This type can be any object, ideally another bean.

XML Source

// We add the returnType attribute
// which denotes the return type of the executable method call

<Bean name="LoginUser" returnType="String">
   <Field name="email" validator="email" />
   <Field name="password" validator="password" />
</Bean>

The resulting bean is just like any other, however it implements IExecutableBean which extends IBean. Generated Code

public class LoginUser implements ILoginUser {

   /** The email field. */
   private String email = null;
   /** The password field. */
   private String password = null;
}

public interface ILoginUser extends IExecutableBean<Long>, Comparable<ILoginUser> {

   int SERIALIZATION_ID = 1101;

...

Serialization

When sending and receiving requests for data, whether by HTTP or binary, the request needs to be serialized. This serialization can be a text format such as JSON or XML for low volume communication, but often needs to be in a binary format for optimal performance.

Currently, three serialization formats are supported:

XML Source

<JsonSerializer type="LoginUser" />
<XmlSerializer type="LoginUser" />
<DataSerializer type="LoginUser" />

The resulting serialization code includes read and write methods.

Generated Code

public class LoginUserDataSerializer extends ObjectSerializer<ILoginUser> {

   @Override
   public ILoginUser readValue(IDataReader reader) throws IOException {
      String param1 = reader.readObject(new StringSerializer(false));
      String param2 = reader.readObject(new StringSerializer(false));
      return new LoginUser(param1, param2);
   }

   @Override
   public void writeValue(IDataWriter writer, ILoginUser object) throws IOException {
      writer.writeObject(object.getEmail(), new StringSerializer(false));
      writer.writeObject(object.getPassword(), new StringSerializer(false));
   }
}

Data Serialization

As both XML and JSON formats are self explanatory, I will just include a brief explanation of the binary 'data' format. This format is significantly more efficient than the text formats. It is faster to serialize and deserialize. The size of requests and responses is also much smaller. It is worth noting it is not the in-built Java serialization. It is more efficient, but achieves this by making assumptions that Java serialization does not. It is well tested and stable, but like all serialization it is vulnerable to versioning changes.

Data Stores

SQL Backed Data Stores

One of the most powerful features of this API, the data store, provides a simple interface to a database table. The following implementations are automatically generated from a datastore definition.

Complimentary Data Stores

In addition to the SQL implementations, a selection of other utility data store implementations are generated.
Map A Map backed DataStore implementation, useful for caching or as an in-memory table.
Delegate A simple delegate DataStore
Concurrent A delegate DataStore that uses a ReadWriteLock on individual method calls.
Copy A delegate DataStore that can be configured to clone values stored or retrieved.
Write Behind A delegate DataStore that uses an ExecutorService to execute write methods asynchronously.
Cached Persister A DataStore that delegates read methods to one DataStore (typically a cache), and write methods to another DataStore (typically a persister).

A Basic Data Store

The definition of a data store first requires the definition of at least two beans.
  • A Row Bean - the fields represent the full set of columns in the table
  • A Key Bean - the fields are a subset of the row

It is important to specify a constructor in the key bean over the row bean. It is also desirable to make the key bean immutable, although not mandatory. A data store is defined by its row and key beans

XML Source

<Bean name="UserRow">
   <Field name="id" type="int" />
   <Field name="email" type="String" />
   <Field name="password" type="String" />
</Bean>

<Bean name="UserKey">
   <Constructor type="IUserRow" />
   <Field name="id" type="int" />
</Bean>

<DataStore name="UserTable" element="UserRow" key="UserKey" />

By default all the different data store variants are generated... To selectively disable generation, the following attributes are available.

  • sql="false"
  • map="false"
  • copy="false"
  • delegate="false"
  • concurrent="false"
  • writeBehind="false"
  • cachedPersister="false"

It is not currently possible to selectively disable generation of individual SQL implementations.

IDataView & IDataStore

There are two interfaces that form the bases of all the data store implementations IDataView and IDataStore. The view is a read only version of the store, which extends it.

The view is read only

public interface IDataView<R> {
   Lock getReadLock();
   boolean exists();
   int size();
   boolean isEmpty();
   boolean contains(R row);
   List<R> getAll();
}

The store extends the view and is both read and write

public interface IDataStore<R> extends IDataView<R> {
   Lock getWriteLock();
   void create();
   void destroy();
   void clear();
   void remove(R row);
   void add(R row);
   void set(R row);
   void addAll(Collection<? extends R> elements);
   void setAll(Collection<? extends R> elements);
   void removeAll(Collection<? extends R> elements);
}

Notice that the data store looks very similar to a collection. The interfaces both expose access to a read and write lock, used for locking in concurrent versions of the data store.

Map Data Store

It is not always necessary to store data in the database directly. The map data store provides an alternative which can be used to store data in memory. The map store delegates to an implementation of the Map interface.

Generated Code

public class MapUserTable implements IUserTable {

   private final Map<IUserId, IUser> map;
   private final ReadWriteLock reentrantLock;
   private final AtomicInteger autoIncrement;
   private final Set<String> emailSet = new HashSet<String>();

   public MapUserTable(Map<IUserId, IUser> map) {
      if (map == null) {
         throw new NullPointerException("map");
      }
      this.map = map;
      this.reentrantLock = new ReentrantReadWriteLock(true);
      this.autoIncrement = new AtomicInteger(0);
   }

   @Override
   public void add(IUser element) {
      if (emailSet.contains(element.getEmail())) {
         throw new IllegalStateException("email already exists: " + element.getEmail());
      }
      UserId key = getKey(element);
      if (map.containsKey(key)) {
         throw new IllegalStateException("key already exists: " + key);
      }
      if (autoIncrement.get() < element.getId()) {
         autoIncrement.set(element.getId());
      }
      map.put(key, element);
   }

   @Override
   public IUser get(IUserId key) {
      if (key == null) {
         throw new NullPointerException("key");
      }
      return map.get(key);
   }

}

Concurrent Data Store

The concurrent data store provides a thread safe implementation of the data store. It wraps a delegate data store, using a read lock on all view methods and a write lock on any mutative methods. The concurrent data store users locking on all methods

Generated Code

public class ConcurrentUserTable implements IUserTable {

   private final IUserTable delegate;

   public ConcurrentUserTable(IUserTable delegate) {
      if (delegate == null) {
         throw new NullPointerException("delegate");
      }
      this.delegate = delegate;
   }

   @Override
   public IUser get(IUserId key) {
      Lock lock = getReadLock();
      lock.lock();
      try {
         return delegate.get(key);
      } finally {
         lock.unlock();
      }
   }

   @Override
   public void add(IUser element) {
      Lock lock = getWriteLock();
      lock.lock();
      try {
         delegate.add(element);
      } finally {
         lock.unlock();
      }
   }
}

Write Behind Data Store

The concurrent data store provides a thread safe implementation of the data store. It wraps a delegate data store, a read lock on all view methods and a write lock on any mutative methods. The concurrent data store uses locking on all methods

Generated Code

public class WriteBehindUserTable implements IUserTable {

   private final IUserTable delegate;
   private final ExecutorService executor;

   public WriteBehindUserTable(IUserTable delegate, ExecutorService executor) {
      this.delegate = delegate;
      this.executor = executor;
   }

   @Override
   public IUser get(final IUserId key) {
      // Synchronous
      Future<IUser> future = executor.submit(new Callable<IUser>() {
         public IUser call() {
            return delegate.get(key);
         }
      });

      try {
         return future.get();
      } catch(Exception e) {
         throw Throwables.propagate(e);
      }
   }

   @Override
   public void add(final IUser element) {
      // Asynchronous (Write Behind)
      executor.submit(new Runnable() {
         public void run() {
            delegate.add(element);
         }
      });
   }
}

Copy Data Store

The copy data store provides a way of protecting a data store from external interference. Any data store that maintains references to the object added to or retrieved from itself after method call complete, such as a cache, is open to unintentional abuse. Unless the objects are immutable, any manipulation of the object will change its state in the data store.

Generated Code

public class CopyUserTable implements IUserTable {

   private final boolean copyOnRead;
   private final boolean copyOnWrite;
   private final IUserTable delegate;

   public CopyUserTable(IUserTable delegate, boolean copyOnRead, boolean copyOnWrite) {
      if (delegate == null) {
         throw new NullPointerException("delegate");
      }
      this.delegate = delegate;
      this.copyOnRead = copyOnRead;
      this.copyOnWrite = copyOnWrite;
   }

   @Override
   public void add(IUser element) {
      if (copyOnWrite) {
         element = new User(element);
      }
      delegate.add(element);
   }

   @Override
   public IUser get(IUserId key) {
      IUser returnValue = delegate.get(key);
      if (copyOnRead) {
         returnValue = new User(returnValue);
      }
      return returnValue;
   }
}

Cached Persister Data Store

The cached persister data store provides an efficient means of accessing a data store that is small enough to be cached in memory. It references two data stores, one as a cache, the other as a persister. Read methods access the cache only, while write methods write to both the cache and the persister. The underlying SQL table is kept up to date, while read access to the table is exceptionally fast using the cache. The cached persister data store is backed by both a cache and an underlying SQL persister.

Generated Code

public class CachedPersisterUserTable implements IUserTable {

   private final IUserTable cache;
   private final IUserTable persister;

   public CachedPersisterUserTable(IUserTable cache, IUserTable persister) {
      this.cache = cache;
      this.persister = persister;
   }

   @Override
   public void add(IUser element) {
      cache.add(element);
      persister.add(element);
   }

   @Override
   public IUser get(IUserId key) {
      return cache.get(key);
   }
}

Typically this data store is combined with the write behind data store to provide exceptional read and write access speeds. Reads are made directly in to the cache, and writes are written asynchonously. For thread safety the entire thing can be wrapped in a concurrent data store! To protect the data store against the mutation of added or accessed rows, a copy data store can also be used. This clonse the rows read from and written to the data store, so developer manipulation of the objects outside the store has no effect on its internal state.