...
Scrollbar |
---|
...
Tapestry
...
Wiki Markup |
---|
{float:right|background=#eee}
{contentbylabel:title=Related Articles|showLabels=false|showSpace=false|space=@self|labels=configuration}
{float} |
Tapestry services – both those IOC Configuration is the configuration of both the IOC services provided by Tapestry and those you write yourself – . Both are configured in the same way: using Java, not XML.
Div | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||
|
One of the key concepts in Tapestry IoC is distributed configuration. The distributed part refers to the fact that any module may configure a service. Distributed configuration is the key feature of Tapestry IoC that supports extensibility and modularity.
Modules configure a service by contributing to service configurations. This seems may seem esoteric , but is quite handy, and is best explained by but is really pretty simple. We'll explain with an example.
Let's say you've written a bunch of different services, each of which does something specific for a particular type of file (identified by the file's extension), and each implements the same interface, which we'll call FileServicer. And now let's say you need a central service that selects the one of your FileServicer implementations based on a given file extension. You start by providing a service builder method:
Code Block | ||||
---|---|---|---|---|
| ||||
public static FileServiceDispatcher buildFileServicerDispatcher(Map<String,FileServicer> contributions)
{
return new FileServiceDispatcherImpl(contributions);
} |
...
So where do the values come from? Your service contributor methods, methods whose names start with "contribute":
Code Block | ||||
---|---|---|---|---|
| ||||
public static void contributeFileServicerDispatcher(MappedConfiguration<String,FileServicer> configuration)
{
configuration.add("txt", new TextFileServicer());
configuration.add("pdf", new PDFFileServicer());
} |
Or, instead of instantiating those services ourselves, we could inject them:
Code Block | ||||
---|---|---|---|---|
| ||||
public static void contributeFileServicerDispatcher(MappedConfiguration<String,FileServicer> configuration,
@InjectService("TextFileServicer") FileServicer textFileServicer,
@InjectService("PDFFileServicer") FileServicer pdfFileServicer)
{
configuration.add("txt", textFileServicer);
configuration.add("pdf", pdfFileServicer);
} |
The extensibility comes from the fact that multiple modules may all contribute to the same service configuration:
Code Block | ||||
---|---|---|---|---|
| ||||
public static void contributeFileServicerDispatcher(MappedConfiguration<String,FileServicer> configuration)
{
configuration.add("doc", new WordFileServicer());
configuration.add("ppt", new PowerPointFileServicer());
} |
...
Naming conventions vs. Annotations
Since | ||
---|---|---|
| ||
...
- There is no longer a linkage between the contribution method name and the service id, which is much more refactoring safe: if you change the service interface name, or the id ID of the service, your method will still be invoked when using @Contribute.
- It makes it much easier for an override of the service to get the configuration intended for the original service.
The following example is an annotation-based alternative for the contribution method above.
Code Block | ||||
---|---|---|---|---|
| ||||
@Contribute(FileServiceDispatcher.class)
public static void arbitraryMethodName(MappedConfiguration<String,FileServicer> configuration)
{
configuration.add("doc", new WordFileServicer());
configuration.add("ppt", new PowerPointFileServicer());
}
|
If you have several implementations of a service interface, you have to disambiguate the services. For this purpose the marker annotations should be placed on the contributor method.
Code Block | ||||
---|---|---|---|---|
| ||||
@Contribute(FileServiceDispatcher.class)
@Red @Blue
public static void arbitraryMethodName(MappedConfiguration<String,FileServicer> configuration)
{
configuration.add("doc", new WordFileServicer());
configuration.add("ppt", new PowerPointFileServicer());
}
|
...
If the special @Local annotation is present, then the contribution is made only to the configuration of a service being constructed in the same module.
It is not impossible that Note that it is possible for the same contribution method will to be invoked to contribute to the configuration of multiple different services.
Code Block | ||||
---|---|---|---|---|
| ||||
@Contribute(FileServiceDispatcher.class)
@Local
public static void arbitraryMethodName(MappedConfiguration<String,FileServicer> configuration)
{
configuration.add("doc", new WordFileServicer());
configuration.add("ppt", new PowerPointFileServicer());
}
|
...
One thing to remember is that the order in which contributions occur is unspecified. There will be a possibly large number of modules, each having zero or more methods that contribute into the service. The order in which these methods are invoked is unknown.
For example, here's a kind of Startup service that needs some Runnable objects. It doesn't care what order the Runnable objects are executed in.
Code Block | ||||
---|---|---|---|---|
| ||||
public static Runnable buildStartup(final Collection<Runnable> configuration)
{
return new Runnable()
{
public void run()
{
for (Runnable contribution : configuration)
contribution.run();
}
};
} |
Here we don't even need a separate class for the implementation, we use a an inner class for the implementation. The point is, the configuration is provided to the builder method, which passes it along to the implementation of the service.
On the contribution side, a service contribution method sees a Configuration object:
Code Block | ||||
---|---|---|---|---|
| ||||
public static void contributeStartup(Configuration<Runnable> configuration)
{
configuration.add(new JMSStartup());
configuration.add(new FileSystemStartup());
} |
The Configuration interface defines just a single method: add()
. This is very intentional: the only thing you can do is add new items. If we passed in a Collection, you might be tempted to check it for values, or remove them ... but that flys flies in the face of the fact that the order of execution of these service contribution methods is entirely unknown.
For readability (if Java any longer supports that concept), we've parameterized the configuration parameter of the method, constraining it to instances of java.lang.Runnable, so as to match the corresponding parameter. This is optional, but often very helpful. In any case, attempting to contribute an object that doesn't extend or implement the type (Runnable) will result in a runtime warning (and the value will be ignored).
...
So, if we changed our Startup service to require a specific order for startup:
Code Block | ||||
---|---|---|---|---|
| ||||
public static Runnable buildStartup(final List<Runnable> configuration)
{
return new Runnable()
{
public void run()
{
for (Runnable contribution : configuration)
contribution.run();
}
};
} |
Notice that the service builder method is shielded from the details of how the items are ordered. It doesn't have to know about ids IDs and pre- and post-requisites. By using a parameter type of List, we've triggered Tapestry to collected collect all the ordering information.
For our service contribution methods, we must provide a parameter of type OrderedConfiguration:
Code Block | ||||
---|---|---|---|---|
| ||||
public static void contributeStartup(OrderedConfiguration<Runnable> configuration)
{
configuration.add("JMS", new JMSStartup());
configuration.add("FileSystem", new FileSystemStartup(), "after:CacheSetup");
} |
Often, you don't care about ordering, ; the first form of the add method is used then. The ordering algorithm will find a spot for the object (here the JMSStartup instance) based on the constraints of other contributed objects.
...
Null values, once ordered, are edited out (the List passed to the service builder method does not include any nulls). Again, they are allowed as placeholders, for the actual contributed objects to organize themselves around.
Since | ||
---|---|---|
| ||
When using {{add()}} without any specific constraints, a default constraint is added: after the previously added element. These default constraints are only within a single contribution method, but makes it much easier to set the order of several related contributions. Note that the contributions will be ordered relative to each other and it's possible that contributions by some other module or method may be interspersed between them. |
Mapped Configurations
As discussed in the earlier examples, mapped configurations are also supported. The keys passed in must be unique. When conflicts occur, Tapestry will log warnings (identifying the source of the conflict, in terms of invoked methods, of the conflict), and ignore the conflicting value.
...
For mapped configurations where the key type is String, a CaseInsensitiveMap will be automatically used (and passed to the service builder method), to help ensure that case insensitivity is automatic and pervasive.
...
Anchor | ||||
---|---|---|---|---|
|
Configuration Overrides
Since | ||
---|---|---|
| ||
...
In Tapestry 5.0, services that wanted to support this kind of override behavior had to implement it on an ad-hoc basis, such as ApplicationDefaults overriding FactoryDefaults. In many cases, that is still useful.
...
Scrollbar |
---|