[ Team LiB ] Previous Section Next Section

23.3 Integrating Custom Iteration Actions

JSTL offers two utilities for customized iterations: a support class that can be extended for application-specific iteration actions and interfaces that actions nested in the body of an iteration action can use to get information about the iteration status.

23.3.1 Implementing a Custom Iteration Action

The JSTL <c:forEach> action is so flexible that it probably covers most cases, but to help develop application-specific iteration actions when needed, JSTL provides a base class for this as well. It's named javax.servlet.jsp.jstl.core.LoopTagSupport:

public abstract class LoopTagSupport 
  extends javax.servlet.jsp.tagext.TagSupport
  implements javax.servlet.jsp.jstl.core.LoopTag,
    javax.servlet.jsp.tagext.IterationTag,
    javax.servlet.jsp.tagext.TryCatchFinally

The class has the following fields a subclass can access:

protected int begin
protected int end
protected int step
protected String itemId
protected String statusId
protected boolean beginSpecified
protected boolean endSpecified
protected boolean stepSpecified

These variables hold the value of the corresponding attributes. The variable names for the var and varStatus attributes (itemId and statusId) are, unfortunately, not in sync with the attribute names, due to an oversight when the attribute naming conventions where changed. Nobody's perfect. For the int variables, there are also boolean variables that tell whether the corresponding attributes were set.

Here are the main methods a subclass must implement:

protected abstract void prepare(  ) 
  throws javax.servlet.jsp.JspTagException
protected abstract Object next(  ) throws javax.servlet.jsp.JspTagException
protected abstract boolean hasNext(  ) 
  throws javax.servlet.jsp.JspTagException

The prepare( ) method prepares for the iteration, for instance by creating an Iterator for the collection to iterate over. The next( ) method returns the next item from the collection, and the hasNext( ) method tells whether there are more items.

The LoopTagSupport class provides implementations for the standard Tag and TryCatchFinally methods, plus setter methods for the var and varStatus attributes:

public void setVar(String varName)
public void setVarStatus(String statusName)
public void doStartTag(  ) throws JspException
public void doAfterBody(  ) throws JspException
public void doCatch(Throwable t) throws Throwable
public void doFinally(  )
public void release(  )

Setter methods for begin, end, and step must be implemented by the subclass. They are not included in the support class because some subclasses may not want to support these attributes.[2]

[2] Another reason is that before the EL got integrated in the JSP spec, how to deal with dynamic values was best left to each tag handler subclass. This class was introduced in JSTL 1.0, based on JSP 1.2, and the EL processing was then a part of the JSTL specification so it required special processing in the tag handler.

