-
Notifications
You must be signed in to change notification settings - Fork 4
Schema Writing Guide
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.
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.
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);
}
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.
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.
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.
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);
}
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);
}
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.