移植到Velocity
Pages: 1, 2

複製JSP

觀察一下基本的JSP,為了方便我們做移植`,有一些功能還是是需要的。

在下面的例子,我們設定了content type,產生一個JavaBean (一個Date物件),包含了一些JSP "片段(fragment)"檔,使用JSTL taglib中的fmt以呈現ResourceBundle中的文字。最後一行呼叫getTime 此Date的方法(也可以很簡單地透過JSTLc:out來取得同樣的值)

<%@ page contentType="text/html; charset=UTF-8" %>
<fmt:setBundle basename="com.lateralnz.messages" var="sysm" />
<jsp:useBean id="test" scope="session" class="java.util.Date" />
<html xmlns="http://www.w3.org/1999/xhtml">

<%@ include file="htmlhead.jsf" %>

<body>
<%@ include file="header.jsf" %>

<h1><fmt:message key="app.welcome_title" bundle="${sysm}" /></h1>

<p><fmt:message key="app.welcome_text" bundle="${sysm}" /></p>

<p><%= test.getTime() %></p>

</div>
</body>
</html>

Velocity提供了一些同樣功能的directive,但並非全部。 在標準的Velocity安裝中,DateResourceBundle 會被放在Servlet Context之中,所以可以在樣板中直接呼叫。第二步,滿足需求(先暫時不管之前我所講的MVC的目標)。我寫了屬於自己的#usebean 的directive(參照 UseBeanDirective.java 的程式碼)。它傳進name、使用的class name、還有它的scope。UseBeanDirective 則會產生JavaBean且把它放在context之中。如果scope是"application",它會被放在static HashMap 供之後使用;如果scope是"session",則它會被放在使用者的session之中。

VMServlet 中,同樣需要一個method可以在template中如JSP中同樣的方法去設定content type。我目前的選擇是快取content type,以requesrtURI當作Key,並使用Velocity樣板的getData 方法來找此頁中($contentType)這個變數:

private final String determineContentType(
    Node node) {
  for (int i = 0; 
       i < node.jjtGetNumChildren(); 
       i++) {
    StringBuffer sb = new StringBuffer();
    Node n = node.jjtGetChild(i);
    
    if (n instanceof org.apache.velocity.
          runtime.parser.node.ASTSetDirective) {
      Token t = 
        findTokenWithPrefix(n.getFirstToken(), 
                            "$");
      if (t == null || 
         !t.toString().equalsIgnoreCase(
             "$contentType")) {
        continue;
      }
      else {
        t = findTokenWithPrefix(t, "\"");
        String ct = StringUtils.remove(
                      t.toString(), "\"");
        return ct;
      }
    }
  }

  return "text/html";
}

private final Token findTokenWithPrefix(
    Token t, 
    String prefix) {
    
  if (t == null) {
    return null;
  }
  else if (t.toString().startsWith(prefix)) {
    return t;
  }
  else {
    return findTokenWithPrefix(t.next, prefix);
  }
}

雖然很明顯地,這是竄改原始碼,但是也達到了目的。因此,我加了以下的程式碼到processRequest:

String contentType;
if (contentTypes.containsKey(requestURI)) {
  contentType = 
      (String)contentTypes.get(requestURI);
}
else {
  contentType = 
      determineContentType((Node)tmp.getData());
  contentTypes.put(requestURI, contentType);
}

最終的Velocity樣板如下面所示:

#set ( $contentType = "text/html" )
<html xmlns="http://www.w3.org/1999/xhtml">

#usebean ( test "java.util.Date" page )
#usebean ( sysm 
  "com.lateralnz.MyMessagesBundle" application )

#parse ( "htmlhead.vm" )

<body>
#include ( "header.vm" )

<h1>
$sysm.getString('app.welcome_title')
</h1>

<p>
$sysm.getString('app.welcome_text')
</p>

<p>$test.getTime()</p>

</body>
</html>

如您所見,跟原本的JSP頁面沒有太大的改變。而程式碼中ResourceBundle 是透過一個JavaBean的方式讀進來,但在真正的系統下,我把它快取在VMServlet中,並且自動地當作隱含物件讀進來中。

還有一小塊我們還沒有提到。如果使用JSTL可能還會用到一些Jakarta taglibs。如果是這個情況的話,則還需要一些程式碼。幸好,透過產生新的directive去延伸Velocity是很容易的事情 (事實上我發現比寫taglib還簡單,除了API相對少以外)。到目前為止,我只需要產生一些額外的directive:一個是產生格式化的日期,一個是格式化的數字,還有一些簡單 的directive去終止繪製一個樣板(以貼在Velocity mailing list中的一個小技巧為基礎):

public class ExitDirective extends Directive {
  private static final String EXIT = "exit";
  private static final 
    StopRenderingException STOP_RENDER_EXCEPTION = 
      new StopRenderingException();
  
  public boolean render(
      InternalContextAdapter context, 
      Writer writer, 
      Node node) 
        throws IOException, 
           ResourceNotFoundException, 
           ParseErrorException, 
           MethodInvocationException {
    throw STOP_RENDER_EXCEPTION;
  }
  
  public int getType() {
    return DirectiveConstants.LINE;
  }
  
  public String getName() {
    return EXIT;
  }
}

VMServletprocessRequest 方法中,會捕捉到StopRenderingException ,並且有效地終止樣板繪製。在我們的系統中,我們用這個特別的directive在WAP header中去檔掉沒有登入的使用者:

#if ( !$session.getAttribute(USER_ID) )
<card id="message" title="Invalid Session">
<p>Sorry, but your session has 
timed out.</p>
</card>
</wml>
#exit ( )
#end

總結

Velocity提供Web開發者一個強大的樣板機制—然而,也可能對想要從JSP和taglibs移植的人剛開始就帶來不算小的障礙。使用簡單的servlet,還有採用directive去延伸Velocity,大家可以效法這 些方來完成大部分JSP的功能。也因此讓移植的工作可以以小規模、低風險的方式一步一步地邁進。

相關鏈結

最後一點:如果您是*nix的開發者,絕對不要忘記sed 這個好用的工具,它可以幫您做掉許多移植JSP時所需面對的苦工。我並不是sed的專家,但也只花了我一點時間就馬上轉換了90%的程式碼。

Jason R. Briggs is a technical architect and developer for a wireless technology company