Custom Tag Libraries

The developer can supply a custom Tag Library that can supplement the "tag" functionality that JETT supports. The developer supplies a class that implements the TagLibrary interface, which defines one method:

public Map<String, Class<? extends Tag>> getTagMap();
            

This method returns a Map that maps the name of the tag (e.g. "if") to the tag class object that defines the tag's functionality (e.g. IfTag.class).

Creating a Custom Tag

The developer creates a custom tag by implementing the Tag interface, although it is easier to subclass the abstract class BaseTag, which implements Tag and provides a lot of boiler-plate Tag code. See "AggTag", "IfTag", "NullTag", "SpanTag", and "TotalTag" for examples about how to implement tags by subclassing "BaseTag".

To subclass "BaseTag", implement the following abstract methods:

  • public String getName() - Return the tag's name as a String.
  • protected List<String> getRequiredAttributes() - Return a list of required attribute names. If all required attributes are not found, then BaseTag throws a TagParseException.
  • protected List<String> getOptionalAttributes() - Return a list of optional attribute names. If an attribute in the tag is not listed in the required attributes or the optional attributes, then BaseTag throws a TagParseException.
  • public void validateAttributes() - Validate attributes and values. Throw a TagParseException if any attribute input is not valid.
  • public boolean process() - Implement the logic of the Tag in this method. All Tags have access to the TagContext object through the inherited "getContext()" method. The TagContext object allows access to the current Sheet object, the containing Block object, the Map of beans, the List of registered CellListeners, the List of registered SheetListeners, and a Map of Cells that have already been processed. Call BaseTag's "removeBlock()" method to delete the tag's Block (and shift Cells over). To transform the body Cells of the tag's Block, create a BlockTransformer, pass it a new TagContext, and call the "transform()" method. Return a boolean indicating whether the first Cell of the block was processed. (It may not be processed if the "process" method removes or clears the Block.)

Creating a Custom Looping Tag

For a repeating tag, the developer may want to subclass the abstract class BaseLoopTag, which subclasses BaseTag. This abstract class defines the "process" method, but in doing so, it uses the Template Method Pattern to provide additional abstract methods as "hooks" to assist the "process" method in processing the loop. See the code for "ForEachTag", "MultiForEachTag", and "ForTag" for examples about how to implement repeating tags by subclassing "BaseLoopTag". Here are the abstract methods to implement:

  • protected abstract List<String> getCollectionNames() - Return a possibly null List of Collection names on which the tag is processing. This is only used to determine if any Collections are marked as "fixed size collections", and that information is used to determine whether to shift other content out of the way, to make room for additional copied blocks.
  • protected abstract int getNumIterations() - Return the number of iterations to be run.
  • protected abstract Iterator<?> getLoopIterator() - Return an Iterator object that can iterate over a collection of items.
  • protected abstract void beforeBlockProcessed(TagContext context, Block currBlock, Object item, int index) - Before the current iteration's Block is processed, this method is called, to prepare the Block for processing.
  • protected abstract void afterBlockProcessed(TagContext context, Block currBlock, Object item, int index) - After the current iteration's Block is processed, this method is called, to clean up after Block processing.

Example

In JETT's JUnit tests, there is code for the custom "AddOneTag", a custom tag that subclasses BaseTag.

package net.sf.jett.test.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.RichTextString;
import org.apache.poi.ss.usermodel.Sheet;

import net.sf.jett.expression.Expression;
import net.sf.jett.exception.TagParseException;
import net.sf.jett.model.Block;
import net.sf.jett.tag.BaseTag;
import net.sf.jett.tag.TagContext;
import net.sf.jett.util.AttributeUtil;
import net.sf.jett.util.SheetUtil;

/**
 * An <code>AddOneTag</code> is a custom <code>Tag</code> that adds 1 to the
 * numeric "value" attribute.  The main purpose of this <code>Tag</code> is to
 * demonstrate custom tags and custom tag libraries.
 *
 * <br>Attributes:
 * <ul>
 * <li><em>Inherits all attributes from {@link BaseTag}.</em>
 * <li>value (required): <code>Number</code>
 * </ul>
 */
