Dienstag, 22. Mai 2012


Artikel

September 2006 | Artikel

JavaServer Faces: Integrationsstrategien für AJAX Fortsetzung, Teil 3

Teil 1   Teil 2   Teil 3   

Probleme der zweiten Strategie
Die zweite Variante ist wesentlich eleganter als die erste Strategie. Allerdings hat auch sie einige Mängel. Zunächst ist anzumerken, dass das Servlet lediglich auf den HTTP-Parameter form:eingabe reagiert. Ändert der Page Author die Komponenten-ID, ist das Servlet nutzlos. Daneben muss der Page Author sicherstellen, dass das Servlet immer bereitsteht. Eine vollständig auf JSF-Mitteln basierte Lösung, die ohne ein Servlet oder einen Filter funktioniert, ist vonnöten.

  1. Listing 6
  2. --------------------------------------------------------------------
  3. <f:view>
  4. <h:form id="form">
  5. <h:panelGrid columns="2">
  6. <h:outputText value="Eingabe"/>
  7. <ajax:inputText2 id="eingabe" value="#{ajaxbacking.eingabe}"/>
  8. <h:commandButton value="Senden"/>
  9. <h:outputText escape="false" value="Wert: <b>#{ajaxbacking.eingabe}</b>" rendered="#{!empty ajaxbacking.eingabe}"/>
  10. </h:panelGrid>
  11. </h:form>
  12. </f:view>
  1. Listing 7
  2. -----------------------------------------------------------------
  3. public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
  4. ...
  5. String actionURL = getActionURL(context);
  6. out.startElement("script", null);
  7. out.writeAttribute("type", "text/javascript", null);
  8. out.writeText("\ntry {\n" +
  9. "new Ajax.Autocompleter('"+component.getClientId(context)+"', '"+DIV_ID+"', '"+
  10. context.getExternalContext().encodeActionURL(actionURL+"?ajaxRequestBy="+component.getClientId(context)) +
  11. "');\n" +
  12. "} catch(e) {\n" +
  13. "alert(e.message);\n"+
  14. "}", null);
  15. out.endElement("script");
  16. }
  17. private void encodeResources(FacesContext context, UIComponent component) throws IOException {
  18. String actionURL = getActionURL(context);
  19. String resourceLocation = context.getExternalContext().encodeActionURL(actionURL+"?ajaxResource=");
  20. JSHelper.addJavaScript(InputSuggestRenderer.class, "controls.js", resourceLocation, context);
  21. ...
  22. }
