 | レベル: 中級 René Pawlitzek (rpa@zurich.ibm.com), Research and Development Engineer, IBM
2007年 2月 07日 2007年 7月 03日 更新 Hamlet フレームワークは、Java™ サーブレットを拡張してコンテンツと表示を強制的に分離するために開発されました。この記事では、著者の René Pawlitzek が、このフレームワークをさらに進化させ、テンプレート・エンジンの洗練した使い方をすることで、動的コンテンツを提供するための、新しい方法を紹介します。
Hamlet は、Web ベースのアプリケーションを開発するための、使いやすく理解しやすいフレームワークを提供します。このフレームワークは、コンテンツと表示の完全な分離をサポートするだけではなく、強制的に両者の分離を行います。しかも単純でスマートに設計されているため、おなじみの、基礎となるサーブレットのインフラが隠されることはありません。
このフレームワークは、Hamlet と呼ばれるサーブレット拡張機能を提供します。Hamlet は、表示を含むテンプレート・ファイルを SAX (Simple API for XML) を使用して読み取ります。(これらのテンプレート・ファイルは、厳密な HTML や XHTML、あるいは XML など、SAX で構文解析可能なコンテンツを含んでいる必要があります。) テンプレート・ファイルが読み取られる間、Hamlet は特別なタグや ID でマーキングされたテンプレート内の場所に、いくつかのコールバック関数からなるセットを使って動的にコンテンツを追加します。
この記事で使用するプロジェクト名 Hamlet は、Web ベースのアプリケーションでのコンテンツと表示を分離するための、サーブレット・ベースのフレームワーク開発の内部プロジェクトを指します。この記事を先に読み進める前に、私が以前 IBM developerWorks に書いた記事、「Introducing Hamlets」と、できれば私が developerWorks に書いたチュートリアル「Programming Hamlets」も読むとよいでしょう。(どちらも「参考文献」にリンクがあります。)
この記事は、Hamlet を実装するためのさまざまな側面について説明します。基本的な Hamlet の実装 (バージョン 1.0) は、2 つのパブリック Java クラスのみを使った 500 行に満たない Java コードで作られており、この実装についてはチュートリアル「Programming Hamlets」に説明してあります。今回の記事に含まれるコード (バージョン 1.1) は、2 つの Java インターフェースと 4 つのパブリック Java クラス、そして合計 695 行の Java コードを含んでいます。この実装はバージョン 1.0 よりも進化しており、より適切に構成されています (図 1 の UML 図を見てください)。この記事では、この実装について詳細に説明し、さらに HamletHandler を使って Hamlet をプログラムする新しい方法について学びます。この記事の完全なサンプルコードを含んだアーカイブ wa-hamlets3-code.zip は、「ダウンロード」セクションから入手することができます。
図 1. Hamlets 1.1 の UML 図
TemplateEngine インターフェースと ContentHandler インターフェース、そして DefaultEngine クラス
Hamlet フレームワークのコアには、テンプレート・エンジンがあります。テンプレート・エンジンは TemplateEngine インターフェースを実装するクラスです (リスト 1)。
リスト 1. TemplateEngine インターフェースがテンプレート・エンジンの機能を定義する
public interface TemplateEngine {
public void perform (InputStream in, ContentHandler handler, PrintWriter out)
throws Exception;
} // TemplateEngine
|
テンプレート・エンジンは、テンプレートを使って構文解析するパブリック・メソッド perform() を提供します。テンプレートが構文解析される間、いくつかのコールバック関数が呼び出されて動的コンテンツを生成し、テンプレート・エンジンはこのコンテンツをテンプレートに入力します。この入力 (テンプレート) は InputStream によって与えられ、また出力 (データの入ったテンプレート) は PrintWriter によって収集されます。ハンドラー・オブジェクトはこれらのコールバック関数を提供し、また Hamlet の ContentHandler インターフェースを実装します (リスト 2)。
リスト 2. ContentHandler インターフェースがコールバック関数のシグニチャーを定義する
public interface ContentHandler {
public int getElementRepeatCount (String id, String name, Attributes atts)
throws Exception;
public String getElementReplacement (String id, String name, Attributes atts)
throws Exception;
public Attributes getElementAttributes (String id, String name, Attributes atts)
throws Exception;
public InputStream getElementIncludeSource (String id, String name, Attributes atts)
throws Exception;
} // ContentHandler
|
DefaultEngine クラスは TemplateEngine インターフェースの標準実装を提供し、また SAX を使ってテンプレートを構文解析します。このクラスの perform() メソッドをリスト 3 に示します。
リスト 3. DefaultEngine クラスが TemplateEngine インターフェースの標準実装を提供する
public class DefaultEngine extends DefaultHandler implements TemplateEngine {
...
public void perform (InputStream in, ContentHandler handler, PrintWriter out)
throws Exception {
XMLReader reader = null;
try {
this.out = out;
this.handler = handler;
reader = readerPool.getReader ();
InputSource inputSource = new InputSource (in);
reader.setErrorHandler (this);
reader.setContentHandler (this);
reader.parse (inputSource);
} finally {
if (reader != null)
readerPool.returnReader (reader);
} // try
} // perform
...
} // DefaultEngine
|
リスト 3 のコードは、SAX リーダーのプールから 1 つの SAX リーダーを取得し、入力パラメーター in を InputSource でラップし、SAX エラーとコンテンツ・ハンドラーを設定し、そして入力ソース (テンプレート) の構文解析を開始します。構文解析が終了すると、その SAX リーダーをリーダーのプールに返します。パフォーマンス上の理由から SAX リーダーはバッファリングされますが、これはリクエストごとに新しい SAX リーダーを作成すると時間がかかりすぎるためです。
テンプレートの構文解析中に、テンプレート・エンジンの SAX コンテンツ・ハンドラー・メソッド (startDocument() と endDocument() 、startElement() 、endElement() そして characters() ) が呼び出されます。これらはリスト 4 のように実装されます。
リスト 4. DefaultEngine クラスが SAX コンテンツ・ハンドラー・メソッドを実装する
public class DefaultEngine extends DefaultHandler implements TemplateEngine {
...
public void startDocument () throws SAXException {
root = new TreeNode ();
curNode = root;
} // startDocument
public void endDocument () throws SAXException {
curNode = null;
root = null;
} // endDocument
public void startElement (String namespaceURI, String localName, String qName,
Attributes atts) throws SAXException {
try {
// category.debug ("local name: " + localName + ", qname: " + qName);
if (REPEAT_TAG.equals (localName)) {
record = true;
StartElement elem = new StartElement (namespaceURI, localName, qName, atts);
curNode.add (elem);
TreeNode newNode = new TreeNode ();
curNode.insert (newNode, 0);
curNode = newNode;
} else if (REPLACE_TAG.equals (localName)) {
if (record) {
StartElement elem = new StartElement (namespaceURI, localName, qName, atts);
curNode.add (elem);
} else {
ignore = true;
String id = atts.getValue (ID_ATTRIBUTE);
if (id != null)
out.print (handler.getElementReplacement (id, localName, atts));
} // if
} else if (INCLUDE_TAG.equals (localName)) {
if (record) {
StartElement elem = new StartElement (namespaceURI, localName, qName, atts);
curNode.add (elem);
} else {
ignore = true;
String id = atts.getValue (ID_ATTRIBUTE);
if (id != null)
atts = handler.getElementAttributes (id, localName, atts);
InputStream in = handler.getElementIncludeSource (id, localName, atts);
if (in != null) {
String line;
BufferedReader r = new BufferedReader (new InputStreamReader (in));
while ((line = r.readLine ()) != null)
out.println (line);
r.close ();
} // if
} // if
} else {
if (record) {
StartElement elem = new StartElement (namespaceURI, localName, qName, atts);
curNode.add (elem);
} else {
String id = atts.getValue (ID_ATTRIBUTE);
if (id != null)
atts = handler.getElementAttributes (id, localName, atts);
out.print ("<" + localName);
for (int i = 0; i < atts.getLength (); i++) {
out.print (" ");
out.print (atts.getLocalName (i));
out.print ("=\"");
out.print (atts.getValue (i));
out.print ("\"");
} // for
out.print (">");
} // if
} // if
} catch (Exception e) {
category.debug (e.getMessage (), e);
throw new SAXException (e);
} // try
} // startElement
public void endElement (String namespaceURI, String localName, String qName)
throws SAXException {
try {
if (REPEAT_TAG.equals (localName)) {
curNode = (TreeNode) curNode.getParent ();
EndElement elem = new EndElement (namespaceURI, localName, qName);
curNode.add (elem);
if (curNode.equals (root)) {
record = false;
playback (root);
root = new TreeNode ();
curNode = root;
} // if
} else if (REPLACE_TAG.equals (localName) || INCLUDE_TAG.equals (localName)) {
if (record) {
EndElement elem = new EndElement (namespaceURI, localName, qName);
curNode.add (elem);
} else {
ignore = false;
} // if
} else {
if (record) {
EndElement elem = new EndElement (namespaceURI, localName, qName);
curNode.add (elem);
} else {
out.print ("</" + localName + ">");
} // if
} // if
} catch (Exception e) {
category.debug (e.getMessage (), e);
throw new SAXException (e);
} // try
} // endElement
public void characters (char[] ch, int start, int length) throws SAXException {
String str = new String (ch, start, length);
if (record) {
CharElement elem = new CharElement (str);
curNode.add (elem);
} else {
if (!ignore)
out.print (str);
} // if
} // characters
...
} // DefaultEngine
|
startDocument() メソッドは、テンプレートが実際に構文解析される前に呼び出されます。このメソッドは、記録用に使われる 2 つのインスタンス変数 (root と curNode) を初期化します。構文解析が終了すると、endDocument() メソッドが両方のインスタンス変数を null に設定します。
SAX パーサーは、開始タグと終了タグを認識するごとに startElement() メソッドと endElement() メソッドを呼び出します。SAX パーサーが次のいずれかのタグを見つけると、特別なアクションが行われます。
-
<REPEAT>
-
</REPEAT>
-
<REPLACE>
-
</REPLACE>
-
<INCLUDE>
-
</INCLUDE>
これらのタグはテンプレート・ファイルの中に埋め込まれ、構文解析の際に動的コンテンツを追加する場所をマーキングします。
SAX リーダーが <REPEAT> タグを見つけると、startElement() メソッドは record フラグを true に設定します (リスト 5 )。従ってテンプレート・ファイルの <REPEAT> タグの後のすべてのコンテンツは、ツリー構造と 4 つの内部クラス (TreeNode と StartElement 、EndElement 、CharElement ) を利用して記録されます。ツリー構造が必要な理由は、<REPEAT> タグはネストが可能なためです。
リスト 5. startElement() での <REPEAT> タグの処理によってコンテンツの記録が開始される
if (REPEAT_TAG.equals (localName)) {
record = true;
...
}
|
endElement() メソッドは、対応する </REPEAT> タグを見つけると record フラグを false に設定し、記録を停止します (リスト 6 )。
リスト 6. endElement() での </REPEAT> タグ処理によってコンテンツの記録が停止され、コンテンツの再生が呼び出される
if (REPEAT_TAG.equals (localName)) {
...
record = false;
playback (root);
...
}
|
endElement() メソッドは playback() 関数も呼び出します (リスト 7)。playback() 関数は ContentHandler の getElementRepeatCount() コールバックを呼び出し、繰り返し回数 N を取得します。(getElementRepeatCount() メソッドは再生中にも繰り返し呼び出され、0 を返すことで終了します。) 次に、記録されたコンテンツ (要素のツリー) が N 回再生されます。記録された要素を再生するには、startElement() メソッドあるいは endElement() メソッド、あるいは characters() メソッドを呼び出します。これらのメソッドは記録されたコンテンツを、記録されていないコンテンツとまったく同じように扱います。<REPEAT> タグはネストが可能なため、playback() は再帰的であることに注意してください。
リスト 7. 記録されたコンテンツを playback() 関数によって N 回繰り返す
private void playback (TreeNode node) throws Exception {
int childIndex = node.getChildCount () - 1;
for (int i = 0; i < node.getCount (); i++) {
Object obj = node.elementAt (i);
if (obj instanceof StartElement) {
StartElement elem = (StartElement) obj;
if (REPEAT_TAG.equals (elem.localName)) {
int count = 0;
String id = elem.atts.getValue (ID_ATTRIBUTE);
if (id != null)
count = handler.getElementRepeatCount (id, elem.localName, elem.atts);
for (int j = 0; j < count; j++) {
playback ((TreeNode) node.getChildAt (childIndex));
if (handler.getElementRepeatCount (id, elem.localName, elem.atts) == 0)
break;
} // if
childIndex--;
} else
startElement (elem.namespaceURI, elem.localName, elem.qName, elem.atts);
} else if (obj instanceof EndElement) {
EndElement elem = (EndElement) obj;
if (!REPEAT_TAG.equals (elem.localName))
endElement (elem.namespaceURI, elem.localName, elem.qName);
} else if (obj instanceof CharElement) {
CharElement elem = (CharElement) obj;
characters (elem.str.toCharArray (), 0, elem.str.length ());
} // if
} // for
} // playback
|
SAX リーダーが <REPLACE> タグを見つけ、記録がアクティブでなくなると、startElement() メソッドは、もし ID 属性が存在すれば ContentHandler の getElementReplacement() メソッドを呼び出します。getElementReplacement() コールバックは、print() メソッドによる出力に含まれる、動的に作成されたコンテンツを返します。ignore フラグは、テンプレート・ファイルのプレースホルダー値 (<REPLACE> タグと </REPLACE> タグの間のコンテンツ) が無視され、characters() メソッドによる出力に含まれないように、事前に true に設定されています。
リスト 8. startElement() での <REPLACE> タグの処理
if (REPLACE_TAG.equals (localName)) {
if (record) {
...
} else {
ignore = true;
String id = atts.getValue (ID_ATTRIBUTE);
if (id != null)
out.print (handler.getElementReplacement (id, localName, atts));
} // if
} ...
|
同様に、SAX リーダーが <INCLUDE> タグを見つけ、記録がオフされると、startElement() メソッドは ContentHandler の getElementIncludeSource() メソッドを呼び出し、その時に出力に含まれているコンテンツを取得します。この前に、もし <INCLUDE> タグが ID 属性を含んでいる場合は Hamlet から属性を変更できるように、getElementAttributes() コールバックが呼び出されます。さらに、テンプレート・ファイルのプレースホルダー値 (<INCLUDE> タグと </INCLUDE> タグの間のコンテンツ) を無視するために、ignore フラグが true に設定されます。
リスト 9. startElement() での <INCLUDE> タグの処理
if (INCLUDE_TAG.equals (localName)) {
if (record) {
...
} else {
ignore = true;
String id = atts.getValue (ID_ATTRIBUTE);
if (id != null)
atts = handler.getElementAttributes (id, localName, atts);
InputStream in = handler.getElementIncludeSource (id, localName, atts);
if (in != null) {
String line;
BufferedReader r = new BufferedReader (new InputStreamReader (in));
while ((line = r.readLine ()) != null)
out.println (line);
r.close ();
} // if
} // if
} ...
|
SAX リーダーが </REPLACE> タグあるいは </INCLUDE> タグを見つけると、ignore フラグは endElement() メソッドの中で false に設定されます (リスト 10 )。
リスト 10. endElement() での </REPLACE> タグと </INCLUDE> タグの処理
if (REPLACE_TAG.equals (localName) || INCLUDE_TAG.equals (localName)) {
if (record) {
...
} else {
ignore = false;
} // if
} ...
|
記録モードではない場合と、その他のすべてのタグの場合、そのタグが ID 属性を含んでいると startElement() メソッドは getElementAttributes() を呼び出します。getElementAttributes() コールバックは、タグの属性を追加したり削除したり、あるいは変更したりすることができます。次に、そのタグ (属性が変更されている可能性があります) が出力ストリームに書き込まれます。このプロセスをリスト 11 に示します。
リスト 11. startElement() の中で通常の開始タグを処理する
} else {
if (record) {
...
} else {
String id = atts.getValue (ID_ATTRIBUTE);
if (id != null)
atts = handler.getElementAttributes (id, localName, atts);
out.print ("<" + localName);
for (int i = 0; i < atts.getLength (); i++) {
out.print (" ");
out.print (atts.getLocalName (i));
out.print ("=\"");
out.print (atts.getValue (i));
out.print ("\"");
} // for
out.print (">");
} // if
} ...
|
これに対応する終了タグは、endElement() メソッドの中で print() コマンドを使って生成されます (リスト 12 )。
リスト 12. endElement() の中で通常の終了タグを処理する
} else {
if (record) {
...
} else {
out.print ("</" + localName + ">");
} // if
} ...
|
Hamlet クラス
抽象クラスである Hamlet (リスト 13 ) は HttpServlet 拡張機能であり、Hamlet の ContentHandler インターフェースを実装しています。
リスト 13. Hamlet クラスが ContentHandler インターフェースを実装する
public abstract class Hamlet extends HttpServlet implements ContentHandler {
...
private String includePath;
public int getElementRepeatCount (String id, String name, Attributes atts)
throws Exception {
return 0;
} // getElementRepeatCount
public String getElementReplacement (String id, String name, Attributes atts)
throws Exception {
return "";
} // getElementReplacement
public Attributes getElementAttributes (String id, String name, Attributes atts)
throws Exception {
return atts;
} // getElementAttributes
public InputStream getElementIncludeSource (String id, String name, Attributes atts)
throws Exception {
URL url = new URL (getIncludeURL (atts.getValue ("SRC")));
return url.openStream ();
} // getElementIncludeSource
public String getDocumentType () {
return "text/html";
} // getDocumentType
public String getIncludeURL (String fileName) {
return includePath + fileName;
} // getIncludeURL
public TemplateEngine getTemplateEngine () {
return new DefaultEngine ();
} // getTemplateEngine
private void serveDoc (PrintWriter out, String template, ContentHandler handler)
throws Exception {
TemplateEngine engine = getTemplateEngine ();
InputStream in = getServletContext().getResourceAsStream (template);
category.debug ("Parsing '" + template + "' ...");
long t1 = System.currentTimeMillis ();
engine.perform (in, handler, out);
long t2 = System.currentTimeMillis ();
category.debug ("Parsed '" + template + "' in " + (t2 - t1) + " ms.");
} // serveDoc
public void serveDoc (HttpServletRequest req, HttpServletResponse res,
String template, ContentHandler handler) throws Exception {
includePath = "http://localhost:" + req.getServerPort () + "/" +
req.getContextPath () + "/";
PrintWriter out = res.getWriter ();
res.setContentType (getDocumentType ());
serveDoc (out, template, handler);
} // serveDoc
public void serveDoc (HttpServletRequest req, HttpServletResponse res,
String template) throws Exception {
serveDoc (req, res, template, this);
} // serveDoc
} // Hamlet
|
この Hamlet クラスは 3 つの serveDoc() メソッドを実装しています。serveDoc(PrintWriter out, String template, ContentHandler handler) はプライベート・メソッドであり、3 つのメソッドの中で最も興味深いものです。このメソッドは getTemplateEngine() を呼び出して、デフォルトのテンプレート・エンジンを取得し、getResourceAsStream() を呼び出すことでテンプレートを取得し、そしてテンプレート・エンジンの perform() メソッドを呼び出します。他の 2 つの serveDoc() メソッドはパブリック・メソッドであり、直接的に、あるいは間接的に serveDoc(PrintWriter out, String template, ContentHandler handler) を呼び出します。
さらに、抽象クラス Hamlet は Hamlet の ContentHandler インターフェースを実装し、コールバック・メソッド (getElementReplacement() と getElementRepeatCount()、getElementAttributes() 、そして getElementIncludeSource() ) のデフォルト実装を提供します。Hamlet クラスからクラスを派生することで、これらのコールバック関数の具体的な実装を提供し、ある特定のテンプレートに追加することができます。
リスト 14 の Logout1 クラスは Hamlet の拡張です。このクラスは getElementReplacement() メソッドを上書きし、テンプレート LogoutTemplate.html にユーザー ID を入力します。
リスト 14. Logout1 クラスが LogoutTemplate.html に対して動的コンテンツを提供する
public class Logout1 extends Hamlet {
// log4j
private static Category category = Category.getInstance (Logout1.class.getName ());
private String userID = null;
public void init () throws ServletException {
// get properties
ContextProperties props = ContextProperties.getProperties (this);
// configure logging
Utilities.configLog (props);
category.debug ("init");
} // init
public synchronized void doGet (HttpServletRequest req, HttpServletResponse res)
throws ServletException {
try {
category.debug ("doGet");
HttpSession session = req.getSession (false);
if (session != null) {
WebApp webApp = (WebApp) session.getAttribute (WebApp.ID);
userID = webApp.getUserID ();
// invalidate session
session.invalidate ();
// serve document
serveDoc (req, res, "LogoutTemplate.html");
} else {
// redirect
res.sendRedirect (res.encodeURL (req.getContextPath () +
"/StaticPage.html?Page=LoginHere.html"));
} // if
} catch (Exception e) {
category.debug ("", e);
throw new ServletException (e);
} // try
} // doGet
public String getElementReplacement (String id, String name, Attributes atts)
throws Exception {
if (id.equals ("UserID")) {
return (userID == null) ? "" : userID;
} // if
return "?";
} // getElementReplacement
} // Logout1
|
getElementReplacement() コールバックは LogoutTemplate.html の構文解析中にテンプレート・エンジンによって呼び出されます。構文解析そのものは serveDoc(req, res, "LogoutTemplate.html") メソッドによって開始されます。Logout1 は Hamlet クラスから serveDoc() を継承します。getElementReplacement() がテンプレート・エンジンに提供するコンテンツ (ユーザー ID) は doGet() メソッド内で取得され、Hamlet の userID インスタンス変数の中に保存されます。
サーブレット・コンテナーはデフォルトでサーブレット/Hamlet のインスタンスを 1 つしか提供しないため、複数のリクエストに対して doGet() メソッドを実行する複数のスレッド間でインスタンス変数 userID が共有されます。従って、適切な実行を保証するためには doGet() は同期している必要があります。あるいは、Logout1 Hamlet が空の SingleThreadModel インターフェースを実装する方法もあります (リスト 15 )。この場合は、doGet() メソッドの中で 2 つのスレッドが並列に実行されないよう、サーブレット・コンテナーが保証します。SingleThreadModel インターフェースを実装するすべてのサーブレット/Hamlet はスレッド・セーフと見なすことができ、そのインスタンス変数 (この場合は userID ) に対するアクセスを同期する必要はありません。つまり、こうした場合には doGet() メソッドを同期する必要がないのです。
リスト 15. Logout1 クラスが SingleThreadModel インターフェースをスレッド・セーフに実装する
public class Logout1 extends Hamlet implements SingleThreadModel {
// log4j
private static Category category = Category.getInstance (Logout1.class.getName ());
private String userID = null;
public void init () throws ServletException {
// get properties
ContextProperties props = ContextProperties.getProperties (this);
// configure logging
Utilities.configLog (props);
category.debug ("init");
} // init
public void doGet (HttpServletRequest req, HttpServletResponse res)
throws ServletException {
try {
category.debug ("doGet");
HttpSession session = req.getSession (false);
if (session != null) {
WebApp webApp = (WebApp) session.getAttribute (WebApp.ID);
userID = webApp.getUserID ();
// invalidate session
session.invalidate ();
// serve document
serveDoc (req, res, "LogoutTemplate.html");
} else {
// redirect
res.sendRedirect (res.encodeURL (req.getContextPath () +
"/StaticPage.html?Page=LoginHere.html"));
} // if
} catch (Exception e) {
category.debug ("", e);
throw new ServletException (e);
} // try
} // doGet
public String getElementReplacement (String id, String name, Attributes atts)
throws Exception {
if (id.equals ("UserID")) {
return (userID == null) ? "" : userID;
} // if
return "?";
} // getElementReplacement
} // Logout1
|
doGet() の同期動作 (リスト 14 ) によって、複数のリクエストが並列に実行されないようになります。ほとんどの場合、テンプレートに入力するためのデータが素早く取得できれば、同期は問題になりません。もしデータの取得に時間がかかりすぎる場合には、doGet() メソッドの一部を同期させます (リスト 16 ) が、一般的にテンプレートの構文解析 (serveDoc() の呼び出し) は非常に高速です。
リスト 16. doGet() を細かい粒度で同期することで並列実行を実現する
public void doGet (HttpServletRequest req, HttpServletResponse res)
throws ServletException {
try {
category.debug ("doGet");
HttpSession session = req.getSession (false);
if (session != null) {
WebApp webApp = (WebApp) session.getAttribute (WebApp.ID);
// obtain user ID, takes some time
String aUserID = webApp.getUserID ();
// invalidate session
session.invalidate ();
// serve document
synchronized (this) {
userID = aUserID;
serveDoc (req, res, "LogoutTemplate.html");
} // synchronized
} else {
// redirect
res.sendRedirect (res.encodeURL (req.getContextPath () +
"/StaticPage.html?Page=LoginHere.html"));
} // if
} catch (Exception e) {
category.debug ("", e);
throw new ServletException (e);
} // try
} // doGet
|
HamletHandler を導入する
一時的な保存や doGet() メソッドの同期のため、あるいは SingleThreadModel インターフェースを実装するために、Hamlet インスタンス変数を使う方法は、単純な Web ベースのアプリケーションを作成する際に複雑さを減らすための簡単な方法です。Hamlets v1.1 には、Hamlet を作成するための、新しい別の方法が用意されています (リスト 17 )。
リスト 17. Logout クラスが HamletHandler 拡張機能を使って動的コンテンツを提供する
public class Logout extends Hamlet {
// log4j
private static Category category = Category.getInstance (Logout.class.getName ());
private class LogoutHandler extends HamletHandler {
private String userID = null;
public LogoutHandler (Hamlet hamlet, String userID) {
super (hamlet);
this.userID = userID;
} // LogoutHandler
public String getElementReplacement (String id, String name, Attributes atts)
throws Exception {
if (id.equals ("UserID")) {
return (userID == null) ? "" : userID;
} // if
return "?";
} // getElementReplacement
} // LogoutHandler
public void init () throws ServletException {
// get properties
ContextProperties props = ContextProperties.getProperties (this);
// configure logging
Utilities.configLog (props);
category.debug ("init");
} // init
public void doGet (HttpServletRequest req, HttpServletResponse res)
throws ServletException {
try {
category.debug ("doGet");
HttpSession session = req.getSession (false);
if (session != null) {
WebApp webApp = (WebApp) session.getAttribute (WebApp.ID);
String userID = webApp.getUserID ();
// invalidate session
session.invalidate ();
// serve document
HamletHandler handler = new LogoutHandler (this, userID);
serveDoc (req, res, "LogoutTemplate.html", handler);
} else {
// redirect
res.sendRedirect (res.encodeURL (req.getContextPath () +
"/StaticPage.html?Page=LoginHere.html"));
} // if
} catch (Exception e) {
category.debug ("", e);
throw new ServletException (e);
} // try
} // doGet
} // Logout
|
Logout クラスは、Logout1 クラスとまったく同じように Hamlet を拡張しますが、HamletHandler を拡張する LogoutHandler というプライベート・クラスも含みます。HamletHandler (リスト 18 ) は、Hamlet ContentHandler インターフェースを実装し、そして 4 つのコールバック関数すべてに対するデフォルト実装 (例えば Hamlet クラスなど) を提供します。
リスト 18. HamletHandler クラスが ContentHandler インターフェースの標準実装を提供する
public class HamletHandler implements ContentHandler {
private Hamlet hamlet;
public HamletHandler (Hamlet hamlet) {
this.hamlet = hamlet;
} // HamletHandler
public int getElementRepeatCount (String id, String name, Attributes atts)
throws Exception {
return 0;
} // getElementRepeatCount
public String getElementReplacement (String id, String name, Attributes atts)
throws Exception {
return "";
} // getElementReplacement
public Attributes getElementAttributes (String id, String name, Attributes atts)
throws Exception {
return atts;
} // getElementAttributes
public InputStream getElementIncludeSource (String id, String name, Attributes atts)
throws Exception {
URL url = new URL (hamlet.getIncludeURL (atts.getValue ("SRC")));
return url.openStream ();
} // getElementIncludeSource
} // HamletHandler
|
Logout クラスの doGet() メソッドは、(handler という名前の) LogoutHandler のインスタンスを作成し、それを serveDoc() メソッドに渡します。テンプレート・エンジンは、LogoutTemplate.html テンプレートに動的コンテンツを追加しなければならない時に LogoutHandler を呼び出します。LogoutHandler コードはテンプレート・エンジンにユーザー ID を返します。このユーザー ID は doGet() メソッドの中で取得されますが、Hamlet クラスのインスタンス変数の中には保存されず、コンストラクターによって LogoutHandler クラスの userID インスタンス変数の中に保存されます。リクエストごとに LogoutHandler が作成されるため、複数のリクエストが Hamlet クラスの 1 つのインスタンス変数を共有する必要はありません。プライベート・クラス LogoutHandler によって複雑さが増しますが、その一方で同期の問題を回避することができ、複数のリクエストを並列に実行できるようになります。
Hamlet を拡張する
Hamlet フレームワークは拡張性を考慮して設計されています。TemplateEngine インターフェースを実装することで、皆さんが独自のテンプレート・エンジンを作成することができます。例えば、名前空間や他の要件をサポートするエンジンを作成しなければならないことがあるかもしれません。リスト 19 は、そうした例を示しています。
リスト 19. XMLEngine クラスが別の TemplateEngine インターフェース実装を提供する
public class XMLEngine implements TemplateEngine {
...
public void perform (InputStream in, ContentHandler handler, PrintWriter out)
throws Exception {
...
} // perform
...
} // XMLEngine
|
またリスト 20 に示すように、カスタムのテンプレート・エンジンを使うように Hamlet に指示することもできます。
リスト 20. XMLHamlet がカスタムのテンプレート・エンジンを使う
public class XMLHamlet extends Hamlet {
...
private class XMLHamletHandler extends HamletHandler {
public XMLHamletHandler (Hamlet hamlet) {
super (hamlet);
} // XMLHamletHandler
} // XMLHamletHandler
public String getDocumentType () {
return "text/xml";
} // getDocumentType
public TemplateEngine getTemplateEngine () {
return new XMLTemplateEngine ();
} // getTemplateEngine
public void doGet (HttpServletRequest req, HttpServletResponse res)
throws ServletException {
try {
// serve document
HamletHandler handler = new XMLHamletHandler (this);
serveDoc (req, res, "XMLTemplate.html", handler);
} catch (Exception e) {
throw new ServletException (e);
} // try
} // doGet
} // XMLHamlet
|
まとめ
以前に「Programming Hamlets」チュートリアルで示した実装 (2 つのパブリック Java クラスのみを使った 500 行に満たないコードで作成された、バージョン 1.0) と比較して、新しい実装 (2 つの Java インターフェースと 4 つのパブリック Java クラス、そして 695 行の Java コードを含む、バージョン 1.1) はさらに進化しており、テンプレート・エンジンを Hamlet クラスから分離しています。コールバック関数は、今度は ContentHandler インターフェースによって定義されます。さらに新しい実装では、テンプレート・エンジンに動的コンテンツを提供するための別の方法が用意されています。また、リクエストごとに HamletHandler のインスタンスが別であるため、doGet() メソッドを同期する必要がなくなりました。
謝辞
Hamlet の高度な実装に助言や貢献をしてくださった Yann Duponchel 氏に感謝いたします。
ダウンロード | 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|
| Complete code sample for this article | wa-hamlets3-code.zip | 6KB | HTTP |
|---|
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | 
|  | Rene Pawlitzek は、リヒテンシュタインの市民であり、Swiss Federal Institute of Technology (ETH Zurich) のコンピューター・サイエンスの工学学位を取得しています。スイスにある IBM Zurich Research Laboratory の Advanced Operating Environment グループ (旧 GSAL (Global Security Analysis Lab)) で、セキュリティー情報管理ソリューションにフォーカスした研究・開発のエンジニアとして働いています。IBM に入社する前は、カリフォルニアの Hewlett-Packard、WindRiver Systems、および Borland International で働いていました。
|
記事の評価
|  |