public class AddOneTag extends BaseTag
{
   /**
    * Attribute for specifying the value.
    */
   public static final String ATTR_VALUE = "value";

   private static final List<String> REQ_ATTRS =
      new ArrayList<String>(Arrays.asList(ATTR_VALUE));

   private double myValue;

   /**
    * Returns this <code>Tag's</code> name.
    * @return This <code>Tag's</code> name.
    */
   public String getName()
   {
      return "addOne";
   }

   /**
    * Returns a <code>List</code> of required attribute names.
    * @return A <code>List</code> of required attribute names.
    */
   protected List<String> getRequiredAttributes()
   {
      List<String> reqAttrs = super.getRequiredAttributes();
      reqAttrs.addAll(REQ_ATTRS);
      return reqAttrs;
   }

   /**
    * Returns a <code>List</code> of optional attribute names.
    * @return A <code>List</code> of optional attribute names.
    */
   protected List<String> getOptionalAttributes()
   {
      return super.getOptionalAttributes();
   }

   /**
    * Validates the attributes for this <code>Tag</code>.  Some optional
    * attributes are only valid for bodiless tags, and others are only valid
    * for tags without bodies.
    */
   public void validateAttributes()
   {
      super.validateAttributes();
      TagContext context = getContext();
      Map<String, Object> beans = context.getBeans();
      Map<String, RichTextString> attributes = getAttributes();

      if (!isBodiless())
         throw new TagParseException("AddOne tags must not have a body.  AddOne tag with body found" + getLocation());

      myValue = AttributeUtil.evaluateDouble(context, attributes.get(ATTR_VALUE), beans, ATTR_VALUE, 0);
   }

   /**
    * Replace the cell's content with the value plus one.
    * @return <code>true</code>, this cell's content was processed.
    */
   public boolean process()
   {
      TagContext context = getContext();
      Sheet sheet = context.getSheet();
      Block block = context.getBlock();

      // Replace the bodiless tag text with the proper result.
      Cell cell = sheet.getRow(block.getTopRowNum()).getCell(block.getLeftColNum());
      SheetUtil.setCellValue(cell, myValue + 1, getAttributes().get(ATTR_VALUE));

      return true;
   }
}
            

In JETT's JUnit tests, there is code for the "CustomTagLibrary", a custom tag library that implements TagLibrary.

package net.sf.jett.test.model;

import java.util.HashMap;
import java.util.Map;

import net.sf.jett.tag.Tag;
import net.sf.jett.tag.TagLibrary;

/**
 * The <code>CustomTagLibrary</code> is for testing the custom tag libraries
 * feature.
 */
public class CustomTagLibrary implements TagLibrary
{
   private static CustomTagLibrary theLibrary = new CustomTagLibrary();

   private Map<String, Class<? extends Tag>> myTagMap;

   /**
    * Singleton constructor.
    */
   private CustomTagLibrary()
   {
      myTagMap = new HashMap<String, Class<? extends Tag>>();
      myTagMap.put("addOne", AddOneTag.class);
   }

   /**
    * Returns the singleton instance of a <code>CustomTagLibrary</code>.
    * @return The <code>CustomTagLibrary</code>.
    */
   public static CustomTagLibrary getCustomTagLibrary()
   {
      return theLibrary;
   }

   /**
    * Returns the <code>Map</code> of tag names to tag <code>Class</code>
    * objects.
    * @return A <code>Map</code> of tag names to tag <code>Class</code>
    *    objects.
    */
   public Map<String, Class<? extends Tag>> getTagMap()
   {
      return myTagMap;
   }
}
            

Prior to transformation, call the proper ExcelTransformer method to register a custom Tag Library:

ExcelTransformer transformer = new ExcelTransformer();
CustomTagLibrary library = CustomTagLibrary.getCustomTagLibrary();
transformer.registerTagLibrary("custom", library);
            

Here is the example template. During transformation, the "num" bean contains 3.14.

Demo addOne tag, custom namespace: <custom:addOne value="5"/>
Pass variable: <custom:addOne value="${num}"/>

... gets transformed into...

Demo addOne tag, custom namespace: 6
Pass variable: 4.14