Mechanizm rozwiązywania zmiennych w JSF - variable-resolver

Z Jacek Laskowski - Wiki Projektanta Java EE

Podczas tworzenia aplikacji JSF nie sposób nie korzystać z języka Unified EL (język wyrażeń wykorzystywany w JSP oraz JSF). Konstrukcje typu #{ziarno.atrybut} czy ${zmienna.atrybut} są na porządku dziennym podczas pracy z JSF.

W trakcie pracy z rozszerzeniem EJB 3.0 dla IBM WebSphere Application Server 6.1 (WAS) natrafiłem na problem z mechanizmem wstrzeliwania zależności w aplikacji JSF. WAS 6.1 jest serwerem aplikacji wspierającym specyfikację Java EE 1.4 i część specyfikacji Java EE 5 jest realizowana przez tzw. rozszerzenia funkcjonalne (ang. feature packs), które dostarczają wybrane części specyfikacji. Jedną z usług, której brakuje jest usługa wstrzeliwania zależności dla ziaren zarządzanych JSF. Innymi słowy, dla WAS 6.1 ziarna zarządzane JSF nie są w pełni zarządzane i mechanizm wstrzeliwania zależności ich nie obsługuje. Konstrukcje typu @EJB w przypadku ziaren JSF nie są obsługiwane i kończy się zazwyczaj na NullPointerException. W ten sposób natrafiłem na kolejną funkcjonalność JSF - komponenty rozwiązujące zmienne (ang. variable resolvers), dalej zwane komponentami rozwiązującymi.

Rozdział 5 Expression Language and Managed Bean Facility specyfikacji JavaServer Faces 1.2 opisuje temat komponentów rozwiązujących w szczegółach. Ja sprowadzę temat do niezbędnego minimum, za pomocą którego udało mi się obejść niepełne wsparcie dla elementów Java EE 5 przez WAS 6.1.

Czym jest mechanizm rozwiązywania zmiennych? Przyjrzyjmy się prostej stronie JSF - index.jsp.

 <%@page contentType="text/html" pageEncoding="UTF-8"%>
 
 <%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%>
 <%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%>
 
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
 
 <html>
     <head>
         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
         <title>Variable Resolver demo</title>
     </head>
     <body>
         <f:view>
             <h1><h:outputText value="Zwykły tekst" /></h1>
         </f:view>
     </body>
 </html>

Nic nadzwyczajnego. Prosta strona JSF z kontrolką h:outputText, której zadaniem jest wyświetlenie tekstu zawartego w atrybucie value. Wynikiem uruchomienia strony jest wyświetlenie tekstu Zwykły tekst. Dokładnie taki sam efekt uzyskalibyśmy bez korzystania z kontrolki h:outputText.

 <%@page contentType="text/html" pageEncoding="UTF-8"%>
 
 <%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%>
 <%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%>
 
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
 
 <html>
     <head>
         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
         <title>Variable Resolver demo</title>
     </head>
     <body>
         <f:view>
             <h1>Zwykły tekst</h1>
         </f:view>
     </body>
 </html>

Jeśli tak, to nasuwa się pytanie, po co korzystać z h:outputText? Dzięki niemu mamy możliwość włączania/wyłączania tekstu dynamicznie za pomocą atrybutu rendered, np. w zależności od roli użytkownika, czy obecności parametru formularza.

 <%@page contentType="text/html" pageEncoding="UTF-8"%>
 
 <%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%>
 <%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%>
 
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
 
 <html>
     <head>
         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
         <title>Variable Resolver demo</title>
     </head>
     <body>
         <f:view>
             <h1><h:outputText value="Zwykły tekst" rendered="#{param['pokaz']}"/></h1>
         </f:view>
     </body>
 </html>

W powyższym przykładzie, tekst pojawi się wyłącznie, jeśli wywołano stronę z parametrem pokaz, który ma wartość true, np. http://localhost:8080/resolver/faces/index.jsp?pokaz=true. Zmienna param reprezentuje w JSF mapę wszystkich parametrów wejściowych i dostęp do ich wartości odbywa się przy pomocy wyrażenia tablicowego zmienna[atrybut] bądź wyrażenia prostego zmienna.atrybut. Poniższa strona jest analogiczna funkcjonalnie do poprzedniej.

 <%@page contentType="text/html" pageEncoding="UTF-8"%>
 
 <%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%>
 <%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%>
 
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
 
 <html>
     <head>
         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
         <title>Variable Resolver demo</title>
     </head>
     <body>
         <f:view>
             <h1><h:outputText value="Zwykły tekst" rendered="#{param.pokaz}"/></h1>
         </f:view>
     </body>
 </html>

