In the previous article, I discussed the basics of Tapestry 5 IoC. I focused on the terseness of Tapestry's container, even though everything occurs in Java code. I alluded to special features of Tapestry 5 IoC, service configurations. Let's start investigating those.
In traditional dependency injection, the relationship between a service and its dependencies is many-to-one: many services may
inject a specific dependency. Whether that dependency is selected just by service type, or by service id, or by some other mechanism, it's still one single object.
Service configurations are somewhat inverted: they are a relationship from one service to many objects. The objects, or contributions, may be simple objects or may themselves be services.
Let's use a specific example from Tapestry to put this into perspective. Previously I showed
the service builder method for the TranslatorSource service:
public static TranslatorSource buildTranslatorSource(ComponentInstantiatorSource componentInstantiatorSource,
ServiceResources resources)
{
TranslatorSourceImpl service = resources.autobuild(TranslatorSourceImpl.class);
componentInstantiatorSource.addInvalidationListener(service);
return service;
}
Let's dive a little deeper and look at what this service does. It's a source for
Translator objects, which are an integral part of Tapestry's HTML form support. Translators are responsible for converting between server-side values (such as numbers, dates, and so forth) and client-side strings. They also play a role in client-side validation of user input.
Tapestry matches up properties that are edited by TextFields with corresponding Translator instances. This all happens inside the TextField component and is largely invisible to programmers. In any case, the TranslatorSource service is central:
public interface TranslatorSource
{
/**
* Returns the translator with the given logical name.
*
* @param name name of translator (as configured)
* @return the shared translator instance
* @throws RuntimeException if no translator is configured for the provided name
*/
Translator get(String name);
/**
* Finds a {@link Translator} that is appropriate to the given type, which is usually obtained via {@link
* org.apache.tapestry5.Binding#getBindingType()}. Performs an inheritanced-based search for the best match.
*
* @param valueType the type of value for which a default translator is needed
* @return the matching translator, or null if no match can be found
*/
Translator findByType(Class valueType);
/**
* Finds a {@link Translator} that is appropriate to the given type, which is usually obtained via {@link
* org.apache.tapestry5.Binding#getBindingType()}. Performs an inheritanced-based search for the best match.
*
* @param valueType the type of value for which a default translator is needed
* @return the matching translator
* @throws IllegalArgumentException if no known validator matches the provided type
*/
Translator getByType(Class valueType);
}
Here's where it gets interesting
So, what Translators are built into Tapestry? You might think you could tell by looking at the implementation of the service:
public class TranslatorSourceImpl implements TranslatorSource, InvalidationListener
{
private final Map<String, Translator> translators = CollectionFactory.newCaseInsensitiveMap();
private final StrategyRegistry<Translator> registry;
public TranslatorSourceImpl(Collection<Translator> configuration)
{
Map<Class, Translator> typeToTranslator = CollectionFactory.newMap();
for (Translator t : configuration)
{
translators.put(t.getName(), t);
typeToTranslator.put(t.getType(), t);
}
registry = StrategyRegistry.newInstance(Translator.class, typeToTranslator, true);
}
public Translator get(String name)
{
Translator result = translators.get(name);
if (result == null)
throw new RuntimeException(ServicesMessages.unknownTranslatorType(name, InternalUtils
.sortedKeys(translators)));
return result;
}
public Translator getByType(Class valueType)
{
Translator result = registry.get(valueType);
if (result == null)
{
List<String> names = CollectionFactory.newList();
for (Class type : registry.getTypes())
{
names.add(type.getName());
}
throw new IllegalArgumentException(ServicesMessages.noTranslatorForType(valueType, names));
}
return result;
}
public Translator findByType(Class valueType)
{
return registry.get(valueType);
}
public void objectWasInvalidated()
{
registry.clearCache();
}
}
But you don't see any pre-defined Translator instances here ... just Collection<Translator> configuration
passed
to the constructor. Each Translator provides its name, and those all go into the translators
map ... but the question is, where do they come from?
Jumping back to TapestryModule, we see a likely method:
public static void contributeTranslatorSource(Configuration<Translator> configuration)
{
configuration.add(new StringTranslator());
configuration.add(new ByteTranslator());
configuration.add(new IntegerTranslator());
configuration.add(new LongTranslator());
configuration.add(new FloatTranslator());
configuration.add(new DoubleTranslator());
configuration.add(new ShortTranslator());
}
It's looking pretty likely that Tapestry supports string, byte, integer, long, float, double and short out of the box.
The naming of this method is another example of convention over configuration. The prefix this time is
contribute
and the rest of the method name matches the service id, TranslatorSource.
The Configuration object has a single method, add()
:
public interface Configuration<T>
{
/**
* Adds an object to the service's contribution.
*
* @param object to add to the service's configuration
*/
void add(T object);
}
So, Tapestry has invoked the contributeTranslatorSource()
method, collected up the objects, the Translators, added to the configuration object, and converted the configuration object to
a Collection, which is ultimately passed to the TranslatorSourceImpl constructor.
Seems awfully complicated, doesn't it? Well, it is nice (from a testing perspective) that TranslatorSourceImpl isn't tied
to any particular implementations of Translator. But that's not the real benefit.
The real benefit, and this is the basis of the entire concept, is that you are not locked into just these Translators. You can define your own, and mix them in with the ones supplied by Tapestry. And you don't have to hack TapestryModule or TranslatorSourceImpl to do it.
Say your application defines a Currency class, to track currency amounts of orders and payments. That might be handy to use instead of double, for accuracy reasons. You might also want to parse and format currency values differently than naked doubles ... for example, to require exactly two digits of precision, or to ignore a leading dollar sign.
To mix in your own Translators, all you need to do is define your own module:
public class AppModule
{
public static void contributeTranslatorSource(Configuration<Translator> configuration)
{
configuration.add(new CurrencyTranslator());
}
}
This translator, CurrencyTranslator, will be indistinguishable from the default set of Translators provided by Tapestry; TranslatorSourceImpl will have no way of telling which Translators came from where. Your contributions are on an even footing with those provided by Tapestry itself.
You might ask in what order are these contribute methods are invoked? The answer is: Who knows? That's why the configuration is passed to the service implementation as (unordered) Collection, not (ordered) List. As we'll see in the next article, Tapestry has alternatives for when you care about ordering, or when you want your configuration in the form of a Map.
When are these methods called? They are called when the TranslatorSource service is realized, which happens when a method of the service is first invoked. Tapestry IoC is by default very lazy, it doesn't instantiate services until necessary. The service's proxy is responsible for this realization process, and its done in a thread-safe manner.
Dealing with this kind of loose binding in a structured manner is very helpful: it means that the TranslatorSource service is simplified: it doesn't need a method to add a new Translator, there's fewer issues related to multiple threads, and the available set of Translators never changes, which makes the behavior of the service much more predictable.
Conclusion
We've only just pierced the surface of Tapestry configurations, but we're beginning to see what I mean when talk about Tapestry's extensibility. Much of the key behavior of Tapestry is specified in terms of this kind of configuration, or one of its close relatives. And, as the example showed, building a service that uses a configuration is just a matter of defining a parameter of type Collection in the service's constructor.
In future articles, I'll discuss other variations of service configurations, and show how to go meta with Tapestry by leveraging configurations in combination with service-building services!