與父標籤溝通


如果要設計的自訂標籤是放置在某個標籤之中,而且必須與外層標籤作溝通,例如 JSTL 中的 <c:when><c:otherwise> 必須放在 <c:choose> 中,且 <c:when><c:otherwise> 必須得知先前的 <c:when> 是否已經測試通過並執行本體內容,如果是的話就不再執行測試。

了解生命週期與架構 中談過,當JSP中包括自訂標籤時,會建立自訂標籤處理器的實例,呼叫 setJspContext() 設定 PageContext 實例,再來若是巢狀標籤中的內層標籤,則還會呼叫標籤處理器的 setParent() 方法,並傳入外層標籤處理器的實例,這就是你與外層標籤接觸的機會。

接下來將以模彷 JSTL 的 <c:choose><c:when><c:otherwise> 標籤為例,製作自訂的 <f: choose><f:when><f:otherwise> 標籤,了解內層標籤如何與外層標籤溝通。首先來看看 <f:choose> 的標籤處理器如何撰寫:

package cc.openhome;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.SimpleTagSupport;

public class ChooseTag extends SimpleTagSupport {
    private boolean matched;

    @Override
    public void doTag() throws JspException {
        try {
            this.getJspBody().invoke(null);
        } catch (java.io.IOException ex) {
            throw new JspException("ChooseTag 執行錯誤", ex);
        }
    }

    public boolean isMatched() {
        return matched;
    }

    public void setMatched(boolean matched) {
        this.matched = matched;
    }
} 

ChooseTag 基本上沒什麼事,只是內含一個 boolean 型態的成員 matched,預設是 false。一旦內部的 <f:when> 有測試成功的情況,會 將 matched 設定為 trueChooseTagdoTag() 只需要作一件事,取得 JspFragment 並呼叫 invoke(null) 執行標籤本體內容。

再來看看 <f:when> 的標籤處理器實作:

package cc.openhome;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.tagext.JspTag;
import javax.servlet.jsp.tagext.SimpleTagSupport;

public class WhenTag extends SimpleTagSupport {
    private boolean test;

    @Override
    public void doTag() throws JspException {
        JspTag parent = null;
        if (!((parent = getParent()) instanceof ChooseTag)) {
            throw new JspTagException("必須置於choose標籤中");
        }

        if (((ChooseTag) parent).isMatched()) {
            return;
        }

        if (test) {
            ((ChooseTag) parent).setMatched(true);
            try {
                this.getJspBody().invoke(null);
            } catch (java.io.IOException ex) {
                throw new JspException("WhenTag 執行錯誤", ex);
            }
        }
    }

    public void setTest(boolean test) {
        this.test = test;
    }
}

<f:when> 可以設定 test 屬性來看看是否執行本體內容。在測試開始前,必須先嘗試取得 parent,如果無法取得(也就是為 null 的情況),表示不在任何標籤之中;或是 parent 不為 ChooseTag 型態時,表示不是置於 <f:choose> 之中,這是個錯誤的使用方式,所以必須丟出例外。

如果確實是置於 <f:choose> 標籤之中,接著嘗試取得 parentmatched 狀態,如果已經被設定為 true,表示先前有 <f:when> 已經通過測試並執行了其本體內容,那麼目前這個 <f:when> 就不用再作測試了。如果是置於 <f:choose> 之中,而且先前沒有 <f:when> 通過測試,接著就可以進行目前這個 <f:when> 的測試,如果測試成功,則設定 parentmatchedtrue,並執行標籤本體。

接著來看 <f:otherwise> 的標籤處理器如何撰寫:

package cc.openhome;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.tagext.JspTag;
import javax.servlet.jsp.tagext.SimpleTagSupport;

public class OtherwiseTag extends SimpleTagSupport {
    @Override
    public void doTag() throws JspException {
        JspTag parent = null;
        if (!((parent = getParent()) instanceof ChooseTag)) {
            throw new JspTagException("WHEN_OUTSIDE_CHOOSE");
        }

        if (((ChooseTag) parent).isMatched()) {
            return;
        }

        try {
            this.getJspBody().invoke(null);
        } catch (java.io.IOException ex) {
            throw new JspException("Error in OtherwiseTag tag", ex);
        }
    }
}

<f:otherwise> 標籤的處理基本上與 <c:when> 類似,必須確認是否置於 <f:choose> 標籤之 中;必須確認先前是否有 <c:when> 測試成功,如果先前沒有 <c:when> 測試成功的話,就直接執行標籤本體內容。

接著記得定義 TLD 檔,在當中加入自訂標籤定義:

f.tld

<?xml version="1.0" encoding="UTF-8"?>
<taglib version="2.`" xmlns="http://java.sun.com/xml/ns/j2ee"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee  
    web-jsptaglibrary_2_1.xsd">
    <tlib-version>1.0</tlib-version>
    <short-name>f</short-name>
    <uri>https://openhome.cc/jstl/fake</uri>
    // 略...
    <tag>
        <name>choose</name>
        <tag-class>cc.openhome.ChooseTag</tag-class>
        <body-content>scriptless</body-content>
    </tag>
    <tag>
        <name>when</name>
        <tag-class>cc.openhome.WhenTag</tag-class>
        <body-content>scriptless</body-content>
        <attribute>
            <name>test</name>
            <required>true</required>
            <rtexprvalue>true</rtexprvalue>
            <type>boolean</type>
        </attribute>
    </tag>
    <tag>
        <name>otherwise</name>
        <tag-class>cc.openhome.OtherwiseTag</tag-class>
        <body-content>scriptless</body-content>
    </tag>
</taglib> 

接下來使用自訂的 <f:choose><f:when><f:otherwise> 標籤改寫〈流程處理標籤〉中的例子:

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@taglib prefix="f" uri="https://openhome.cc/jstl/fake"%>
<jsp:useBean id="user" class="cc.openhome.User"  />
<jsp:setProperty name="user" property="*" />
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>登入頁面</title>
    </head>
    <body>
        <f:choose>
            <f:when test="${user.valid}">
                <h1>${user.name}登入成功</h1>
            </f:when>
            <f:otherwise>
                 <h1>登入失敗</h1>
            </f:otherwise>
        </f:choose>
    </body>
</html> 

你可以使用 getParent() 取得 parent 標籤,也就是目前標籤的上一層標籤。如果在一個數個巢狀的標籤中,想要直接取得某個指定類型的外層標籤,則可以透過 SimpleTagSupportfindAncestorWithClass() 靜態方法。例如:

SomeTag ancestor = (SomeTag) findAncestorWithClass(this, SomeTag.class);

findAncestorWithClass() 方法會在目前標籤的外層標籤中尋找,直到找到指定的類型之外層標籤物件後傳回。