Możnaby również skorzystać z funkcji empty i skonstruować wyrażenie EL jak w poniższym przykładzie.

 <%@page contentType="text/html" pageEncoding="UTF-8"%>
 
 <%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%>
 <%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%>
 
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
 
 <html>
     <head>
         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
         <title>Variable Resolver demo</title>
     </head>
     <body>
         <f:view>
             <h1><h:outputText value="Zwykły tekst" rendered="#{!empty param['pokaz']}"/></h1>
         </f:view>
     </body>
 </html>

co odczytujemy jako: wyświetl tekst, jeśli parametr pokaz istnieje oraz jego wartość jest niepusta, np. http://localhost:8080/resolver/faces/index.jsp?pokaz=cokolwiek.

I możnaby tak dalej konstruować coraz bardziej skomplikowane wyrażenia EL, aż...możliwości dostarczane przez język Unified EL nie spełnią naszych oczekiwań, np. wywoływanie metod z parametrami na zadanej zmiennej reprezentującej ziarno zarządzane JSF, albo ogólnie wykonanie metody na zmiennej, która niekoniecznie jest ziarnem zarządzanym JSF. I tutaj nadchodzi moment wyjaśnienia mechanizmu rozwiązywania zmiennych w JSF - variable-resolver.

Każdy człon wyrażenia EL jest interpretowany przez dedykowany komponent rozwiązywania zmiennych, który wykonywany jest podczas przetwarzania zlecenia JSF. Istnieje kilka komponentów rozwiązujących zmienne:

  • komponenty rozwiązywania bezpośredniego
  • komponenty rozwiązywania referencji ziaren zarządzanych
  • komponenty rozwiązywania pakietów lokalizacyjnych (ang. resource bundle)
  • komponenty rozwiązywania zmiennych zadeklarowane w faces-config.xml w sekcji variable-resolver
  • komponenty związane z aplikacją JSF poprzez Application.addELResolver()

Naszym tematem przewodnim są komponenty rozwiązujące zadeklarowane w faces-config.xml w sekcji variable-resolver. Rozpocznijmy przykładem. Jaki będzie efekt wykonania następującej strony zakładając pusty plik faces-config.xml i wywołanie strony poprzez http://localhost:8080/resolver/faces/index.jsp?

 <%@page contentType="text/html" pageEncoding="UTF-8"%>
 
 <%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%>
 <%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%>
 
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
 
 <html>
     <head>
         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
         <title>Variable Resolver demo</title>
     </head>
     <body>
         <f:view>
             <h1><h:outputText value="#{jacek}" /></h1>
         </f:view>
     </body>
 </html>

Oczywiście wynikiem będzie pusta strona. A co będzie, kiedy zmodyfikujemy stronę do następującej?

 <%@page contentType="text/html" pageEncoding="UTF-8"%>
 
 <%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%>
 <%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%>
 
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
 
 <html>
     <head>
         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
         <title>Variable Resolver demo</title>
     </head>
     <body>
         <f:view>
             <h1><h:outputText value="#{empty jacek}" /></h1>
         </f:view>
     </body>
 </html>

Wynikiem będzie wyświetlenie napisu true. Zmienna jest pusta, gdyż żaden z komponentów rozwiązujących nie obsługuje zmiennej jacek. Utwórzmy własny komponent rozwiązujący, który będzie akceptował zmienne rozpoczynające się ciągiem znaków jacek_ i zwracał pewien tekst (jako wynik rozwiązania zmiennej).

Każdy komponent rozwiązywania zmiennych musi rozszerzać klasę javax.faces.el.VariableResolver i dostarczyć realizację metody Object resolveVariable(FacesContext context, String name) (zapomnijmy na moment o zastąpieniu metody i w ogóle całej klasy przez javax.el.ELResolver).

 package pl.jaceklaskowski.jsf.resolver;
 
 import java.util.logging.Logger;
 import javax.faces.context.FacesContext;
 import javax.faces.el.VariableResolver;
 
 public class JacekVariableResolver extends VariableResolver {
 
     private static final String VARIABLE_PREFIX = "jacek_";
 
     private Logger log = Logger.getLogger(getClass().getName().toString());
 
     private VariableResolver originalResolver;
 
     public JacekVariableResolver(VariableResolver variableresolver) {
         this.originalResolver = variableresolver;
     }
 
     public Object resolveVariable(FacesContext context, String name) {
         Object variable = null;
         if (name.startsWith(VARIABLE_PREFIX)) {
             log.fine("1. resolveVariable (przed): " + name);
             name = name.substring(VARIABLE_PREFIX.length());
             log.fine("2. resolveVariable (po):  " + name);
 
             // Zrób coś ciekawego z name
             variable = "JacekVariableResolver: " + name;
             
         } else {
             variable = originalResolver.resolveVariable(context, name);
         }
         return variable;
     }
 }

