Skip to content

Schema Writing Guide

Cameron Beccario edited this page Jun 21, 2013 · 3 revisions

Grain schemas are simply standard Java interfaces. Taken as a whole, these schemas should describe your object model as generally as possible and ideally "stand alone", i.e., not have specific implementation requirements. Grains can then be considered just one of several potential implementations of your model.

Nonetheless, the Grains framework does impose some requirements on your object model that are hopefully not too onerous. In fact, these requirements help the model "stand alone", allowing multiple implementations should they be needed in the future.

Getters, Not Behavior

Firstly, the model should simply be a description of data, not logic or behavior. The only methods allowed in schemas are parameterless getter methods using standard JavaBean naming conventions: getFoo(), isBar(), etc. Other methods, such as setters, indexed properties or actions, will cause compilation errors.

Immutable Types

The Grains framework generates an immutable implementation of your schema, therefore each property type must have a suitable corresponding immutable type. Most primitives and value types in Java are immutable by definition and so can be freely used:

  • boolean/Boolean
  • byte/Byte
  • short/Short
  • int/Integer
  • long/Long
  • float/Float
  • double/Double
  • char/Character
  • String
  • BigInteger
  • BigDecimal
  • UUID
  • URI
  • any enum

(Note the absence of Object, Date, Calendar, and arrays, all of which are mutable. For convenience, BigDecimal, BigInteger, and enums are considered immutable although they are technically mutable.)

Example:

@GrainSchema
public interface Order {

    String getProduct();

    int getQuantity();
}

// -----------------------------------------------
// Corresponding Generated Grain:
public interface OrderGrain extends Order, Grain {

    String getProduct();
    OrderGrain withProduct(String product);

    int getQuantity();
    OrderGrain withQuantity(int quantity);
}

Collections

The following Java collections can be used in schemas:

  • Collection
  • List
  • Set
  • Map
  • SortedSet
  • SortedMap

Unfortunately, because these collections are not immutable by contract, we need to map them to corresponding immutable versions during code generation. By default, the generator maps them to the ConstCollection types contained in the net.nullschool.collect package. It is also possible to use alternative immutable collection frameworks, such as the one contained in Google Guava (discussed later). The collections' type arguments must have corresponding immutable types. List<Object> or even raw List are not allowed because Object cannot be made immutable.

Example:

@GrainSchema
public interface Order {

    ...

    List<String> getNotes();
}

// -----------------------------------------------
// Corresponding Generated Grain:
public interface OrderGrain extends Order, Grain {

    ...

    ConstList<String> getNotes();
    OrderGrain setNotes(ConstList<String> notes);
}

Although it is valid to use ConstList, ConstSet, etc., directly in a schema, doing so would unnecessarily introduce implementation details into your object model. Using the example above, there is nothing special about the concept of an order that requires the list of notes to be immutable--a simple list is sufficient. Immutability is an implementation detail of the grain.

@GrainSchema Annotation

Perhaps obvious from the examples above, each interface schema must be annotated with the @GrainSchema annotation. This annotation makes the interface discoverable to the grain generator.

Compound Grains

Any schema can also be used in schemas. It will be mapped to its corresponding grain type:

@GrainSchema
public interface Contact {

    String getName();

    String getEmail();
}

@GrainSchema
public interface Order {

    ...

    Contact getPurchaser();
}

// ---------------------------------------------------
// Corresponding Generated Grains:
public interface ContactGrain extends Contact, Grain {

    String getName();
    ContactGrain withName(String name);

    String getEmail();
    ContactGrain withEmail(String email);
}

public interface OrderGrain extends Order, Grain {

    ...

    ContactGrain getPurchaser();
    OrderGrain withPurchaser(ContactGrain purchaser);
}

Although it is valid to use ContactGrain directly in the Order schema, doing so would unnecessarily introduce grain implementation details into your object model. Keep your model pure and let the mapping occur during code generation. Your model won't even compile if the grain it refers to has not yet been generated.

Schema Inheritance and Composite Grains

Schemas may extend any interface that contributes properties, including other schemas and generic interfaces. This creates a composite grain which represents the union of all inherited properties:

public interface Identifiable<T> {
    T getId();
}

@GrainSchema
public interface Trackable {
    String getTrackingNumber();
}

@GrainSchema
public interface Order extends Identifiable<UUID>, Trackable {

    ...
}

// ---------------------------------------------------------------
// Corresponding Generated Grains:
public interface TrackableGrain extends Trackable, Grain {

    String getTrackingNumber();
    TrackableGrain withTrackingNumber(String trackingNumber);
}

public interface OrderGrain extends Order, Grain, TrackableGrain {

    UUID getId();
    OrderGrain withId(UUID id);

    ...

    String getTrackingNumber();
    OrderGrain withTrackingNumber(String trackingNumber);
}

Covariant Generic Types

Because the generator sometimes performs mapping to narrower types during code generation, it may be necessary to add wildcards to generic type arguments for the generated code to compile. This is due to the Java language rule that, given T extends S, List<T> is not covariant with (i.e., cannot override) List<S> but is covariant with List<? extends S>. This occurs when a type argument is mapped to a narrower type, particularly when using collections of collections, or collections of grains.

Example:

@GrainSchema
public interface Foo {

    Set<Order> getA();  // not ok

    Set<? extends Order> getB();  // ok

    Set<Set<Set<Long>>> getC();  // not ok

    Set<? extends Set<? extends Set<Long>>> getD();  // ok
}

// ---------------------------------------------------------------
// Corresponding Generated Grain:
public interface FooGrain extends Foo, Grain {

    ConstSet<OrderGrain> getA();  // compilation error!
    FooGrain withA(ConstSet<OrderGrain> a);

    ConstSet<OrderGrain> getB();
    FooGrain withB(ConstSet<OrderGrain> b);

    ConstSet<ConstSet<ConstSet<Long>>> getC();  // compilation error!
    FooGrain withC(ConstSet<ConstSet<ConstSet<Long>>> c);

    ConstSet<ConstSet<ConstSet<Long>>> getD();
    FooGrain withD(ConstSet<ConstSet<ConstSet<Long>>> d);
}

Additional Immutable Types and Mappings

Defining type policies makes it possible to expand the set of immutable types recognized by the generator and create new mappings. The easiest way to define a custom type policy is to create a singleton instance of ConfigurableTypePolicy, place it in the same package as your object model, then specify the access expression in the grains-plugin configuration:

<plugin>
    <groupId>net.nullschool</groupId>
    <artifactId>grains-plugin</artifactId>
    ...
    <configuration>
        <typePolicy>com.acme.model.MyCustomTypePolicy.INSTANCE</typePolicy>
    </configuration>
</plugin>

This sample project provides an example custom type policy that adds support for Google Guava's immutable collections.