Mastering JavaServer Faces 2.2
上QQ阅读APP看书,第一时间看更新

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:

  1. Create your own class that extends the ELResolver class.
  2. Implement the inherited abstract methods.
  3. Add the ELResolver class in faces-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 the getValue method, you will return the ATP items if the property requested is named atp. 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 a ClassCastException to be thrown. Since we return a collection, we can say that the general acceptable type is List. The implementation of the getType 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 type PropertyNotWritableException. The implementation of the setValue 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 and false otherwise. Since the atp variable is read-only, the implementation is obvious. This method is directly related to the setValue method, meaning that it signals whether it is safe or not to call the setValue method without getting PropertyNotWritableException as a response. The implementation of the isReadOnly 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 the getFeatureDescriptors 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.