Podczas tworzenia komponentu rozwiązującego, konstruktor otrzymuje kolejny komponent rozwiązujący w łańcuchu wszystkich komponentów rozwiązujących, aby mógł go wywołać do pomocy przy rozwiązaniu zmiennej, której sam nie interpretuje i na bazie jego wyniku podjąć dalsze czynności. W naszym przykładzie nie jest on wykorzystywany.

Rejestracja komponentu rozwiązującego następuje poprzez plik faces-config.xml w sekcji variable-resolver.

 <?xml version='1.0' encoding='UTF-8'?>
 
 <faces-config version="1.2" 
               xmlns="http://java.sun.com/xml/ns/javaee" 
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
               xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd">
     <application>
         <variable-resolver>pl.jaceklaskowski.jsf.resolver.JacekVariableResolver</variable-resolver>
     </application>
 </faces-config>

Z tak skonfigurowaną aplikacją JSF, następująca strona index.jsp

 <%@page contentType="text/html" pageEncoding="UTF-8"%>
 
 <%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%>
 <%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%>
 
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
 
 <html>
     <head>
         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
         <title>Variable Resolver demo</title>
     </head>
     <body>
         <f:view>
             <h1><h:outputText value="#{jacek_cokolwiek}" /></h1>
         </f:view>
     </body>
 </html>

wyświetli napis: JacekVariableResolver: cokolwiek. Proste, nieprawdaż?

A jaki to ma związek z brakującą funkcjonalnością mechanizmu wstrzeliwania zależności w WAS 6.1 z rozszerzeniem EJB 3.0? Przyjrzyjmy się następującemy komponentowi rozwiązującemu.

package pl.jaceklaskowski.jsf.resolver;

import java.lang.reflect.Field;

import javax.ejb.EJB;
import javax.faces.context.FacesContext;
import javax.faces.el.VariableResolver;
import javax.naming.Context;
import javax.naming.InitialContext;

public class EJB3VariableResolver extends VariableResolver {

    private static final String EJB3_VARIABLE_PREFIX = "ejb3_";

    private VariableResolver originalResolver;

    public EJB3VariableResolver(VariableResolver variableresolver) {
        this.originalResolver = variableresolver;
    }

    public Object resolveVariable(FacesContext context, String name) {
        Object variable = null;
        if (name.startsWith(EJB3_VARIABLE_PREFIX)) {
            System.out.println("1. resolveVariable (before): " + name);
            name = name.substring(EJB3_VARIABLE_PREFIX.length());
            System.out.println("2. resolveVariable (after):  " + name);

            variable = originalResolver.resolveVariable(context, name);

            System.out.println("3. resolveVariable - after originalResolver: " + variable);

            System.out.println("4. Checking out whether @EJB field-level annotation is used");
            Field[] fields = variable.getClass().getDeclaredFields();
            try {
                Context ctx = new InitialContext();
                for (Field field : fields) {
                    System.out.println("5. ...field: " + field.getName());
                    EJB ejb = field.getAnnotation(EJB.class);
                    if (ejb != null) {
                        System.out.println("6. ...field is @EJB-annotated");
                        String jndiName = field.getType().getName();
                        System.out.println("7. ...jndi name is " + jndiName);
                        Object value = ctx.lookup("ejblocal:" + jndiName);
                        System.out.println("8. ......value returned " + value);
                        field.setAccessible(true);
                        field.set(variable, value);
                        System.out.println("9. ...field set");
                    }
                }
            } catch (Exception ignored) {
                ignored.printStackTrace();
            }
        } else {
            variable = originalResolver.resolveVariable(context, name);
        }
        return variable;
    }
}

Przeanalizujmy go. Komponent akceptuje zmienne poprzedzone przedrostkiem ejb3_. Jeśli na stronie JSF umieścimy wyrażenie EL postaci ejb3_<nazwa_ziarna_jsf>, to dla niego zostanie wywołany komponent rozwiązujący EJB3VariableResolver i w kroku 4., po otrzymaniu poprawnej referencji ziarna JSF o nazwie <nazwa_ziarna_jsf>, zostaną wstrzelone ziarna EJB do wszystkich pól udekorowanych adnotacją @EJB. W ten sposób zarządzane ziarna JSF są faktycznie zarządzane, mimo początkowego braku ich traktowania w sposób, jaki nakazuje specyfikacja Java EE 5.

Kompletny projekt do zaimportowania do NetBeans IDE 6.0 dostępny jest jako jsf-variableresolver.zip.

Osobiste