- Mastering JavaServer Faces 2.2
- Anghel Leonard
- 1684字
- 2021-12-08 12:41:27
Writing a custom EL resolver
EL flexibility can be tested by extending it with custom implicit variables, properties, and method calls. This is possible if we extend the VariableResolver
or PropertyResolver
class, or even better, the ELResolver
class that give us flexibility to reuse the same implementation for different tasks. The following are three simple steps to add custom implicit variables:
- Create your own class that extends the
ELResolver
class. - Implement the inherited abstract methods.
- Add the
ELResolver
class infaces-config.xml
.
Next, you will see how to add a custom implicit variable by extending EL based on these steps. In this example, you want to retrieve a collection that contains the ATP singles rankings using EL directly in your JSF page. The variable name used to access the collection will be atp
.
First, you need to create a class that extends the javax.el.ELResolver
class. This is very simple. The code for the ATPVarResolver
class is as follows:
public class ATPVarResolver extends ELResolver { private static final Logger logger = Logger.getLogger(ATPVarResolver.class.getName()); private static final String PLAYERS = "atp"; private final Class<?> CONTENT = List.class; ... }
Second, you need to implement six abstract methods:
getValue
: This method is defined in the following manner:public abstract Object getValue(ELContext context, Object base, Object property)
This is the most important method of an
ELResolver
class. In the implementation of thegetValue
method, you will return the ATP items if the property requested is namedatp
. Therefore, the implementation will be as follows:@Override public Object getValue(ELContext ctx, Object base, Object property) { logger.log(Level.INFO, "Get Value property : {0}", property); if ((base == null) && property.equals(PLAYERS)) { logger.log(Level.INFO, "Found request {0}", base); ctx.setPropertyResolved(true); List<String> values = ATPSinglesRankings.getSinglesRankings(); return values; } return null; }
getType
: This method is defined in the following manner:public abstract Class<?> getType(ELContext context, Object base,Object property)
This method identifies the most general acceptable type for our property. The scope of this method is to determine if a call of the
setValue
method is safe without causing aClassCastException
to be thrown. Since we return a collection, we can say that the general acceptable type isList
. The implementation of thegetType
method is as follows:@Override public Class<?> getType(ELContext ctx, Object base, Object property) { if (base != null) { return null; } if (property == null) { String message = MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "property"); throw new PropertyNotFoundException(message); } if ((base == null) && property.equals(PLAYERS)) { ctx.setPropertyResolved(true); return CONTENT; } return null; }
setValue
: This method is defined in the following manner:public abstract void setValue(ELContext context, Object base, Object property, Object value)
This method tries to set the value for a given property and base. For read-only variables, such as
atp
, you need to throw an exception of typePropertyNotWritableException
. The implementation of thesetValue
method is as follows:@Override public void setValue(ELContext ctx, Object base, Object property, Object value) { if (base != null) { return; } ctx.setPropertyResolved(false); if (property == null) { String message = MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "property"); throw new PropertyNotFoundException(message); } if (PLAYERS.equals(property)) { throw new PropertyNotWritableException((String) property); } }
isReadOnly
: This method is defined in the following manner:public abstract boolean isReadOnly(ELContext context, Object base, Object property)
This method returns
true
if the variable is read-only andfalse
otherwise. Since theatp
variable is read-only, the implementation is obvious. This method is directly related to thesetValue
method, meaning that it signals whether it is safe or not to call thesetValue
method without gettingPropertyNotWritableException
as a response. The implementation of theisReadOnly
method is as follows:@Override public boolean isReadOnly(ELContext ctx, Object base, Object property) { return true; }
getFeatureDescriptors
: This method is defined in the following manner:public abstract Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base
This method returns a set of information about the variables or properties that can be resolved (commonly it is used by a design time tool (for example, JDeveloper has such a tool) to allow code completion of expressions). In this case, you can return
null
. The implementation of thegetFeatureDescriptors
method is as follows:@Override public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext ctx, Object base) { return null; }
getCommonPropertyType
: This method is defined in the following manner:public abstract Class<?> getCommonPropertyType(ELContext context, Object base)
This method returns the most general type that this resolver accepts. The implementation of the
getCommonPropertyType
method is as follows:@Override public Class<?> getCommonPropertyType(ELContext ctx, Object base) { if (base != null) { return null; } return String.class; }
Note
How do you know if the ELResolver
class acts as a VariableResolver
class (these two classes are deprecated in JSF 2.2) or as a PropertyResolver
class? The answer lies in the first part of the expression (known as the base argument), which in our case is null
(the base is before the first dot or the square bracket, while property is after this dot or the square bracket). When the base is null
, the ELresolver
class acts as a VariableResolver
class; otherwise, it acts as a PropertyResolver
class.
The getSinglesRankings
method (that populates the collection) is called from the getValue
method, and is defined in the following ATPSinglesRankings
class:
public class ATPSinglesRankings { public static List<String> getSinglesRankings(){ List<String> atp_ranking= new ArrayList<>(); atp_ranking.add("1 Nadal, Rafael (ESP)"); ... return atp_ranking; } }
Third, you register the custom ELResolver
class in faces-config.xml
using the <el-resolver>
tag and specifying the fully qualified name of the corresponding class. In other words, you add the ELResolver
class in the chain of responsibility, which represents the pattern used by JSF to deal with ELResolvers
:
<application> <el-resolver>book.beans.ATPVarResolver</el-resolver> </application>
Note
Each time an expression needs to be resolved, JSF will call the default expression language resolver implementation. Each value expression is evaluated behind the scenes by the getValue
method. When the <el-resolver>
tag is present, the custom resolver is added in the chain of responsibility. The EL implementation manages a chain of resolver instances for different types of expression elements. For each part of an expression, EL will traverse the chain until it finds a resolver capable to resolve that part. The resolver capable of dealing with that part will pass true
to the setPropertyResolved
method; this method acts as a flag at the ELContext
level.
Furthermore, EL implementation checks, after each resolver call, the value of this flag via the getPropertyResolved
method. When the flag is true
, EL implementation will repeat the process for the next part of the expression.
Done! Next, you can simply output the collection items in a data table, as shown in the following code:
<h:dataTable id="atpTableId" value="#{atp}" var="t"> <h:column> #{t} </h:column> </h:dataTable>
Well, so far so good! Now, our custom EL resolver returns the plain list of ATP rankings. But, what can we do if we need the list items in the reverse order, or to have the items in uppercase, or to obtain a random list? The answer could consist in adapting the preceding EL resolver to this situation.
First, you need to modify the getValue
method. At this moment, it returns List
, but you need to obtain an instance of the ATPSinglesRankings
class. Therefore, modify it as shown in the following code:
public Object getValue(ELContext ctx, Object base, Object property) { if ((base == null) && property.equals(PLAYERS)) { ctx.setPropertyResolved(true); return new ATPSinglesRankings(); } return null; }
Moreover, you need to redefine the CONTENT
constant accordingly as shown in the following line of code:
private final Class<?> CONTENT = ATPSinglesRankings.class;
Next, the ATPSinglesRankings
class can contain a method for each case, as shown in the following code:
public class ATPSinglesRankings { public List<String> getSinglesRankings(){ List<String> atp_ranking= new ArrayList<>(); atp_ranking.add("1 Nadal, Rafael (ESP)"); ... return atp_ranking; } public List<String> getSinglesRankingsReversed(){ List<String> atp_ranking= new ArrayList<>(); atp_ranking.add("5 Del Potro, Juan Martin (ARG)"); atp_ranking.add("4 Murray, Andy (GBR)"); ... return atp_ranking; } public List<String> getSinglesRankingsUpperCase(){ List<String> atp_ranking= new ArrayList<>(); atp_ranking.add("5 Del Potro, Juan Martin (ARG)".toUpperCase()); atp_ranking.add("4 Murray, Andy (GBR)".toUpperCase()); ... return atp_ranking; } ... }
Since the EL resolver returns an instance of the ATPSinglesRankings
class in the getValue
method, you can easily call the getSinglesRankings
, getSinglesRankingsReversed
, and getSinglesRankingsUpperCase
methods directly from your EL expressions, as shown in the following code:
<b>Ordered:</b><br/> <h:dataTable id="atpTableId1" value="#{atp.singlesRankings}"var="t"> <h:column>#{t}</h:column> </h:dataTable> <br/><br/><b>Reversed:</b><br/> <h:dataTable id="atpTableId2" value="#{atp.singlesRankingsReversed}" var="t"> <h:column>#{t}</h:column> </h:dataTable> <br/><br/><b>UpperCase:</b><br/> <h:dataTable id="atpTableId3" value="#{atp.singlesRankingsUpperCase}" var="t"> <h:column>#{t}</h:column> </h:dataTable>
The complete applications to demonstrate custom ELResolvers
are available in the code bundle of this chapter and are named ch1_2
and ch1_3
.
In order to develop the last example of writing a custom resolver, let's imagine the following scenario: we want to access the ELContext
object as an implicit object, by writing #{elContext}
instead of #{facesContext.ELContext}
. For this, we can use the knowledge accumulated from the previous two examples to write the following custom resolver:
public class ELContextResolver extends ELResolver { private static final String EL_CONTEXT_NAME = "elContext"; @Override public Class<?> getCommonPropertyType(ELContext ctx,Object base){ if (base != null) { return null; } return String.class; } @Override public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext ctx, Object base) { if (base != null) { return null; } ArrayList<FeatureDescriptor> list = new ArrayList<>(1); list.add(Util.getFeatureDescriptor("elContext", "elContext","elContext", false, false, true, ELContext.class, Boolean.TRUE)); return list.iterator(); } @Override public Class<?> getType(ELContext ctx, Object base, Object property) { if (base != null) { return null; } if (property == null) { String message = MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "property"); throw new PropertyNotFoundException(message); } if ((base == null) && property.equals(EL_CONTEXT_NAME)) { ctx.setPropertyResolved(true); } return null; } @Override public Object getValue(ELContext ctx, Object base, Object property) { if ((base == null) && property.equals(EL_CONTEXT_NAME)) { ctx.setPropertyResolved(true); FacesContext facesContext = FacesContext.getCurrentInstance(); return facesContext.getELContext(); } return null; } @Override public boolean isReadOnly(ELContext ctx, Object base, Object property) { if (base != null) { return false; } if (property == null) { String message = MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "property"); throw new PropertyNotFoundException(message); } if (EL_CONTEXT_NAME.equals(property)) { ctx.setPropertyResolved(true); return true; } return false; } @Override public void setValue(ELContext ctx, Object base, Object property, Object value) { if (base != null) { return; } ctx.setPropertyResolved(false); if (property == null) { String message = MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "property"); throw new PropertyNotFoundException(message); } if (EL_CONTEXT_NAME.equals(property)) { throw new PropertyNotWritableException((String) property); } } }
The complete application is named, ch1_6
. The goal of these three examples was to get you familiar with the main steps of writing a custom resolver. In Chapter 3, JSF Scopes – Lifespan and Use in Managed Beans Communication, you will see how to write a custom resolver for a custom scope.