JSF 2.3 new feature: registrable DataModels
While JSF has steadily expanded the number of build-in wrappers and JSF 2.3 has provided new ones for Map and Iterable, a long standing request is for users (or libraries) to be able to register their own wrappers.
JSF 2.3 will now (finally) let users do this. The way this is done is by creating a wrapper DataModel for a specific type, just as one may have done years ago when returning data from a backing bean, and then annotating it with the new @FacesDataModel annotation. A “forClass” attribute has to be specified on this annotation that designates the type this wrapper is able to handle.
The following gives an abbreviated example of this:
@FacesDataModel(forClass = MyCollection.class) public class MyCollectionModel<E> extends DataModel<E> { @Override public E getRowData() { // access MyCollection here } @Override public void setWrappedData(Object myCollection) { // likely just store myCollection } // Other methods omitted for brevity }
Note that there are two types involved here. The “forClass” attribute is the collection or container type that the DataModel wraps, while the generic parameter E concerns the data this collection contains. E.g. Suppose we have a MyCollection<User>, then “forClass” would correspond to MyCollection, and E would correspond to User. If set/getWrappedData was generic the “forClass” attribute may not have been needed, as generic parameters can be read from class definitions, but alas.
With a class definition as given above present, a backing bean can now return a MyCollection as in the following example:
@Named public class MyBacking { public MyCollection<User> getUsers() { // return myCollection } }h:dataTable will be able to work with this directly, as shown in the example below:
<h:dataTable value="#{myBacking.users}" var="user"> <h:column>#{user.name}</h:column> </h:dataTable>
There are a few things noteworthy here.
Traditionally JSF artefacts like e.g. ViewHandlers are registered using a JSF specific mechanism, kept internally in a JSF data structure and are looked up using a JSF factory. @FacesDataModel however has none of this and instead fully delegates to CDI for all these concerns. The registration is done automatically by CDI by the simple fact that @FacesDataModel is a CDI qualifier, and lookup happens via the CDI BeanManager (although with a small catch, as explained below).
This is a new direction that JSF is going in. It has already effectively deprecated its own managed bean facility in favour of CDI named beans, but is now also favouring CDI for registration and lookup of the pluggable artefacts it supports. New artefacts will henceforth very likely exclusively use CDI for this, while some existing ones are retrofitted (like e.g. Converters and Validators). Because of the large number of artefacts involved and the subtle changes in behaviour that can occur, not all existing JSF artefacts will however change overnight to registration/lookup via CDI.
Another thing to note concerns the small catch with the CDI lookup that was mentioned above. The thing is that with a direct lookup using the BeanManager we’d get a very specific wrapper type. E.g. suppose there was no build-in wrapper for List and one was provided via @FacesDataModel. Now also suppose the actual data type encountered at runtime is an ArrayList. Clearly, a direct lookup for ArrayList will do us no good as there’s no wrapper available for exactly this type.
This problem is handled via a CDI extension that observes all definitions of @FacesDataModel that are found by CDI during startup and stores the types they handle in a collection. This is afterwards sorted such that for any 2 classes X and Y from this collection, if an object of X is an instanceof an object of Y, X appears in the collection before Y. The collection's sorting is otherwise arbitrary.
With this collection available, the logic behind @FacesDataModel scans this collection of types from beginning to end to find the first match which is assignable from the type that we encountered at runtime. Although it’s an implementation detail, the following shows an example of how the RI implements this:
getDataModelClassesMap(cdi).entrySet().stream() .filter(e -> e.getKey().isAssignableFrom(forClass)) .findFirst() .ifPresent( e -> dataModel.add( cdi.select( e.getValue(), new FacesDataModelAnnotationLiteral(e.getKey()) ).get()) );
In effect this means we either lookup the wrapper for our exact runtime type, or the closest super type. I.e. following the example above, the wrapper for List is found and used when the runtime type is ArrayList.
Before JSF 2.3 is finalised there are a couple of things that may still change. For instance, Map and Iterable have been added earlier as build-in wrappers, but could be refactored to be based on @FacesDataModel as well. The advantage is be that the runtime would be a client of the new API as well, which on its turn means its easier for the user to comprehend and override.
A more difficult and controversial change is to allow @FacesDataModel wrappers to override build-in wrappers. Currently it’s not possible to provide one own’s List wrapper, since List is build in and takes precedence. If @FacesDataModel would take precedence, then a user or library would be able to override this. This by itself is not that bad, since JSF lives and breathes by its ability to let users or libraries override or extend core functionality. However, the fear is that via this particular way of overriding a user may update one if its libraries that happens to ship with an @FacesDataModel implementation for List, which would then take that user by surprise.
Things get even more complicated when both the new Iterable and Map would be implemented as @FacesDataModel AND @FacesDataModel would take precedence over the build-in types. In that case the Iterable wrapper would always match before the build-in List wrapper, making the latter unreachable. Now logically this would not matter as Iterable handles lists just as well, but in practice this may be a problem for applications that in some subtle way depend on the specific behaviour of a given List wrapper (in all honestly, such applications will likely fail too when switching JSF implementations).
Finally, doing totally away with the build-in wrappers and depending solely on @FacesDataModel is arguably the best option, but problematic too for reasons of backwards compatibility. This thus poses an interesting challenge between two opposite concerns: “Nothing can ever change, ever” and “Modernise to stay relevant and competitive”.
Conclusion
With @FacesDataModel custom DataModel wrappers can be registered, but those wrappers can not (yet) override any of the build-in types.
Arjan Tijms
Comments
Post a Comment