JSP Custom tags

Расскажу вам для затравки, например, о кастом-тэгах для JSP (а по принципу - и для каких-нибудь там Java Server Faces). Информации об этом действительно не так уж мало. Но тем не менее хотелось предложить для начала что-нибудь простенькое дабы развернуть тему.

В качестве примера я решил взять свой немного хитрый, но зато относительно широко показывающий возможности тего-фабрицирования, тег.

В качестве задачи нам требуется встроить в JSP возможность изменения стиля текста, обрамленного тегом, в зависимости от величины переданного значения. Яркий пример - отображение в таблице задач с небольшими приоритетами - курсивом, а задач с высокими приоритетами - жирным шрифтом, стиль задач с нормальным приоритетом при этом не меняется. Кроме того, требовалась возможность передать тегу запись CSS-стиля, поэтому спецификация тега получилась даже больше его реализации :).

Что ж, посмотрим какие свойства должен иметь тег. У него, разумеется, должно быть тело, содержащее текст, подлежащий изменению стиля. У него должен быть обязательный атрибут со значением текущего уровня задачи и несколько необязательных (забитых дефолтовыми значениями) атрибутов - численное значение среднего уровня приоритета, стиль (по умолчанию, допустим, жирный) для значения с высоким приоритетом и стиль для значения с низким приоритетом.

Спецификация получилась примерно такой:


/**
 * @author uwilfred
 *
 * Adds priorityFontTag, specified in .tld as
 *      <prefix:priorityFont
 *      value = Integer       // priority value; if tag body not
                specified, also specifies the value
 *      [ level = Integer ]       // priority level to change the
                style on
 *      [ lowChange = String ]    // style description to apply
                if priority less than level (format listed below)
 *      [ highChange = String ]   // style description to apply
                if priority greater than level (format listed below)
 *      ( /> |
 *          body                  // value
 *      </prefix:priorityFont> )  // may be empty tag,
                    so the value it taken from priority value parameter
 *
 *  default for highChange is “bold”
 *  default for lowChange is “italic”
 *  default for level is 3
 *
 *  Formats:
 *      bold                                  -> <strong>body</strong>
 *      italic                                -> <em>body</em>
 *      underline                             -> <u>body</u>
 *      strike                                -> <strike>body</strike>
 *      .<css-class>            .foo          -> <span class=”foo”>body</span>
 *      {<css-style>; ...}  	{font-weight: bold;} -> <span style="font-weight: bold;">body</span>
 *      /<html-tag-name>    /foo              -> <foo>body</foo>
 */

То есть за счёт всяких хитрых символов и алиасов я включил поддержку практически любых желаний пользователя :).

В код класса тега я также включу XDoclet-теги по которым при необходимости можно будет сгенерировать запись в .tld-шке.

По спецификации, опишем тег в .tld-файле - библиотеке тегов:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE taglib PUBLIC
    "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN"
    "http://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_1.dtd">
<taglib>
   <tlibversion>1.0</tlibversion>
   <jspversion>1.1</jspversion>
   <shortname>uwilfred</shortname>

   <tag>

      <name>priorityFont</name>
      <tagclass>org.individpro.uwilfred.tag.PriorityFontTag</tagclass>
      <bodycontent>JSP</bodycontent>

      <attribute>
         <name>value</name>
         <required>true</required>
         <rtexprvalue>true</rtexprvalue>
      </attribute>

      <attribute>
         <name>highChange</name>
         <required>false</required>
      </attribute>

      <attribute>
         <name>level</name>
         <required>false</required>
      </attribute>

      <attribute>
         <name>lowChange</name>
         <required>false</required>
      </attribute>

   </tag>

</taglib>

Значит тело нашего тега - это нечто, вычисляемое засчет JSP-кода (обычный текст на выходе дает текст), атрибут value необходим и содержит выражение, все остальные атрибуты необязательны.

Ну и сразу чтобы не тянуть - описываем класс тега:


package org.individpro.uwilfred.tag;

import java.io.IOException;
import javax.servlet.jsp.JspException;

import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.BodyContent;
import javax.servlet.jsp.tagext.BodyTagSupport;
import javax.servlet.jsp.tagext.Tag;

/**
 * @note adds specified-level style change support
 * @author uwilfred
 *
 * @jsp.tag
 *   name="priorityFont"
 *   body-content="JSP"
 */

public class PriorityFontTag extends BodyTagSupport implements Tag {

   private static final long serialVersionUID = -4941606719316390930L;

