Annotation Interface DSL
DSL is used to designate a DSL/Model object, which is enriched using the AST transformation. The annotation can also be placed on interfaces, however, interfaces are not transformed at all, fields with an DSL interface type are still handled correctly.
The DSL annotation leads to the creation of a couple of useful DSL methods. Note that most of these methods are not visible by default, as not to clutter the interface of the model. Instead they are created in a special inner class that is only accessible with
Factory and apply methods
Each instantiable DSL class gets a static field Create of either a subclass of KlumFactory.Keyed or
KlumFactory.Unkeyed, which provides methods to create instances of the class; abstract classed get an
implementation of KlumFactory instead.
@DSL
class Config {}
@DSL
class ConfigWithKey {
@Key String name
}
allows to create instances with the following calls:
Config.Create.One()
Config.Create.With(a: 1, b: 2)
Config.Create.With(a: 1, b: 2) { c 3 }
Config.Create.With { c 3 }
ConfigWithKey.Create.One('Dieter')
ConfigWithKey.Create.With('Dieter', a: 1, b: 2)
ConfigWithKey.Create.With('Dieter', a: 1, b: 2) { c 3 }
ConfigWithKey.Create.With('Dieter') { c 3 }
The optional closure to the With method is used to set values on the created object. The 'One' method is a shortcut for
'With' without any given values, which makes a nicer syntax (Config.Create.With() seems a bit strange).
Note that pre 2.0 versions of KlumAST did create the methods directly as static methods of the model class. These methods are now deprecated in will be removed in a future version.
If the class contains an static inner class named Factory of the appropriate type or the member factory points to such a class, this class is used as a base for the generated factory instead. This allows adding additional methods to the factory.
Completed DSL Objects expose no generated mutation method. Configuration is performed on a generated Builder through factory callbacks, Templates, and pre-materialization lifecycle methods.
Create.With also supports named parameters, allowing values to be set in a concise way. Every map element of
the method call is converted in a setter call (actually, any method named like the key with a single argument will be called):
Config.Create.With {
name "Dieter"
age 15
}
Could also be written as:
Config.Create.With(name: 'Dieter', age: 15)
Of course, named parameters and regular calls inside the closure can be combined ad lib.
There are also a couple of [[Convenience Factories]] to load a model into client code.
Lifecycle Methods
Lifecycle methods can are methods annotated with {@literal @PostCreate} and {@literal @PostApply}. These methods will be called
automatically after the creation of the object (**after the [[template|Templates]] - if set - has been applied**) and
after the call to the apply method, respectively.
Lifecycle methods must not be private and will automatically be made protected, which means you can usually safely
use default groovy visibility (i.e. simply use def myMethod()).
copyFrom() method
Each DSLObject gets a copyFrom() method with its own class as parameter. This method copies fields from the given
object over to this objects, excluding key and owner fields. For non collection fields, only a reference is copied,
for Lists and Maps, shallow copies are created.
Currently, it is in discussion whether this should be deep clone instead, see: (#36)
equals() and toString() methods
If not yet present, equals() and toString() methods are generated using the respective ASTTransformations. You
can customize them by using the original ASTTransformations.
hashCode()
A barebone hashcode is created, with a constant 0 for non-keyed objects, and the hashcode of the key for keyed objects. While this is correct and works with changing objects after adding them to a HashSet / HashMap, the performance for Sets of non-Keyed objects is severely reduced.
Field setter
Field setter for simple fields
For each simple value field create an accessor named like the field, containing the field type as parameter.
@DSL
class Config {
String name
}
creates the following method:
def name(String value)
Used by:
Config.Create.With {
name "Hallo"
}
Setter for simple collections
for each simple collection, two/three methods are generated:
- two methods with the collection name and a Iterable/Vararg argument for Collections or a Map argument for maps. These methods add* the given parameters to the collection
- an adder method named like the element name of the collection an containing a the element type
@DSL
class Config {
List<String> roles
Map<String, Integer> levels
}
creates the following methods:
def roles(String... values)
def roles(Iterable<String> values)
def role(String value)
def levels(Map levels)
def level(String key, Integer value)
Usage:
Config.Create.With {
roles "a", "b"
role "another"
levels a:5, b:10
level "high", 8
}
If the collection has no initial value, it is automatically initialized.
Setters and closures for DSL-Object Fields
For each DSL Object composition field, a Builder closure method is generated. If the field is a keyed object, this
method has an additional String parameter. Existing completed objects are accepted only for LINK
aggregation fields.
@DSL
class Config {
UnKeyed unkeyed
Keyed keyed
}
@DSL
class UnKeyed {
String name
}
@DSL
class Keyed {
@Key String name
String value
}
Conceptually creates the following Builder methods (the concrete generated Builder type is not public API):
def unkeyed(Closure closure) // closure delegates to the generated UnKeyed Builder
def keyed(String key, Closure closure) // closure delegates to the generated Keyed Builder
Usage:
Config.Create.With {
unkeyed {
name "other"
}
keyed("klaus") {
value "a Value"
}
}
The closure methods return the child Builder, so construction-time configuration can be composed:
Config.Create.With {
def childBuilder = unkeyed {
name "other"
}
childBuilder.name "final"
}
Collections of DSL Objects
Collections of DSL-Objects are created using a nested closure. The name of the (optional) outer closure is the field name, the name of the inner closures the element name (which defaults to field name minus a trailing 's'). The syntax for adding keyed members to a list and to a map is identical (obviously, only keyed objects can be added to a map).
Inner creators produce child Builders in the owning Builder's lifecycle and return those Builders for
construction-time composition. Completed DSL Objects can be added only to collections marked
LINK; they remain existing aggregation targets and are never re-owned.
@DSL
class Config {
List<UnKeyed> elements
List<Keyed> keyedElements
Map<String, Keyed> mapElements
}
@DSL
class UnKeyed {
String name
}
@DSL
class Keyed {
@Owner owner
@Key String name
String value
}
def objectForReuse = UnKeyed.Create.With { name "reuse" }
def anotherObjectForReuse
def createAnObject(String name, String value) {
Keyed.Create.With(name) { value(value) }
}
Config.Create.With {
elements {
element {
name "an element"
}
element {
name "another element"
}
element objectForReuse
}
keyedElements {
anotherObjectForReuse = keyedElement ("klaus") {
value "a Value"
}
}
mapElements {
mapElement ("dieter") {
value "another"
}
mapElement anotherObjectForReuse // owner is NOT changed
mapElement createAnObject("Hans", "Franz") // owner is set to Config instance
}
}
// flat syntax without nested closures:
Config.Create.With {
element {
name "an element"
}
element {
name "another element"
}
element objectForReuse
anotherObjectForReuse = keyedElement ("klaus") {
value "a Value"
}
mapElement ("dieter") {
value "another"
}
mapElement anotherObjectForReuse // owner is NOT changed
mapElement createAnObject("Hans", "Franz") // owner is set to Config instance
}
On collections
Although most examples in the user documentation use List, basically any class implementing / sub interface of Collection can be
used instead. There are a couple of points to take note, however:
- The default Java Collection Framework interfaces (Collection, List, Set, SortedSet, Stack, Queue) work out of the box
- When using a custom collection **class** or **interface**, in order for initial values to be provided,
Listmust be coerced to your custom type, i.e. the code[] as <YourType>must be resolvable. This can be done by- enhance the
List.asType()method to handle your custom type - in case of a custom class, provide a constructor taking an
Iterable(orCollectionorList) argument
- enhance the
However, it is strongly advised to only take the basic interfaces. If additional functionality is needed, it might make more sense to apply it using a decorator (for example using KlumWrap) after the object is constructed.
For maps, onlyMap and SortedMap is supported.
Be careful when using a simple Set. Since Klum creates barebone hashcode implementations
(constant zero for non-keyed objects, hashCode of key for keyed objects), a (non Sorted)Set of
non-Keyed model objects might result in a severe degradation of performance of that Set.
-
Optional Element Summary
Optional ElementsModifier and TypeOptional ElementDescriptionClass<?>When present, the given type is used as default type for a field of this type.Class<?>When set, the given class, which must be a subclass of either KlumFactory (for abstract classes) or KlumFactory.Keyed/Unkeyed will be used as a base for the generated factory class.The short name of the class to be used in collections.When present, the given suffix is stripped from child class names to determine the short name.
-
Element Details
-
shortName
String shortNameThe short name of the class to be used in collections. If not set, defaults to the name of the class, with the first character converted to lowercase.- Default:
- ""
-
stripSuffix
String stripSuffixWhen present, the given suffix is stripped from child class names to determine the short name.- Default:
- ""
-
defaultImpl
Class<?> defaultImplWhen present, the given type is used as default type for a field of this type. This makes most sense on interfaces or abstract classes.- Default:
- groovy.transform.Undefined.class
-
factory
Class<?> factoryWhen set, the given class, which must be a subclass of either KlumFactory (for abstract classes) or KlumFactory.Keyed/Unkeyed will be used as a base for the generated factory class. Note that if the annotated class contains a static inner class named "Factory", this class will be used by default.- Default:
- groovy.transform.Undefined.class
-