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.
