Hi,

Because the "dynamic service" approach might be conceptually tricky, I came up with a different approach, called configuration groups. This doesn't mean that I've thrown "dynamic services" away, just that I spend some time on a different solution. It's a free market, I suppose the best solution wins. I'm also not sure if there IS an actual "market" for dynamic services.

Summary: you can add @ConfigurationGroup("...") to N services, then if you contribute M markers, NxM services will be created. Service dependencies are automatically resolved to use services with the same marker.

In order to prevent any other core overloads in your (and Howard's) brain, I'll try to explain how it works step by step.

=== CONFIGURATION GROUPS ===

A configuration group means that of the same service implementation, multiple services will be created, for example with id's "Service:Red", "Service:Blue" and markers @Red and @Blue. The id's are automatically generated in the format <service>:<marker>, but implementations do not need to be aware of this. In fact, if there are name clashes, they are automatically resolved by adding a number. By default, if no markers are contributed, only "Service" will be created without markers. If "null" is contributed to the configuration group, this service will be created as well.

I defined an annotation that can be added to a service:
@ConfigurationGroup("yourGroupIdHere")
public Service buildService(...) { ... }

This works with the service binder too:
binder.bind(interface, implementation).withConfigurationGroup("hibernate-core");

You can contribute to configuration groups using a second annotation I defined:
@ContributeConfigurationGroup("yourGroupIdHere")
public static void contribute(Configuration<Class> configuration) {
    configuration.add(Red.class);
    configuration.add(Blue.class);
    configuration.add(null);
}

This method MUST be "public static void (Configuration<Class>)". This is important. We want to know the configurations to create before creating the Module objects, it makes life much simpler. If you add a "null" marker, there will also be a configuration without added markers.

A contribution to the original service will be contributed to all its copies as well. You can contribute to specific copies using their marker:
Contribute(Service.class) @Red
public void someMethodName(...)

=== INJECTION ===

We can use a third annotation to get information about our marker(s):
public static Service buildService(@ConfigurationGroupMarker Class myMarker, @ConfigurationGroupMarker Set<Class> allMarkers) { ... }

Indeed, the same annotation (@ConfigurationGroupMarker) is used for Class and for Sets of Class. You can ONLY use this annotation on services that are in a configuration group.

The set of markers:
- empty or null, meaning that the services are defined without markers
- non-empty, including null, meaning that the services are defined without markers and with all markers - non-empty, without null, meaning that the services are only defined with markers

Now there are two extra injection rules, to make sure that all services in the same configuration group only use services that have their marker as well: * @InjectService(serviceId) and serviceId is in our configuration group : get service with same marker * Injection of a service interface also in our configuration group: first try to resolve it with my marker added, then just default behavior

So you can have the parameter "@Local Service something", and it will try to resolve the service with markers @Local and @MyMarker.

There are also extra injection rules for all services (inside or outside configuration groups) and components (in core): * @InjectService with a marker: get the service created from this service with the marker *If an injection results in multiple services with the same interface, but all in the same configuration group, IoC will return the unmarked (null) service if it exists, or throw an exception. * I modified @Inject for components (in core), so it will first resolve services/marker matches and then use the MasterObjectProvider (so they will be handled BEFORE ServiceOverride, just like getObject(...) of the registry already does for the IoC services) ==> was this a bug in tapestry-core?


=== USAGE FOR HIBERNATE ===

I've used this for hibernate-core.
* Add @ConfigurationGroup("hibernate-core") annotation to all relevant services * Add a HibernateUtil class, that uses ServiceActivityScoreboard to find out which sessions are realized (used by hibernate integration with tapestry)
* Modify DefaultHibernateConfigurer to use /hibernate-<marker>.cfg.xml
* Modify CommitAfter to accept arrays of markers

I had to do a little more in hibernate integration with tapestry-core, because it needs to figure out to which session an object belongs (this is where HibernateUtil from core is used, making sure no unnecessary connections are made to the database)

Good news! All test cases work fine if no markers are contributed, so I expect existing applications will work just fine. My test web application also works, so the case of multiple databases works. Also, I can sneak in the ServiceOverride without problems now.


=== TECHNICAL STUFF ===

Now, in order to make it all go smooth, I had to modify the injection stuff in InternalUtils and other objects. Most importantly, I had to add a lot of functionality to ObjectLocator:
* getService(serviceId, serviceInterface, Class configurationMarker)
* getObject(objectType, annotationProvider, boolean required?)
* getObject(serviceInterface, Class... markers)
* getMarkers() = returns all used markers in the Registry, used for filtering of annotations in InternalUtils * Set<Class> getConfigurationGroupMarkers(String groupId) = returns all markers used in a configuration group (including null)

ServiceResources also implements ConfigurationGroupResources with a number of methods, and I added extra methods to InternalRegistry, so RegistryImpl is a bit bigger. These are mostly changes that are necessary to implement the injection changes.

I needed to modify the DefaultInjectionProvider in tapestry-core to use "locator.getObject(...) with required=false", because @Inject with markers would not work otherwise. This may be a bug in Tapestry-core. I'm not sure, but the DefaultInjectionProvider right now uses ONLY the MasterObjectProvider, while its equivalent in IoC first checks services+marker matches.

I'm sitting on a very large number of modified files (77 files right now, 23 new files mostly for test cases and stuff), partially because various services needed tiny changes to work with the new interfaces (e.g. modified tests and mocks)



=== CONCLUSIONS ===

While both approaches work, this approach has the advantage of offering very concise syntax for the general problem of "services with multiple configurations", of which "hibernate with multiple databases" is a specific case.

Dynamic services offers more flexibility, being a more general solution that solves the problem of "services that cannot be defined at compile-time" but in return, it takes a bit more coding to convert a bunch of ordinary services to a bunch of dynamic services, as opposed to just adding an annotation here and there.

I do not have the code on git or anywhere else yet. I ought to add javadoc first... and I expect it will take at least a few weeks until 5.3 anyway, so there's no hurry... ( well I want to use this in a project for a customer, but other than that... ) Also, I still need to add proper test cases to tapestry-hibernate for the multiple databases case, instead of the custom web application I use now.

Anyone know how I can run a test webapp without running all that selenium stuff?

Feel free to give me feedback. I'm sort of poking around blindly if you experts don't say anything.


Tom.

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to