In de vorige post heb ik het over het nieuwe resource management systeem van JSF 2.0 gehad. Dit keer wil ik met een voorbeeldje laten zien wat je kunt met het nieuwe Publish/Subscribe Event System.
Publish/Subscribe
Publish/Subscribe is een algemeen toegepast mechanisme om event driven applicaties te maken. Het werkt zo: Een publisher is de zender van een bericht. Deze verzendt een bericht naar de runtime die Publish/Subscribe verzorgt, zoals middleware of een framework. Op basis van bijvoorbeeld configuratie is bekend welke ontvangers geïnteresseerd zijn in dit bericht en het bericht wordt naar al deze ontvangers verstuurd.
Eigenschappen van Publish/Subscribe zijn:
- Ontkoppeling van zender en ontvanger; het enige dat ze verbindt, is het bericht,
- Een zender kan een bericht naar meerdere ontvangers sturen,
- Een ontvanger kan van meerdere zenders een bericht ontvangen.
Typische Publish/Subscribe voorbeelden zijn een radiostation en RSS feeds. Publish/subscribe in GUI’s wordt doorgaans geimplementeerd middels het Observer pattern.
Laten we eerst kijken hoe JSF 2.0 dit model wil implementeren.
JSF 1.x events
De huidige versie van JSF bevat al een event mechanisme waarmee objecten/componenten events kunnen versturen en ontvangen. Een voorbeeld hiervan is het UICommand component, waarvan <h:commandLink /> en <h:commandButton /> gebruikmaken. Dit component kan ActionEvents versturen, wat ervoor zorgt dat bijvoorbeeld een action method in een managed bean uitgevoerd wordt.
Dit hele systeem draait om de methode queueEvent, gedefinieerd in UIComponent. Met deze methode worden events verstuurd die door de lifecycle op het juiste moment naar de juiste listener verstuurd worden.
Daarnaast heb je PhaseEvents, die aan het begin en aan het einde van elke fase in de lifecycle verstuurd worden en door PhaseListeners afgevangen worden.
Ten slotte definieert de JSF 1.x spec ValueChangeEvents, die afgevuurd worden als de nieuwe waarde (submittedValue) van een UIInput anders is dan de waarde tijdens het renderen van de view.
Naast de standaard events is het mogelijk om eigen events te definiëren, bijvoorbeeld als je een AJAX component library ontwikkelt.
JSF 2.0
In JSF 2.0 wordt een nieuw soort event geïntroduceerd, namelijk het SystemEvent. Dit event wordt gebruikt om op specifieke momenten in te prikken in de JSF applicatie. Op het moment zijn er 4 soorten SystemEvents gedefinieerd, namelijk:
- AfterAddToParentEvent
Geeft aan dat het component (de source parameter) is toegevoegd aan de component tree.
- BeforeRenderEvent
Geeft aan dat het component (de source parameter) straks gerenderd wordt. Dit is het moment om last minute aanpassingen te doen.
- ViewMapCreatedEvent
De view scope is aangemaakt. Zie View scope.
- ViewMapDestroyedEvent
De view scope is verwijderd. Zie View scope.
De spec is echter nog niet definitief, dus er kunnen nog events bijkomen. Daarnaast kun je zelf events definiëren.
Deze events maken het mogelijk om tweaks te doen aan de lifecycle van JSF, zonder terug te hoeven vallen op low-level zelfbouwoplossingen, zoals eigen PhaseListeners. Wie zelf weleens een AJAX component geschreven heeft met een PhaseListener (om het AJAX request af te vangen of voor resource requests), weet hoe onhandig het werkt en hoe foutgevoelig het is.
Bovendien wordt integratie van component libraries hiermee stukken makkelijker, aangezien ze onder water allemaal van dezelfde mechanismen gebruik maken. Nu nog hopen dat het voldoende flexibiliteit biedt voor libraries zoals Trinidad en Ajax4jsf, zodat ze hun eigen (Filter/RenderKit/ContextListener) oplossingen te migreren naar de officiële manier.
Eerlijk gezegd valt het systeem me tot nu toe nog wat tegen, voornamelijk het registreren van event listeners is niet echt handig. De voorbeeldapplicatie die met de JSF 2.0 EDR meegeleverd wordt, bevat een hack in de meegeleverde Facelets versie (de methode apply in ValidateHandler is non-final gemaakt) die het mogelijk maakt om de listener te registreren. Zonder een dergelijke hack kun je niet fatsoenlijk listeners registreren. Ik hoop dat ze een soort configuratie systeem toevoegen, want nu moet je weer terugvallen op PhaseListeners.
View scope
In JSF 2.0 is een nieuwe scope gedefinieerd, naast de bestaande scopes (request, session, application), namelijk de View scope. De spec zegt nog niets over de view scope, maar conceptueel is de view scope al wel bekend.
https://javaserverfaces-spec-public.dev.java.net/issues/show_bug.cgi?id=290
De view scope zit tussen request en sessie in qua lifetime. De view scope begint op het moment dat een UIViewRoot instantie aangemaakt wordt en eindigt als een nieuwe UIViewRoot instantie actief wordt gemaakt via FacesContext.setViewRoot.
Concreet betekent dit dat zolang je op dezelfde pagina blijft (bijvoorbeeld een invoerscherm, waarin je – voordat je op “Save” drukt – nog wat extra acties doet), de data in de view scope in leven blijft. Op het moment dat je naar de volgende pagina (bijvoorbeeld van detail terug naar master) navigeert, wordt de view scope gereset.
Hoe werkt het?
Om dit alles mogelijk te maken zijn er een paar aanpassingen gedaan aan de API.
SystemEvent (nieuw)
Dit is de base klasse waarvan alle SystemEvents moeten overerven.
SystemEventListener (nieuw)
Dit is een nieuwe interface, die aangeeft dat de implementerende klasse als listener kan dienen voor SystemEvents. Er zijn twee methoden gedefinieerd, namelijk isListenerForSource en processEvent. processEvent spreekt voor zich, die wordt aangeroepen met als parameter de SystemEvent instantie. isListenerForSource wordt gebruikt om de calls van processEvent te beperken. Bijvoorbeeld tot events die gegooid zijn door instanties van een bepaald type. Je kunt een SystemEventListener dus configureren om alleen events van bepaalde componenten te verwerken.
ComponentSystemEventListener (nieuw)
Deze interface doet hetzelfde als SystemEventListener, maar bevat geen methode isListenerForSource, aangezien de koppeling met een bepaald type component al voor een impliciete variant van deze methode zorgt.
UIComponent (toevoegingen)
Hieraan zijn nieuwe methoden toegevoegd, namelijk subscribeToEvent, unsubscribeFromEvent en getListenersForEventClass.
public void subscribeToEvent(FacesContext context,
Class<? extends SystemEvent> eventClass,
ComponentSystemEventListener componentListener)
Met subscribeToEvent kun je componentListener aan deze component instantie koppelen die alle events afvangt die van het type eventClass zijn.
Met unsubscribeToEvent doe je precies het tegenovergestelde, namelijk de listener weer verwijderen.
getListenersForEventClass returned een lijst met alle listeners die events van het opgegeven type afvangen.
Application (toevoegingen)
Hieraan zijn ook een aantal methoden toegevoegd, namelijk subscribeToEvent, unsubscribeFromEvent en publishEvent. De eerste twee zijn volgens mij wel duidelijk, maar nu zijn de event handlers niet gekoppeld aan een specifieke component instantie. Met publishEvent kun je een event opgooien die dan door de JSF runtime verwerkt wordt.
@ListenerFor (nieuw)
Ten slotte is een annotation gemaakt, genaamd ListenerFor. Klassen die hiermee geannoteerd zijn, worden als listener geregistreerd. Dit gebeurt op dezelfde manier als hoe de subscribeToEvent methoden op Application en UIComponent werken. Dit is de verantwoordelijkheid van JSF.
Als je de annotation gebruikt heb je minder last van het feit dat er geen fatsoenlijk configuratiemechanisme is.
Voorbeeld
Iedereen (die met JSF gewerkt heeft) heeft het weleens gehad. Er treedt een validatiefout op, maar je hebt het niet door, omdat er geen <h:messages /> tag op de pagina ligt. Er vervelend. In dit voorbeeld laat ik een oplossing hiervoor zien met behulp van het beschreven event mechanisme.
We gaan door met de code uit het vorige voorbeeld. Ik beschrijf hier alleen de onderdelen die betrekking hebben op dit onderwerp.
Het idee is dat we op het moment dat we de view gaan renderen, kijken of er al een <h:messages /> tag op de pagina ligt. Als dit niet het geval is, voegen we deze programmatisch toe.
Een extra detail, dit mag alleen zichtbaar zijn tijdens development, daarom moeten we de volgende code toevoegen aan web.xml:
web.xml
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
Om te beginnen, zorgen we dat er geen <h:messages /> tag in de pagina ligt. Zo forceren we dat ons mechanisme afgaat. Zie de volgende pagina.
homepage.xhtml
<html ... >
<h:head>
<title>JSF2 test</title>
</h:head>
<h:body>
<p>Hello, you are logged in. </p>
<h:form>
<h:commandButton action="#{loginBean.addMessage}" value="Click" />
<hr />
<h:commandLink action="loginPage">Back to login page</h:commandLink>
</h:form>
</h:body>
</html>
Dan maken we de event listener klasse, die ervoor zorgt dat een <h:messages /> tag aan de pagina wordt toegevoegd. In dit voorbeeld heb ik er zelfs een custom component van gemaakt, zodat je tijdens development goed kunt zien dat dit component automatisch toegevoegd is. Dat doen we in de volgende klasse.
AddDefaultMessagesListener.java
public class AddDefaultMessagesListener implements SystemEventListener {
public boolean isListenerForSource(Object source) {
return source instanceof UIViewRoot;
}
public void processEvent(SystemEvent evt) throws AbortProcessingException {
if (!(evt instanceof BeforeRenderEvent)) {
String klazz = (evt != null) ? evt.getClass().getName() : "null";
throw new AssertionError("Event '"+evt+"' is not of type BeforeRenderEvent, but of type '"+klazz+"'");
}
FacesContext fc = FacesContext.getCurrentInstance();
if (fc.getApplication().getProjectStage().equals(ProjectStage.Development)) {
if (!hasMessagesComponent(fc, fc.getViewRoot())) {
String viewId = fc.getViewRoot().getViewId();
System.out.println("No <h:messages /> tag found in view '"+viewId+"'. ");
addMessagesComponentToPage(fc, fc.getViewRoot());
}
}
}
private void addMessagesComponentToPage(FacesContext fc, UIViewRoot viewRoot) {
String viewId = fc.getViewRoot().getViewId();
UIForm form = findFirstForm(fc, viewRoot);
if (form != null) {
System.out.println("Adding default <h:messages /> tag to view '"+viewId+"' in the first form on the page. ");
form.getChildren().add(0, new DummyUIMessages());
} else {
System.out.println("No form found while trying to add default <h:messages /> tag to view '"+viewId+"'. No <h:messages /> added. ");
}
}
private boolean hasMessagesComponent(FacesContext fc, UIComponent comp) {
if (comp instanceof UIMessages) {
return true;
}
for (UIComponent child : comp.getChildren()) {
if (hasMessagesComponent(fc, child)) {
return true;
}
}
return false;
}
private UIForm findFirstForm(FacesContext fc, UIComponent comp) {
if (comp instanceof UIForm) {
return (UIForm) comp;
}
for (UIComponent child : comp.getChildren()) {
UIForm form = findFirstForm(fc, child);
if (form != null) {
return form;
}
}
return null;
}
}
Deze code zal vast niet optimaal zijn, al is het maar dat er naar de std out gelogged wordt. Ook zullen de zoekfuncties niet bijzonder efficiënt zijn, al weet ik zo snel geen snellere manier dan een vorm van caching en dat is weer erg fragiel aangezien de component tree nogal dynamisch is.
Deze listener wordt nog niet aangeroepen, want hij is nog niet geregistreerd. Dat doen we nu.
ConfigPhaseListener.java
public class ConfigPhaseListener implements PhaseListener {
private static final Object LOCK = new Object();
private static boolean initialized = false;
//@BadCode(type="reallyBadCodeDoNotUseInProduction")
public ConfigPhaseListener() {
synchronized (LOCK) {
if (!initialized) {
AddDefaultMessagesListener listener = new AddDefaultMessagesListener();
FacesContext fc = FacesContext.getCurrentInstance();
fc.getApplication().subscribeToEvent(BeforeRenderEvent.class, listener);
initialized = true;
}
}
}
public void afterPhase(PhaseEvent event) {}
public void beforePhase(PhaseEvent event) {}
public PhaseId getPhaseId() {
return PhaseId.RESTORE_VIEW;
}
}
Deze code is een beetje eng. Ten eerste wordt er in de constructor gesynchroniseerd. Dit is sowieso alleen maar mogelijk omdat het op een static variabele gebeurt, want in de constructor synchroniseren op this is gevaarlijk, aangezien this naar een algemeen beschikbaar object wijst. Ook staat er een static boolean initialized die in de constructor op true gezet wordt. De reden dat die er staat, is dat in de JSF spec niets gezegd wordt over de lifecycle van PhaseListeners.
Deze twee constructies (syncen en initalized boolean) zorgen er samen voor dat er niet meerdere instanties van de listener geregistreerd worden. Meerdere listeners zorgen namelijk voor dubbele output in de HTML pagina en dat willen we niet.
Note: Dit is absoluut niet production ready! Ik heb bijvoorbeeld geen rekening gehouden met geclusterde deployments.
We hebben nu de SystemEventListener en een PhaseListener om deze te registreren, ter volledigheid zal ik de faces-config nog even laten zien.
faces-config.xml
<lifecycle>
<phase-listener>nl.ordina.jsf2.bean.ConfigPhaseListener</phase-listener>
</lifecycle>
Ten slotte mijn eigen UIMessages implementatie, die duidelijk laat zien dat je een fout gemaakt hebt.
public class DummyUIMessages extends UIMessages {
public void encodeEnd(FacesContext context) throws IOException {
ResponseWriter writer = context.getResponseWriter();
assert(writer != null);
writer.startElement("div", this);
writer.writeAttribute("style", "border: 1px solid #FF0000", null);
writer.startElement("h1", this);
writer.writeAttribute("style", "color: #FF0000", null);
writer.write("Automatically inserted messages tag, for development purposes only. ");
writer.write("");
writer.write("Add a <h:messages /> tag to remove this crap. ");
writer.endElement("h1");
super.encodeEnd(context);
writer.endElement("div");
}
}
Je kunt waarschijnlijk wel zien wat deze code doet. Er wordt een dikke vette grote rode tekst getoond met een waarschuwing. Daaronder staan de messages.
En om het te bewijzen, hier een screenshot.