Zum dritten Akt
Die JSP der dritten Strategie ist ebenfalls kompakt (Listing 6). Hier benötigt der Page Author für den <ajax:inputText2/>-Tag keinen URL. Der Component Writer muss für diese Komponente neben einer Tag- und einer Renderer-Klasse eine weitere Klasse erstellen, die das JSF-Interface PhaseListener implementiert. Mit einem PhaseListener kann sich der Entwickler in die Phasen des JSF-Lifecycle einklinken. Der PhaseListener wird hier für die Bearbeitung des AJAX-Requests und die Auslieferung der Ressourcen genutzt. Die Tag-Klasse ist ähnlich zu der aus Listing 4, außer dass sie nicht die Eigenschaft resourceURL besitzt. Die Renderer-Implementierung zeigt jedoch Unterschiede auf, sodass in Listing 7 die entscheidenden Stellen sichtbar sind. Die Methode encodeBegin() erzeugt analog zum Renderer der zweiten Strategie das Eingabefeld, das <div>-Element und die JavaScript-Anweisung. Allerdings wird hier kein Servlet-Mapping übergeben, sondern der URL der aktuellen JSF-Seite (actionURL). Dadurch verarbeitet die JSF-Engine den AJAX-Request. Ihr wird der HTTP-Parameter ajaxRequestBy angefügt, der die ID der Komponente enthält. Diese wird im nachfolgend beschriebenen PhaseListener bei der Request-Verarbeitung ausgelesen. Die Methode encodeResources() benutzt ebenfalls den JSF-URL für die auszuliefernden Ressourcen. Der HTTP-Parameter ajaxResource identifiziert deren Dateinamen.

  1. Listing 8
  2. ------------------------------------------------------------------------
  3. package net.wessendorf.jm.faces.ajax.component.advanced;
  4. import java.io.*;
  5. import java.net.*;
  6. import java.util.*;
  7. import javax.faces.context.FacesContext;
  8. import javax.faces.event.*;
  9. import javax.servlet.http.HttpServletResponse;
  10. import net.wessendorf.jm.helper.LookupUserHelper;
  11. import org.apache.myfaces.portlet.PortletUtil;
  12. public class AjaxPhaseListener implements PhaseListener , IAjaxController {
  13. private final String AJAX_REQUEST = "ajaxRequestBy";
  14. private final String AJAX_RESOURCE = "ajaxResource";
  15. public void afterPhase(PhaseEvent event) {
  16. FacesContext context = event.getFacesContext();
  17. boolean ajaxRequest = context.getExternalContext().getRequestParameterMap().containsKey(AJAX_REQUEST);
  18. boolean ajaxResource = context.getExternalContext().getRequestParameterMap().containsKey(AJAX_RESOURCE);
  19. if(ajaxRequest)
  20. handleAjaxRequest(context);
  21. if(ajaxResource)
  22. handleResourceRequest(context);
  23. }
  24. public PhaseId getPhaseId() {
  25. return PhaseId.RESTORE_VIEW;
  26. }
  27. public void handleAjaxRequest(FacesContext context) {
  28. if(PortletUtil.isPortletRequest(context))
  29. return;
  30. String clientId = (String) context.getExternalContext().getRequestParameterMap().get(AJAX_REQUEST);
  31. String suggestedInputValue = (String) context.getExternalContext().getRequestParameterMap().get(clientId);
  32. List li = LookupUserHelper.getInstance().lookupUsers(suggestedInputValue);
  33. StringBuffer sb = new StringBuffer();
  34. sb.append("<ul>");
  35. for (Iterator it = li.iterator(); it.hasNext();) {
  36. sb.append("<li>"+it.next()+"</li>");
  37. }
  38. sb.append("</ul>");
  39. HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse();
  40. response.setCharacterEncoding("UTF-8");
  41. OutputStreamWriter out = null;
  42. try {
  43. out = new OutputStreamWriter(response.getOutputStream(), response.getCharacterEncoding());
  44. out.write(sb.toString());
  45. out.flush();
  46. out.close();
  47. context.responseComplete();
  48. } catch (IOException e) {
  49. e.printStackTrace();
  50. }
  51. }
  52. ...
  53. }
PhaseListener
Der PhaseListener, der die Ressourcen ausliefert und die AJAX-Requests behandelt, wird in der ersten Phase des Lifecycles verwendet (Listing 8). Das PhaseListener-Interface definiert die Methoden beforePhase() und afterPhase(), wobei hier nur die Methode afterPhase() benötigt wird. Zu Beginn dieser Methode wird überprüft, ob der Browser Ressourcen anfordert (if(ajaxResource)), um die Seite dem Benutzer zu präsentieren, oder ob der Benutzer eine Eingabe gemacht hat (if(ajaxRequest)). Anhand der Methode handleAjaxRequest() wird die Verarbeitung des AJAX-Requests in einem PhaseListener vorgestellt werden. Das Ausliefern der Ressourcen erfolgt nach dem gleichen Muster. Das Interface IAjaxController dient lediglich der Übersicht. Die Methode handleAjaxRequest() prüft vor der Auslieferung der Benutzerliste mit der MyFaces-Hilfsklasse Portlet-Util, ob der abgesendete AJAX-Request innerhalb eines PortletContainer erfolgt. Ist das der Fall, geschieht nichts, da dieses Beispiel lediglich den ServletContainer abdeckt. Im Fall des ServletContainer wird der HTTP-Parameter ajaxRequestBy, der die Client-ID des Eingabefeldes enthält, ausgelesen. Diese wird benötigt, um die Formulareingabe zu lesen und in deren Abhängigkeit die Benutzerliste zu erzeugen. Nachdem die Liste in den HttpServletResponse geschrieben wurde, erfolgt ein Aufruf der Methode responseComplete(). Dadurch wird der JSF-Lifecycle abgebrochen und die Liste geht direkt zum Webbrowser zurück, wo sie nun dem Webbenutzer angezeigt wird (Abb. 2). Konfiguriert wird diese Komponente analog zu der vorherigen. Allerdings muss der PhaseListener ebenfalls in der Datei faces-config.xml eingetragen werden:

  1. <lifecycle>
  2. <phase-listener>
  3. net.wessendorf.jm.faces.ajax.component.advanced.AjaxPhaseListener
  4. </phase-listener>
  5. </lifecycle>