   private Integer value = -1;
   private Integer level = 3;
   private String lowChange = "bold";
   private String highChange = "italic";
   private String valueHtmlPrefix = "";
   private String valueHtmlPostfix = "";
   private String bodyTextContent = "";

   // TODO: private final Map that will store the styles replacements,
   //       like "bold" -> "strong", "italic" -> "em" & s.o.

   public void release() {
      value = -1;
      level = 3;
      lowChange = "bold";
      highChange = "italic";
      valueHtmlPrefix = "";
      valueHtmlPostfix = "";
      bodyTextContent = "";
   }

   /**
    * any variable to take the priority from, also
    * recognized as value if no body specified, default -1
    */

   public Integer getValue() {
      return value;
   }

   /**
    * @jsp.attribute
    *   required="true"
    *   rtexprvalue="true"
    */

   public void setValue(Integer value) {
      this.value = value;
   }

   /**
    * style description to apply to the content with value higher
    *		 than the level.
    *		 supports: bold, italic, underline or any
                                        body-having html tag or css-style
    *		     format: bold | italic | underline | strike
    *		         .<CSS-class>
    *		         {<CSS-Descriptors-Line>}
    *		         :<HTML-Tag-Name>
    *		 default: italic
    */

   public String getHighChange() {
      return highChange;
   }

   /**
    * @jsp.attribute
    *   required="false"
    */

   public void setHighChange(String highChange) {
      this.highChange = highChange;
   }

   /**
    * level point to change the style, default is 3
    */

   public Integer getLevel() {
      return level;
   }

   /**
    * @jsp.attribute
    *   required="false"
    */

   public void setLevel(Integer level) {
      this.level = level;
   }

   /**
    * style description to apply to the content with value lower
    *		 than the level.
    *		 supports: bold, italic, underline or any
                                    body-having html tag or css-style
    *		     format: bold | italic | underline | strike
    *		         .<CSS-class>
    *		         {<CSS-Descriptors-Line>}
    *		         :<HTML-Tag-Name>
    *		 default: italic
    */

   public String getLowChange() {
      return lowChange;
   }

   /**
    * @jsp.attribute
    *   required="false"
    */

   public void setLowChange(String lowChange) {
      this.lowChange = lowChange;
   }

   public int doStartTag() throws JspException {
      return EVAL_BODY_BUFFERED;
   }

