Ни слова о луке

сэр шаман рассказывает о чём может

JSP Custom tags

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

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
 * @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

<?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 необходим и содержит выражение, все остальные атрибуты необязательны.

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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
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).

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<%@ 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>
Наверх