Alles gut?
Die dritte Strategie scheint eine saubere Lösung zu sein, da sie keine externen Ressourcen wie ein Servlet oder einen Servlet-Filter benötigt. Die AJAX-Komponente ist ebenfalls vor Änderungen der IDs sicher, sodass ein Page Author diese Komponente ohne Bedenken einsetzen kann. Allerdings hat auch diese Komponente einen kleinen Schönheitsfehler, der jedoch nicht ohne Weiteres lösbar ist. Die Komponente ist an den PhaseListener gebunden, um mögliche Benutzer zu laden. Schön wäre es, wenn die Komponente ein weiteres Attribut hätte, das eine Methode innerhalb einer JSF-BackingBean für die Vervollständigung aufruft, wie beispielsweise <ajax:inputText lookup="#{bean.lookup}" ... />. Allerdings kann der PhaseListener nicht ohne Weiteres auf die Eigenschaften der Komponente bei der Request-Verarbeitung zugreifen. Dazu müsste der gesamte Zustand der JSF-Anwendung an den PhaseListener übertragen werden. Die JSF-Spezifikation sieht dazu ein hidden-HTML-Feld vor, lässt jedoch die Implementierungsdetails offen. Eine Realisierung wäre damit proprietär und würde nicht mit allen JSF-Implementierungen funktionieren. Dieses Problem ist der Expertengruppe des JSF-Gremiums jedoch bekannt und wurde schon für die Version 1.2 gelöst. Die aufgelisteten Beispiele basieren auf der Referenzimplementierung von Sun. Apache MyFaces bietet ebenfalls AJAX-Komponenten, die dieses Problem auf die "MyFaces-Art" lösen.

Ausblick und Fazit
Die hier vorgestellten Strategien stützen sich weitgehend auf JSF-Bordmittel und sind ideal für die ersten Gehversuche mit JSF und AJAX. Für komplexere Anwendungsfälle wird allerdings einiges an Mehraufwand notwendig. Innerhalb der JSF-Community entstehen gerade verschiedene Frameworks für die Unterstützung von AJAX. Eines dieser Frameworks ist das Mabon Framework. Es stellt einen eigenen JSF Lifecycle für eine AJAX-basierte Beschaffung von Daten. Für die Kommunikation mit den Backing Beans nutzt es JSON (JavaScript Object Notation). Das Apache Shale Framework bietet mittels seiner Remoting-Komponente spezielle Prozessor-Klassen für die Verarbeitung von AJAX-Requests an. Daneben existieren zahlreiche kommerzielle und freie AJAX-Komponenten-Bibliotheken, wie beispielsweise ICEFaces oder die Sandbox des Apache-MyFaces-Projekts. Insgesamt bleibt festzuhalten, dass die Implementierung von AJAX-basierten JSF-Komponenten eine lohnenswerte Sache ist. Der Einsatz von JSF 1.2 löst kleinere Probleme der Vorgängerversion. Aber auch die Möglichkeiten, die die JSF-Community bereitstellt, helfen dem angehenden Component Writer sehr. Ein Page Author kann ebenfalls aufatmen, da es bereits verschiedene AJAX-Komponenten gibt.

Matthias Weßendorf ist Autor verschiedener Fachbücher und Artikel sowie Sprecher auf internationalen Konferenzen. Daneben ist er Committer einiger Apache-Projekte, wie beispielsweise Apache Shale.
  1. JavaServer Faces 1.0/1.1
  2. JavaServer Faces 1.2
  3. Google Suggest
  4. JavaScript-Bibliothek
  5. Ruby on Rails
  6. Apache MyFaces
  7. Afresco CMS
  8. Java BluePrints Solutions Catalog
  9. Mabon
  10. Apache Shale
  11. ICEFaces
  12. AJAX-Petstore
  13. Ed Burns’ blog

Teil 1   Teil 2   Teil 3   

Kommentare