The doStartTag( ) method calls the prepare( ) method. It then calls the hasNext( ) and next( ) methods begin number of times to throw away the items up to the start index (if it's not 0). Next, it calls hasNext( ), and if that returns true, it calls next( ) to advance to the first item to process and saves a reference to this item.

If step is set to a value other than 1, it calls next( ) as many times as needed to advance to the next valid item. Finally it exposes the current item and the status object through the variables defined by var and varStatus, if any, and returns EVAL_BODY_INCLUDE.

The doAfterBody( ) method is similar to doStartTag( ). It calls hasNext( ) to see if there are more items, and if it returns true and the end index has not been reached, it calls next( ), exposes the returned item through var, calls next( ) again to advance according to step, and returns EVAL_BODY_AGAIN.

The doCatch( ) method simply rethrows the exception, and doFinally( ) removes the var and varStatus variables, since they are available only to nested actions.

The LoopTagSupport class also provides implementations for the methods defined by the LoopTag interface:

public Object getCurrent(  ) throws javax.servlet.jsp.JspTagException
public LoopTagStatus getLoopStatus(  )

This interface can be used by custom actions that depend on the loop status. The getCurrent( ) method returns the current item, and the getLoopStatus( ) method returns a LoopTagStatus instance. I show an example of a custom action that uses this information at the end of this section.

Finally, there are three utility methods for validating the values of the begin, end, and step attribute values:

protected void validateBegin(  )throws javax.servlet.jsp.JspTagException
protected void validateEnd(  )throws javax.servlet.jsp.JspTagException
protected void validateStep(  ) throws javax.servlet.jsp.JspTagException

A custom action should use these to make sure the basic requirements for these values are satisfied: begin and end must be greater than or equal to 0, and step must be greater than or equal to 1.

To see how you can use all of this in a custom iteration action, let's develop an action that helps generate HTML form elements for selecting predefined values, such as a selection list or a group of checkboxes or radio buttons. The custom action can be used as shown in Example 23-2.

Example 23-2. Using a custom iteration action
<form action="validate.jsp">
  <xmp:forEachOption options="${options}" 
    selections="${paramValues.choice}" var="current">
    <input type="checkbox" name="choice" 
      value="${current.value}" ${current.selected ? 'checked' : ''}>
      ${current.text}
    <br>
  </xmp:forEachOption>
  <input type="submit">
</form>

The <xmp:forEachOption> action takes a Map as the value of the options attribute. The Map contains keys representing the text and value for each option. The selections attribute takes an array of String objects, each representing the value for an option that should be marked as selected. The action uses this information to expose a bean with three properties to the actions in its body: text, value, and selected. The first two are the key and value of the current Map entry, while the third is a boolean with the value true if the value for the current entry is present in the selections list. As shown in Example 23-2, EL expressions use the bean properties to set the checkbox value and text and test if the checked attribute should be set.

Extending the LoopTagSupport class makes it easy to implement this action. The complete class is shown in Example 23-3.

Example 23-3. The ForEachOptionTag class
package com.ora.jsp.tags.xmp;
  
import java.util.*;
import java.lang.reflect.Array;
import javax.servlet.jsp.*;
import javax.servlet.jsp.jstl.core.*;
import org.apache.taglibs.standard.lang.support.*;
import com.ora.jsp.util.StringFormat;
  
public class ForEachOptionTag extends LoopTagSupport {
    private Map options;
    private String[] selections;
    private Iterator iterator;
  
    public void setOptions(Map options) {
        this.options = options;
    }
  
    public void setSelections(String[] selections) {
        this.selections = selections;
    }
  
    protected void prepare(  ) {
        if (options != null) {
            iterator = options.entrySet().iterator(  );
        }
    }
  
    protected boolean hasNext(  ) {
        if (iterator == null) {
            return false;
        }
        else {
            return iterator.hasNext(  );
        }
    }
  
    protected Object next(  ) {
        Map.Entry me = (Map.Entry) iterator.next(  );
        String text = (String) me.getKey(  );
        String value = (String) me.getValue(  );
        boolean selected = isSelected(value);
        return new OptionBean(text, value, selected);
    }
  
    private boolean isSelected(String value) {
        return StringFormat.isValidString(value, selections, false); 
    }
  
    public class OptionBean {
        private String text;
        private String value;
        private boolean selected;
  
        public OptionBean(String text, String value, boolean selected) {
            this.text = text;
            this.value = value;
            this.selected = selected;
        }
  
        public String getText(  ) {
            return text;
        }
  
        public String getValue(  ) {
            return value;
        }
  
        public boolean isSelected(  ) {
            return selected;
        }
    }
}

The only things you need to implement are setter methods for the two unique attributes and the three iteration methods: prepare( ), hasNext( ), and next( ). The LoopTagSupport class takes care of the rest.

The prepare( ) method saves a reference to the Iterator for the Map entries in an instance variable, unless the options attribute value is null. In this case, the action should simply do nothing, just as the <c:forEach> action.

The hasNext( ) method returns false if the options attribute is null and calls hasNext( ) on the Iterator created by prepare( ) if not.

The real magic happens in the next( ) method. This method can be called only if hasNext( ) returns true, so we don't have to worry about the Iterator being null. First, the next entry is retrieved from the Iterator, and the text and value values are extracted. Then the private isSelected( ) method sets the selected value, and a bean with the text, value, and selected flag is returned. The LoopTagSupport class exposes this bean as the current item through the variable specified by the var attribute.

The rest is just plain old Java code. The bean class is defined as an inner class, with a constructor for all property values and getter methods for each property. The isSelected( ) method uses a utility class that's bundled with the source code for this book to see if the specified value is included in the list of selected values.

23.3.2 Interacting with an Iteration Action

The JSTL specification also defines two interfaces for iteration actions. The javax.servlet.jsp.jstl.core.LoopTag interface must be implemented by iteration actions that want to cooperate with actions nested in their bodies:

public interface LoopTag extends javax.servlet.jsp.tagext.Tag

It defines two methods nested actions can call:

public java.lang.Object getCurrent(  )
public LoopTagStatus getLoopStatus(  )

The getCurrent( ) method returns the current object in the collection the action iterates over. The getLoopStatus( ) returns an object that implements the other JSTL iteration interface: javax.servlet.jsp.jstl.core.LoopTagStatus.

Before we look at the LoopTagStatus interface, let's see how a custom action can use the LoopTag interface. Example 23-4 shows how such a custom action can be used for the same purpose as the custom iteration action described earlier, namely to generate an HTML checkbox element with the checked attribute set depending on a dynamic list of selections.

Example 23-4. Using a custom action that gets the iteration status from its parent
<form action="foreachoption.jsp">
  <c:forEach items="${options}">
    <xmp:buildCheckbox name="choice" 
      selections="${paramValues.choice}" />
    <br>
  </c:forEach>
  <input type="submit">
</form>

Here the JSTL <c:forEach> action loops through a Map with option texts and values, the same way as in the previous example. The <xmp:buildCheckbox> action generates a checkbox element using the specified name as the name attribute value, the current Map entry value as the value attribute, and the current Map entry key as the text. To decide whether to set the checked attribute or not, it checks if the current Map entry value is included in the list specified by the selections attribute.

Example 23-5 shows the code for the custom action.

Example 23-5. The BuildCheckboxTag class
package com.ora.jsp.tags.xmp;
  
import java.io.*;
import java.util.*;
import java.lang.reflect.Array;
import javax.servlet.jsp.*;
import javax.servlet.jsp.tagext.*;
import javax.servlet.jsp.jstl.core.*;
import org.apache.taglibs.standard.lang.support.*;
import com.ora.jsp.util.StringFormat;
  
public class BuildCheckboxTag extends TagSupport {
    private String name;
    private String[] selections;
  
    public void setName(String name) {
        this.name = name;
    }
  
    public void setSelections(String[] selections) {
        this.selections = selections;
    }
  
    public int doEndTag(  ) throws JspException {
        LoopTag parent = 
            (LoopTag) findAncestorWithClass(this, LoopTag.class);
        if (parent == null) {
            throw new JspTagException("buildCheckbox: invalid parent");
        }
  
        Map.Entry current = (Map.Entry) parent.getCurrent(  );
        String text = (String) current.getKey(  );
        String value = (String) current.getValue(  );
        JspWriter out = pageContext.getOut(  );
        StringBuffer checkbox = 
            new StringBuffer("<input type=\"checkbox\"");
        checkbox.append(" name=\"").append(name).append("\"").
            append(" value=\"").append(value).append("\"");
        if (isSelected(value, selections)) {
            checkbox.append(" checked");
        }
        checkbox.append(">").append(text);
        try {
            out.write(checkbox.toString(  ));
        }
        catch (IOException e) {}
        return EVAL_PAGE;
    }
  
    private boolean isSelected(String value, String[] selections) {
        return StringFormat.isValidString(value, selections, false); 
    }
}

The doEndTag( ) method is where all the action takes place. The parent tag is located using the findAncestorWithClass( ) method from Chapter 22. Note how the LoopTag interface is specified as the type of parent to look for. With a reference to a LoopTag parent in hand, the current iteration object is retrieved simply by calling the getCurrent( ) method. The key and the value is extracted and used as the text and value for the generated <input> element, and the checked attribute is set if the current value matches one in the selections list.

This custom action doesn't need the detailed status information provided by the LoopTagStatus interface, but it's as easy to get as the current iteration object; just call the parent's getLoopStatus( ) method. The LoopTagStatus interface provide a wealth of information through the following methods:

public java.lang.Object getCurrent(  )
public int getIndex(  )
public int getCount(  )
public boolean isFirst(  )
public boolean isLast(  )
public Integer getBegin(  )
public Integer getEnd(  )
public Integer getStep(  )

The getIndex( ) method returns the actual 0-based index of the current element in the collection, while getCount( ) returns the 1-based number for the current iteration. For example, for the second pass through the body of a <c:forEach> action with begin set to 10, getIndex( ) returns 11, and getCount( ) returns 2. The isFirst( ) and isLast( ) methods returns true for the first and last iteration, respectively, taking the values of begin, end, and step into consideration. The other methods are self-explanatory.

You can use the methods in this interface for custom actions that should do something only at certain points in the iteration, for instance for every second pass or only for the first or last pass.

    [ Team LiB ] Previous Section Next Section