Conclusie
Het nieuwe event mechanisme staat nog in zijn kinderschoenen, maar er staat wel een vrij goede basis waarmee je op zijn minst aan de gang kunt. Ik heb momenteel maar een paar dingen aan te merken, namelijk:
- Ik wil nog meer soorten events, bijvoorbeeld events voor Exception handling, of events die veroorzaakt worden door specifieke lifecycle events, zoals validatiefouten waardoor de lifecycle kortsluit.
- Ook zijn de event listeners nog niet echt goed te configureren. De API is er, maar waar roep je die methoden aan? Een ServletContextListener is geen optie, want die valt buiten de JSF FacesContext. Ik heb voor bovenstaand voorbeeld een PhaseListener gebruikt, maar dat is ook niet ideaal, aangezien een PhaseListener bedoeld is om faseovergangen te herkennen en niet gemaakt is voor initialisatie van je framework. JSF biedt hier echter geen hooks voor. Bovendien geeft het overhead. Deze overhead valt op zich wel mee, aangezien de logica in de constructor staat, maar dat is ook een hack, want de spec zegt niets over de lifecycle van PhaseListeners. Dat is ook de reden van deze overdreven defensieve constructie. Het is simpelweg een hack, maar hij werkt wel en deze extra checks maken hem ook portable over JSF implementaties.
- Bovendien zou een meer declaratieve manier van configureren prettig zijn. Er zit een annotation bij, maar ik geef de voorkeur aan een XML config file, zoals een paar extra tags in faces-config.xml. Misschien kunnen ze bij JSF ook overstappen op namespaces zoals bij Spring.
Download sources
Klik hier voor de voorbeeldcode. Er zit ook een simpel autocomplete component bij dat gebruikmaakt van het nieuwe event mechanisme om AJAX requests af te handelen.

Note: ook dit autocomplete component is niet production-ready! Ik heb geen rekening gehouden met performance, security, robuuste code, JSF implementatie portability, Java versie, cross browser issues, client side memory usage, etc. Ook zul je de Prototype.js dependency waarschijnlijk niet willen.
Bronnen
Weblog van Ryan Lubke, waar nieuwe features van JSF 2.0 beschreven worden.
http://blogs.sun.com/rlubke/entry/jsf_2_0_new_feature1
De download pagina van de JSF 2.0 specificatie.
http://jcp.org/aboutJava/communityprocess/edr/jsr314/index.html