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.
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.
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 Double
s.
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.
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
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[])
Strings, arrays, and List
s 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.
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 | |
---|---|
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.
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.
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 | |
---|---|
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.
|
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)
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)
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.
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.