   public int doAfterBody() throws JspException {
      try {
         BodyContent bodyContent = getBodyContent();
         if (bodyContent == null) {
            bodyTextContent = value.toString();
         } else {
            bodyTextContent = bodyContent.getString();
            if (bodyTextContent == null) {
               bodyTextContent = value.toString();
            }
         }
      } catch (NumberFormatException nfe) {
         nfe.printStackTrace();
         throw new JspException("jbpm:priorityFont
                        tag body couldn't be parsed", nfe);
      }
      return SKIP_BODY;
   }

   public int doEndTag() throws JspException {
      if (value == Integer.valueOf(-1)) {
         throw new JspException("jbpm:priorityFont tag requires
                  the body xor the value parameter to
                  be specified (also, negative values are unsupported)");
      }
      try {
         JspWriter jspOut = pageContext.getOut();
         String modificator =
                  (value < level) ? lowChange :
                           ((value > level) ? highChange : "");
         if (modificator.equalsIgnoreCase("bold")) {
            valueHtmlPrefix = "<strong>";
            valueHtmlPostfix = "</strong>";
         } else if (modificator.equalsIgnoreCase("italic")) {
            valueHtmlPrefix = "<em>";
            valueHtmlPostfix = "</em>";
         } else if (modificator.equalsIgnoreCase("underline")) {
            valueHtmlPrefix = "<u>";
            valueHtmlPostfix = "</u>";
         } else if (modificator.equalsIgnoreCase("strike")) {
            valueHtmlPrefix = "<strike>";
            valueHtmlPostfix = "</strike>";
         } else if ((modificator.length() > 1) &&
                                       (modificator.charAt(0) == '.')) {
            // CSS existing style specify
            valueHtmlPrefix = "<span class=\"" + modificator.substring(1) + "\">";
            valueHtmlPostfix = "</span>";
         } else if ((modificator.length() > 1) &&
                                       (modificator.charAt(0) == '/')) {
            // HTML tag redefine
            valueHtmlPrefix = "<" + modificator.substring(1) + ">";
            valueHtmlPostfix = "</" + modificator.substring(1) + ">";
         } else if ((modificator.length() > 2) &&
                (modificator.charAt(0) == '{') &&
                (modificator.charAt(modificator.length() - 1) == '}')) {
            // CSS style line
            valueHtmlPrefix = "<span style=\"" +
                   modificator.substring(1, modificator.length() - 1)
                   + "\">";
            valueHtmlPostfix = "</span>";
         } else if (modificator.length() > 0) {
             throw new JspException ("jbpm:priorityFont tag parameters
                    values couldn't be parsed");
         }
         jspOut.print(valueHtmlPrefix + bodyTextContent +
               valueHtmlPostfix);
      } catch (NumberFormatException nfe) {
         nfe.printStackTrace();
         throw new JspException("jbpm:priorityFont tag parameters
                        couldn't be parsed", nfe);
      } catch (IOException ioe) {
         ioe.printStackTrace();
         throw new JspException("jbpm:priorityFont tag parameters
                        couldn't be parsed", ioe);
      }
      release();
      return EVAL_PAGE;
   }

}

Наследуя тег от класса BodyTagSupport мы подразумевали, что у тега будет тело. Очистка значений происходит в методе release(), значения атрибутов устанавливаются в скомпилированной JSP - HTTP-сервлете - засчет аксессоров. Методы doStartTag(), doAfterBody() и doEndTag() перегружены от родителя (и имплементят соответствующие методы интерфейса Tag) и вызываются в указанном порядке - после обработки открывающего тега, после обработки тела и после обработки закрывающего тега соответственно.

doStartTag() возвращает константу EVAL_BODY_BUFFERED (вычислить тело в буфере), которая указывает, что тело тега надо вычислить. doAfterBody() забивает body значением из value если тело тега отсутствует - и возвращает SKIP_BODY (пропустить тело) дабы тело тега не попало в выходной HTML-код без соответствующей обработки. doEndTag() делает самое главное - на основе значения и установок атрибутов генерирует выходной HTML-код, делает release() (иначе значения атрибутов сохранятся для последующих тегов) и возвращает EVAL_PAGE (вычислить страницу) чтобы компилятор JSP (jasper) по цепочке пошел дальше по тексту страницы.

Если бы наш тег не должен был быть иметь тела (empty в .tld) - то мы бы наследовали его от javax.servlet.tagext.TagSupport (который по аналогичным причинам и является родителем BodyTagSupport) и по сути не должны были бы имплементить ничего кроме методов release() и doEntTag() (который возвращал бы EVAL_PAGE).

Ну и посмотрим на использование тега:


<%@ taglib uri="/WEB-INF/uwilfred.tld" prefix="uwilfred" %>...
<html><%
...
%>
<head>
   <style>

   .priorityHigh {
      font-color: #f00;
      font-weight: bold;
      border: 1px solid #333;
   }

   .priorityLow {
      font-color: #00f;
      font-style: italic;
      border: 1px dotted #999;
   }

   </style>

   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
   <title>Test priorityFont Tag</title>
</head>

<body class="layout">

   <c:forEach items="${model.tasks}" var="task" varStatus="status">
      <uwilfred:priorityFont value="${task.priority}">
            <a href="taskinfo.htm?id=<c:out value="${task.id}"/>">
                  <c:out value="${task.name}"/>
            </a>
      </uwilfred:priorityFont>
   </c:forEach>

   <%-- is identical to: --%>

   <c:forEach items="${model.tasks}" var="task" varStatus="status">
      <uwilfred:priorityFont value="${task.priority}" level="3"
              highChange="bold" lowChange="italic">
         <a href="taskinfo.htm?id=<c:out value="${task.id}"/>">
            <c:out value="${task.name}"/>
         </a>
      </uwilfred:priorityFont>
   </c:forEach>

   <%-- different variants: --%>

   <uwilfred:priorityFont value="${someValue}"
            highChange="{font-color: #f00; font-weight: bold;}"
            lowChange="{font-color: #00f; font-style: italic;}">
                  BlahByCSSInline
   </uwilfred:priorityFont>

   <uwilfred:priorityFont value="${someValue}"
                                    highChange=".priorityHigh"
                                    lowChange=".priorityLow">
         BlahByCSSExistentClass
   </uwilfred:priorityFont>

   <uwilfred:priorityFont value="${someValue}"
               highChange=":strong"
               lowChange=":em">
         BlahByTagRedefine
   </uwilfred:priorityFont>

</body>

</html>
#java #jsp