Chapter 6. The Configuration Language

The workhorse feature of an IoC container is the way it constructs and configures new components. The standard way to configure components in Seedling is through an expression language that's much like plain-old Java but is specialized to the needs of configuration injection. While Seedling doesn't recognize all possible Java expressions, the language should feel natural and the semantics are very close to Java.

6.1. Basic Tech: Properties Files and JavaBeans

The Seedling configuration language is based on two pieces of standard Java technology, properties files and JavaBeans. We should have a quick overview here, but that's yet to be written, so you might want to check out these resources.

Seedling configuration files use the .properties format as specified by the documentation for java.util.Properties. The expression language inherits a few rough edges from that format:

  • Expressions that take more than one line must escape the newline(s) using a backslash.

  • String literals must use Pascal-style escapes for the double quote character, as shown below.

  • You cannot start end-of-line comments (# blah) following other data on the same line.

Seedling leverages "bean properties" as defined by the JavaBeans specification. The most relevant part is the way property getter and setter methods are defined. The Tapestry documentation has a decent overview of how that works.

6.2. Core Literals

The expression language provides literals for these Java features:

  • Boolean, using keywords true and false.

  • Integer and Long, using the expected syntax. Unless the (optional) suffix 'L' or 'l' is used to force interpretation as Long, the type of the literal is just large enough to hold the given value. Hexadecimal notation is not currently supported.

  • Float and Double, using the expected syntax. As with Java literals, unless the suffix 'F' or 'f' is used, floating-point literals are interpreted as Doubles. Hexadecimal notation is not currently supported.

  • String, using Java-style syntax, including backslash escapes (but see below for an important exception).

  • Class, using the usual fully-qualified syntax. To denote nested classes, use Java's semi-secret internal representation: package.ParentClass$NestedClass.

  • Static fields can be denoted using fully-qualified syntax. This also works for enum constants.

Due to brain-damage in the standard Properties-file syntax, the escape-sequence for the double-quote is Pascal-style, not Java-style: two adjacent double-quote characters.

prop = "Here is a double-quote: ""\nGotta love Pascal."

The keyword null means the usual thing:

prop = null

Note that setting a property to null is different from not setting it at all. While the configuration above will cause invocation of setProp(null), one can explicitly avoid calling setProp() altogether by declaring an empty expression:

prop =
# There's no text in the property value above

Such empty configuration is interpreted to mean there’s no configuration for this property. That can be useful to override config from another module.

6.3. Identifiers and Paths

Identifiers follow Java syntax. In most cases, an identifier will reference a sibling node of the node being configured, as opposed to referencing a property of the current node.

# file: config/branch/Node1

otherNode = Node2
# Configures branch/Node1.otherNode to refer to branch/Node2

When two or more identifiers are connected by a slash character, they form a path to another node in the Seedling hierarchy. These paths are interpreted relative to the branch containing the current node.

# file: config/branch/Node1

otherNode = childBranch/Node2
# Configures branch/Node1.otherNode to refer to branch/childBranch/Node2

6.4. Lists (and Arrays)

Instances of java.util.ArrayList can be created by using square brackets around a comma-separated sequence of expressions. The evaluator will automatically convert between lists and arrays when necessary and possible.

prop = [ 1, 2, 3 ]

Such configuration will work with any of the following setters:

  • void setProp(java.util.ArrayList)

  • void setProp(java.util.List)

  • void setProp(java.util.Collection)

  • void setProp(int[])

6.5. Concatenation and Addition

Strings, arrays, and Lists can be concatenated using the + operator:

url = "http://" + this.host + "/"
statusCodes = /some/Node.errorCodes + [ 2234 ] + OtherNode.codes

If the left side is a string (any CharSequence will do), then the right side is converted toString() before concatenation.

Similarly, integers can be added:

port = 80 + this.portOffset
portOffset = 2

In either case, if one side of the operator is null, the result is the other side. If both sides are null, the result is null.

6.6. Method Invocation

The expression language can denote method invocation using Java-like syntax. Methods can be invoked on a specific class, in which case the named method must be static, or on another node, in which case the method may be either static or dynamic. Here are some examples.

# Invoke a static method on a fully-qualified class.
prop = some.package.Class.staticMethod(12)
# Invoke a static or dynamic method on a node.
prop = some/branch/Node.method("hello")
# Ambiguous case resolved by looking for node first, then class.
prop = NodeOrUnpackagedClass.method()
[Note]Note
Static method invocation currently requires fully-qualified class names.

Method calls can be nested and chained:

prop = Node1.method1(Node2.method2()).method3(siblingNode)

Methods declared to return void can be called as expected, with the effective result of null.

As a special case of method invocation, sparkly new Java objects can be constructed using the keyword new and a fully-qualified class name.

prop = new com.eg.Something(12, "hi")

In all cases, Seedling will respect declared access restrictions by only invoking public methods.

6.6.1. Selecting Among Overrides

Whenever an expression denotes a method invocation, Seedling must be prepared to select a specific method from a set of overloads. Since the expression language is dynamically typed, we have different information to go on than Java code. In particular, every Java expression has a single static type (since every name has a declared type). Seedling only has dynamic information and any instance may have numerous types associated with it: an instance of class C is also an instance of Object, and of every other superclass of C, and of every interface that C inherits. Thus every method-invocation expression with one or more parameters may satisfy several potential overloads.

Seedling approaches this problem in a simple and straightforward fashion. First, it evaluates each parameter expression and notes the type of the result. For each overload with the correct number of parameters, it determines whether each (dynamic) parameter value is assignable to the corresponding (static) parameter type declared by the overload. If the parameter values satisfy exactly one overload, that overload is invoked; otherwise an exception is thrown. When that happens, you'll probably want to use a typecast expression to resolve the ambiguity and select the desired overload.

When a parameter uses this-property notation, the value of the referenced property is first determined by evaluating it as usual. However, the result is then converted to the static property type, just as if it were about to be injected via the property setter. This gives the evaluator more precise type information by which to select a method overload.

6.7. Node Creation

So far we've talked about writing expressions that configure individual properties of a node, but we've not shown how to create the node itself! The value of the current node is declared using the metaproperty .this along with an arbitrary expression.

In most cases the .this expression will be a normal constructor call:

.this = new com.eg.Bean(12, true)
.this = new java.lang.String("A node can be any object.")

More advanced cases leverage method invocation; such methods we herein call factory methods. Factory methods can be static or dynamic, just like any method call.

# Invoke a static factory method on a fully-qualified class.
.this = some.user.Class.staticFactoryMethod(...)
# Invoke a static or dynamic factory method on a node.
.this = some/branch/Factory.makeInstance(...)
# Ambiguous case resolved by looking for class first, then node.
.this = NodeOrUnpackagedClass.factoryMethod(...)

We mentioned before that an empty property expression means there’s no configuration, and that goes for .this too:

.this =
# There's no expression, so the node won't be created.

That means you can override a creation expression to completely disable node provisioning. Such configuration is almost equivalent to there being no configuration at all! This can be handy when you're using a module that declares a particular node, but you want to ensure that the node won't be created. (Of course, your empty creation expression could itself be overriden by another configuration layer...)

Another way to end up with a nonexistent node is to have a .this expression that evaluates to null, either literally or as the result of a method invocation.

[Important]Important
The container will assume that the value of the .this expression is not installed elsewhere in the tree, and will perform all of the usual lifecycle operations on it. Violation of this expectation could result in strange behavior, especially if the object uses a callback interface like ServiceNode. If you need the same object to appear in more than one location, be sure to use an alias instead of a creation expression that returns the same object.

6.8. This-Property References

An expression can reference properties on the current node by using Java's this dot member reference notation:

prop1 = "hello"
prop2 = this.prop1
prop3 = node.method(this.prop2)

When such notation is used within a .this = new constructor expression, the named property will not undergo property injection. The evaluator assumes that the property utilizes constructor injection and won't inject it a second time. For nodes with many constructor parameters, this idiom can increase the readability of the configuration file by making the parameter/property association explicit.

.this = new com.eg.MyList(this.initialSize, this.expandable)
initialSize = 12
expandable = true

Despite the fact that no setters will be invoked for property injection, Seedling still needs to be able to discover the type of the properties. This implies that MyList exposes a property called initialSize, meaning it has a getter getInitialSize() and/or a setter setInitialSize(...). (And the same goes for expandable.)

Factory method invocations cannot use this-property names as parameters. That's because the concrete type of such node is not known until after the factory method returns, so the evaluator can't determine what properties are on the node until its too late to reference them.

# This will cause an error:
.this = path/to/MyFactory.makeNode(this.prop)

6.9. Super References

Property assignments fully replace any expressions declared in super-configuration. This is normal overriding of inherited configuration. But there are times when the subconfiguration needs to extend or otherwise modify the inherited value rather than replacing it. In the Seedling expression language, the super keyword means something like the value of the current property were it not being overridden here. It's much like the meaning of the keyword in Java itself when invoking a method as implemented in a superclass.

For example, consider a node that has a property ports of type int[] and a base configuration file declaring:

# superconfig
ports = [ 80 ]

A subconfig file (usually found in a higher-priority module) can extend the array as follows:

# subconfig
ports = super + [8080, 8081]
# The resulting configured value is [80, 8080, 8081]

The super keyword is not yet supported in all contexts in which it could be useful, for example:

# Not supported!
prop = node.method(super)

6.10. Class Expressions

Classes are mostly first-class in the config language: for most expressions where Java requires a class literal, Seedling can handle an arbitrary expression evaluating to a Class instance.

  • If a class-expression is follow by a dot-name chain, then it denotes access to static members, not to members of Class. This is consistent with the usual Java syntax for static access. For example, if the Class instance java.util.Collections is installed at node T then the expression T.EMPTY_LIST evaluates to java.util.Collections.EMPTY_LIST. Similarly, T.emptyList() evaluates to java.util.Collections.emptyList().

  • To get access to members on the Class instance itself, use the special form T.class.member. Here, class is interpreted similarly to the same notation in Java. For example, if the Class instance java.util.Collections is installed at node T then the expression T.class.getSimpleName() and the expression T.class.simpleName both evaluate to the string "Collections".

  • The one context in which this doesn't behave regularly is invoking a constructor, when the class expression ends with a property access. For example, if node N has a getType() method returning a Class, then one might expect the expression new N.type(...) to construct a new instance of that class. However, that runs into a problem because the syntax looks like an invocation of a method called type(...). To work around this, one can use method invocation syntax to read the property: new N.getType()(...) will do the right thing.

6.11. Casts

Values can be coerced to a specific type via the usual Java notation. This can be particularly useful for disambiguating among overridden methods. Here's a ridiculous example:

# Disambiguate between Exception(String) and Exception(Thowable)
.this = new java.lang.Exception( (java.lang.String) null )

Note that Seedling currently requires all class names to be fully-qualified.