5章 応用編

この章ではJavaMailの応用として、ユーザのアクションに応じてメッセージを生成して自動送信(応答)を行ういくつかの例をご紹介します。また、POP3を用いた単純なWebMailシステムについてもサンプルをご紹介します。

TextFormatterによるメッセージ整形

ここでは、送信するメッセージを生成する方法について記述します。これにはさまざまな方法があります。
送信するメッセージが常に固定である場合は、メッセージデータをリソースファイルから単に読み込んで使用すればよいですが、毎回メッセージの内容が変動する場合はどのように生成するのがスマートでしょう?
簡単に思いつくのは、以下のような方法です。

これはjava.text.MessageFormatクラスを用いれば比較的簡単に実現できます。この例を以下に記します。

// MessageFormatクラスを用いたテキストの整形サンプル
import java.text.MessageFormat;
import java.util.Calendar;
import java.util.Date;

class MessageFormatTest {
    private static String source =
            "{0}さん、こんにちは。\n" +
            "今日は{1, date, full}です。\n" +
            "{2, date, full}にセミナーを開催します。" +
            "よろしければご来場ください。\n" +
            "  :";

    public static void main(String[] args) {
        Calendar c = Calendar.getInstance();
        c.set(Calendar.YEAR, 2002);
        c.set(Calendar.MONTH, Calendar.JANUARY);
        c.set(Calendar.DATE, 1);

        Object[] formatArgs = new Object[] {
            "きのした",
            new Date(),
            c.getTime(),
        };

        String string = MessageFormat.format(source, formatArgs);
        System.out.println(string);
    }
}

実際にはテンプレート(*)部分は外部のリソースファイルから読み込むようにし、置き換え部分となるパラメタの生成方法も生成するものに応じて変わるでしょう。
このようにすることで、受信者の名前を本文に埋めこんだり、日程/金額などを本文の特定位置に埋めこむことは容易に実現できます。
ただ、MessageFormatはJDK1.3の時点でも一度に10箇所しか置換対象にできない(10種類ではなく10箇所!!)ことと、置換キーワードを0〜9の数字で指定し、置換文字列も配列で与えるため可読性が悪いという問題があるため筆者は使用する気になりませんでした。単純なプログラムであれば手軽に利用できる手法です。また、プレインテキストのメイルであれば、ほとんどの場合はMessageFormatの機能で十分でしょう。

*:「テンプレート」とはここでは「出力結果の元となる雛形」といった意味に捉えてください。

さて、筆者としては、やはりテンプレートの置き換え部分はキーワードで指定したいですし、もちろんキーワード個数の制限も付けたくありませんでした。また、表のような繰り返しが発生する部分も、レイアウトに関する部分をテンプレート上に切り出すことでプログラムの変更なしに列の配置などのレイアウトに関係する部分の変更を可能にしたいという願望がありました。
そこでTextFormatterなるものを自作することにしました。
実は上に書いた願望は、Servletから出力するHTMLの生成方法を考えたときに感じたことでした。そして、設計に入る前に、Servletに限らず表現とロジックを分離して表現だけを簡単に修正できる仕組みは必要だと感じていましたので、HTMLの生成ではなくテキストの整形を行うライブラリとして作成したのです。従って、このライブラリは「表現とロジックを分離」し、その境界線(*)にも自由度を持たせるというテーマで、割とうまくいったと思っています。

*:「表現」に関する部分をどこまで分離するべきはそのプログラムの質によって異なり、テンプレート部としてくくり出す範囲が大きくなればなるほど、その自由度の高さ故に、テンプレート部の可読性/メンテナンス性が落ちてしまいます。JSP等はテンプレート部分にロジック(処理)を埋めこめてしまうので、そのような状況になりがちです。つまり、テンプレートの部分であまりになんでもできるようにしてしまうと逆効果になりかねないということです。

TextFormatterの仕様はhttp://www.sk-jp.com/java/library/textformatter/を参照して下さい。
ここでは、このようなライブラリを用いてメッセージのボディを生成する利点をもう少し具体的に書いてみようと思います。
同様のことを実現するライブラリは他にもあるでしょうし、読者のみなさんが独自の発想で実装されることもあると思いますので、その手段を考えるときの足しになれば幸いです(*)。

*:本当はこのライブラリを本の中で作成していきたかったのですが、やりたいことをみんな実現しようと思うとここでは説明しきれなくなってしまいますので、このような形になってしまいました。

まず、いきなりですが以下のような本文を持つメッセージを自動生成することを考えて見ます(この手の文書は苦手ですので内容の稚拙さはご容赦下さい^^)。

木下 信 様                                ****** 注文者名
                                     ****** ↓固定部分
この度は、XXXXXにご注文いただき誠にありがとうございます。
お客様がブラウザ上でご注文された商品について確認のメイルを
差し上げております。

注:もし、身に覚えのない場合はこのまま何もしなければ商品の発送はなされません。
   ただ、他人が「なりすまし」て注文操作が行われてしまった可能性がある場合は、
   以下のページで「パスワード変更」を行ってください。

http://www.example.com/account

お客様は現在、以下の商品について、ご購入申し込みをされています。

ご請求先

木下 信<shin@sk-jp.com>                                        ****** 注文者アカウント情報
〒XXX-XXXX
yyyyy県yyyy市yyyy町nn-nn

-----------------------------------------------         ****** 注文内容
書名  :JavaMail完全解説(仮称)                   ****** 注文1
出版  :(株)秀和システム
著者  :木下 信
購入部数:1部
単価  :3000円
小計  :3000円
-----------------------------------------------
書名  :Java(TM)Servlet 最新サーバ・プログラミング         ****** 注文2
出版  :(株)秀和システム
著者  :原田 洋子
購入部数:2部
単価  :4200円
小計  :8400円
====================================================
送料&手数料:    :      800円                  ****** 計
販売価格合計(税別) :    12200円
消費税        :      610円
            -----------
注文合計       :    12810円
                                     ****** ↓固定部分

上記内容に間違いが無ければ、以下のURLにアクセスし、
「購入します」ボタンを押下して下さい。
この操作を行わない限り、商品の配送はされません。

http://www.example.com/buy


XXXXXオンラインショップをご利用いただき、ありがとうございました。

----------------------------------------------------      ****** 広告
4月1日より新サービス「ふ〜」開始
http://www.example.com/foo-/
----------------------------------------------------

--                                   ****** シグネチャ(署名)
XXXXXオンラインサービス
http://www.example.com/
mailto:question@example.com

まず、送付する人ごとに変更する場所をマーク付けしていきます。上記の中で注文者に関する情報と注文内容、および広告は変動する部分で、その他は通常は変更しない固定の部分になりますね。
この、変動する部分をキーワードに書き換えてみました。ここではTextFormatterライブラリを使うので@##keyword@という形式にしています。

@##name@ 様

この度は、XXXXXにご注文いただき誠にありがとうございます。
お客様がブラウザ上でご注文された商品について確認のメイルを
差し上げております。

注:もし、身に覚えのない場合はこのまま何もしなければ商品の発送はなされません。
   ただ、他人が「なりすまし」て注文操作が行われてしまった可能性がある場合は、
   以下のページで「パスワード変更」を行ってください。

http://www.example.com/account

お客様は現在、以下の商品について、ご購入申し込みをされています。

ご請求先

@##name@<@##mailAddress@>
〒@##zipCode@
@##prefecture@@##address1@
@##address2@

-----------------------------------------------
@##orders@
====================================================
送料&手数料:    :@##fee@円
販売価格合計(税別) :@##total1@円
消費税        :@##tax@円
            -----------
注文合計       :@##total2@円


上記内容に間違いが無ければ、以下のURLにアクセスし、
「購入します」ボタンを押下して下さい。
この操作を行わない限り、商品の配送はされません。

http://www.example.com/buy


XXXXXオンラインショップをご利用いただき、ありがとうございました。

----------------------------------------------------
@##advertisements@
----------------------------------------------------

--
XXXXXオンラインサービス
http://www.example.com/
mailto:question@example.com

このような形式のテンプレートに対し、各キーワードとそれに置き換えられる文字列のMapを生成してTextFormatterに処理させる事で、最初の例のような文章ができ上がります。

Map args = new HashMap();
args.put("name", "木下 信");
  :   // 何らかの方法でargsの内容を埋める。
  :
TextFormatter formatter = new TextFormatter("/resources/e-mail.template"); // テンプレートファイル
String messageBody = formatter.format(args);

MessageFormatと比べるとテンプレート文書がいくぶん見やすくなっていると思います(*)し、置き換え文字列群をMapで与える事になるということはPropertiesファイルも簡単に利用できるということです(java.util.PropertiesクラスはMapの実装クラスです)。が、この程度であればテンプレートの機能的にはMessageFormatと大差ありません。番号(配列の添え字)で指定されていた置き換え記号がキーワードに変わっただけです。もう少しテンプレートに自由度を持たせるように注文と広告の部分を変更してみました。

*:キーワードを使えるのはともかく、@##keyword@という記号はあまり気に入ってはいませんが・・。これは他の記号にもできたのですが、"<>"等の他のparserに解釈されそうな記号は避けたかったのです。筆者は着色機能を持つエディッタで、@#([^@]|\n|\r)+@という正規表現に着色するように設定することで見やすくしています。

  :
  :
@#while orders@
-----------------------------------------------
書名  :@##title@
出版  :@##publisher@
著者  :@##author@
購入部数:@##count@部
単価  :@##unitPrice@円
小計  :@##price@@#endwhile@
====================================================
  :
  :
@#exist advertisements@
----------------------------------------------------
@#foreach advertisements@
@##advertisements@
----------------------------------------------------
@#endforeach@@#endexist@
  :

TextFormatterでは、テンプレート上に繰り返しや条件判断の構文を記述できるようにしています。@#while keyword@〜@#endwhile@で囲まれた範囲は、Mapのキー"keyword"の値となるMapの配列(やList)の個数分繰り返し処理されます。ちなみに@#foreach keyword@〜@endforeach@も繰り返し構文の一つで、@#exist keyword@〜@endexist@は、Mapの"keyword"に対応する値がnullでない場合のみ囲まれた範囲を出力するという意味です。
これによって、繰り返される部分をどのようにレイアウトするかについてもテンプレート側で変更ができるようになるのです。例えば各注文の表現方法を表形式にするのもテンプレート上で配置を変更するだけで済みます。

さきにも述べましたが、筆者はこのTextFormatterを元々ServletからのHTML出力に利用しようと思って作成しました。そういうこともあって、HTMLを生成する際により有用に働くようになっており(ソースコードを生成するのにも便利と思っていますが)、プレインテキストで、しかもそれほど複雑になりにくい電子メイルの本文の生成にはややオーバースペックとも思えるのですが、皆さんがWeb関連の業務などで電子メイルを送信しなければならない場合の参考になれば幸いです。

WebアプリケーションへのJavaMailの適用(実践)

さて、ここまででメッセージの生成方法/送信方法については筆者の知る限りのことはほぼ説明しました。
では、最後に簡単なWebベースのアプリケーションの例を挙げて、どのようにJavaMailを利用するかを見ていくことにしましょう。

本書で対象とするのはどちらかというと、JavaMailそのものがターゲットとしているクライアントサイドアプリケーション(主にメイラ)作成者ではなく、実務では最も多く利用されるであろうサーバサイドアプリケーション作成者です。
この分野ではJavaMailが提供するクライアントサイド向けの便利な機能はあまり使うことがありませんが、JavaMailそのものが持つ低レベル制御やMIME処理はそれだけでもこのライブラリを利用する動機として十分なほど便利です。

ユーザへのメイル自動送信(ショッピングカート)

まずはユーザのアクションなどに応じてメイルを自動送信するアプリケーションの例として、簡易ショッピングカートを作ってみました。
内容はちょうど前に送信メイルの例を記した架空オンライン本屋さんということにします。ここではJavaMailを利用する部分が要点であり、Servletそのものの設計詳細については説明を簡略化しようと思うのですが、結局メイル送信部は一行で済んでしまうため、大半はServletの説明になってしまいます。説明の前半部分のServlet群は、送信メッセージの元となるデータを得るために外すことができませんでした。

今回のショッピングカートの機能は以下のようなものとします。

機能と呼ぶほどでもないほど単純なものです。配送先の入力時に決済方法やパスワードなども入力して、さらにそれらの情報はサーバ上に保存する事で次回から入力不要にしたり、Cookieを用いてログインをスキップしたりするものですが、今回のサンプルではすべて省略します(*)。
そうするとだいたい以下のような画面遷移になりますね。

*:そのようにするためには認証する仕組みとサーバ側リソースが必要になりますので。今回は自動応答メイルのサンプルですから^^。

ログイン画面は単なるHTMLとします。以下にログイン画面とそのイメージを示します。
Cookieを用いて以前ログインしていたことを検出してログイン画面をスキップできるようにする場合は、ログイン画面を生成するためのServletを用意しますが、ここでは常にログインが必要ということにします。

[shopping.html]
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<HTML>
<HEAD>
<META http-equiv="Content-Style-Type" content="text/css">
<TITLE>Online shopping</TITLE>
</HEAD>
<BODY>
<DIV style="text-align:center">
<H1>Shopping site login</H1>
<HR>
<FORM method="post" action="/servlets/com.sk_jp.servlet.shop.Login">
<TABLE>
  <TBODY>
    <TR>
      <TD>メイルアドレスを入力してください。</TD>
      <TD><INPUT size="20" type="text" name="mailAddress"></TD>
    </TR>
  </TBODY>
</TABLE>
<P><INPUT type="submit" value="  入場  "></P>
</FORM>
</DIV>
<HR>
</BODY>
</HTML>

さて、次にServletを作成します。今のログイン画面を除いてそれぞれの画面につき一つのServletを作ることになります。
先のTextFormatterの項で説明した通り、Webアプリケーション等で特にプログラマとデザイナが分担するような場合は、デザインとロジックをどのように分けるかがその後のメンテナンス性に大きな影響を与えます。

Servlet毎に初期パラメタの指定をするのが煩雑であるため、初期値情報を獲得する部分を各Servletの共通のスーパークラス上に記述し、さらに、リソース(Class#getResourceAsStream())からパラメタを記述したPropertiesを読み込むようにすることで、初期パラメタを使用しないようにしました。このようにすると、CLASSPATHから参照できる位置にPropertiesファイルを配置しておくだけで、全てのServletがパラメタを参照できるようになります。

// [ShoppingCartServlet.java]
package com.sk_jp.servlet.shop;

import java.io.IOException;
import java.util.Properties;

import com.sk_jp.servlet.BaseServlet;

public abstract class ShoppingCartServlet extends BaseServlet {
    protected Properties properties;
    protected String inputEncoding;
    protected String outputEncoding;

    // init(ServletConfig)メソッドから呼び出されます。
    protected final void init() throws IOException {
        properties = loadProperties(
                "/com/sk_jp/resources/shop/shopping.properties");
        inputEncoding = properties.getProperty(
                "inputEncoding", "ISO-8859-1");
        outputEncoding = properties.getProperty(
                "outputEncoding", "ISO-8859-1");
        initSub();
    }
    protected void initSub() throws IOException {
    }

    //////////////////////////////////////////////////////////////////////////
    // 以下はこのクラスのスーパークラスであるBaseServletに定義されている
    // メソッドを掲載用に記述しています。
    // BaseServletは全てのServletから利用できる基本機能をまとめた
    // クラスです。
    // BaseServlet.javaの内容詳細や他のライブラリの内容は
    // http://www.sk-jp.com/java/library/utility/
    // を参照して下さい

/*
    // init()メソッドを再定義して、Template Method化しています。
    // こうしておけば、super.init(config);を呼び出さなければ
    // ならないという約束は不要になります。
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        try {
            init();
        } catch (IOException e) {
            throw new ServletException(e.toString());
        }
    }

    // CLASSPATHからPropertiesファイルを読み込みます
    protected Properties loadProperties(String resourceName)
                throws IOException {
        Properties properties = new Properties();
        java.io.InputStream in  = getClass().getResourceAsStream(resourceName);
        if (in == null) {
            throw new IOException("File not found : " + resourceName);
        }
        properties.load(in);
        return properties;
    }

    // 出力用のPrintWriterを取得します。
    public static PrintWriter getPrintWriter(HttpServletResponse response)
                throws IOException {
        response.setHeader("Pragma", "no-cache");
        return new PrintWriter(
                // Unicodeの一部のコードの多重定義と
                // エンコーディングによるマッピングの差異を吸収するWriter
                new CorrectOutputStreamWriter(
                    response.getOutputStream(),
                    response.getCharacterEncoding()), true);
    }

    // TextFormatterを使って出力を行います。
    protected void print(HttpServletResponse response,
                         Formatter formatter,
                         Map substitutes,
                         String outputEncoding)
                throws IOException {
        response.setContentType("text/html; charset=" + outputEncoding);
        PrintWriter out = getPrintWriter(response);

        formatter.format(substitutes, out);
        out.flush();
        out.close();
    }

    // 定型のエラーメッセージ表示です。
    // ここもTextFormatterを用いて生成するようにすれば、
    // エラー時の画面デザインをリソースから与えられるようになります。
    // またエラー画面程度であればJSP(Java Server Pages)を用いるのが楽です。
    protected void error(HttpServletResponse response,
                         String title,
                         String message)
                throws IOException {
        response.setContentType("text/html; charset=ISO-2022-JP");
        PrintWriter out = getPrintWriter(response);
        out.println("<HTML>");
        out.println("<HEAD>");
        out.print("<TITLE>");
        out.print(title);
        out.print("</TITLE></HEAD>");
        out.println("<BODY><H1>");
        out.println(title);
        out.println("</h1><BR/><BR/><DIV style=\"text-align:center;\">");
        out.println(message);
        out.println("<BR/><BR/><FORM>");
        out.println("<input type=\"button\" value=\"  戻る  \"");
        out.println(" onClick=\"history.back()\"/>");
        out.println("</FORM>");
        out.println("</DIV>");
        out.println("</BODY>");
        out.println("</HTML>");
        out.close();
    }

    // Sessionが継続できない場合のエラー表示です。
    // 本当のシステムではこのような粗末なエラー表示にせず、
    // 何らかの代替手段を使用するでしょう。
    protected void errorSessionInvalid(HttpServletResponse response)
                throws IOException {
        error(response,
              "セッションを継続できません",
              "長時間リクエストがなかった/またはCookieが機能しないために" +
              "セッションが途切れました。<BR />" +
              "このコンテンツは「セッション毎のCookie機能」" +
              "を有効にしていなければ正常に動作しません。<BR /><BR />" +
              "恐れ入りますが設定をご確認の上、" +
              "もう一度最初から操作してくださいますよう" +
              "よろしくお願いいたします。");
    }
*/
}

init()メソッドまわりを定義して、propertiesという変数に設定ファイルを読み込み、inputEncoding/outputEncodingという変数にそのServletが用いる入力文字エンコーディングと出力文字エンコーディングを格納するまでを行います。サブクラスはinitSub()を定義して個別の初期化処理を行えるようにしています。また、サブクラスから利用する簡易エラーページ生成処理なども含まれるものとします。

では、このShoppingCartServletを元に、ログイン/アカウント情報入力画面から呼び出されるServletを作成します。
まずはLogin時の処理です。

// [Login.java]
package com.sk_jp.servlet.shop;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class Login extends ShoppingCartServlet {
    public void doPost(HttpServletRequest request,
                       HttpServletResponse response)
                throws ServletException, IOException {

        final String address = request.getParameter("mailAddress");
        if (address == null || address.length() == 0) {
            error(response, "メイルアドレスが入力されていません",
                  "前の画面に戻って再度入力してください。");
            return;
        }

        final HttpSession session = request.getSession(true);
        session.putValue("mailAddress", address);

        // (1)
        // メイルアドレスに対応するアカウント情報がサーバ上に存在すれば
        // アカウント情報入力画面をスキップするような作りにするとよいでしょう
        // Cookieを使えば、メイルアドレスの入力すら省略することができます。
        response.sendRedirect("com.sk_jp.servlet.shop.Account");
    }
}

これだけです。HttpSessionオブジェクトにフォームから送られたmailAddressを格納してアカウント情報入力ServletにリダイレクトするだけのServletとなっています。
将来的にアカウント情報をサーバ上に保存できるようにして、それを読み込む場合は、(1)の場所にその処理を組み込んでリダイレクト先を切り替えるようにすればよいでしょう(*)。

*:ServletAPI 2.1以降ではRequestDispatcherを用いてサーバ上でServlet間の連携を行う事になります。本書のサンプルではServletAPI 2.0であるApache JServを用いてテストしているため、ServletAPI 2.0の範囲のAPIのみで作成されています。

ではアカウント入力画面を表示するServletを作成します。

// [Account.java]
package com.sk_jp.servlet.shop;

import java.io.IOException;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.sk_jp.text.Formatter;
import com.sk_jp.text.TextFormatter;
import com.sk_jp.servlet.MapByHttpSession;

public class Account extends ShoppingCartServlet {
    private Formatter formatter;
    protected void initSub() throws IOException {
        formatter = new TextFormatter(
                getClass().getResourceAsStream(
                    "/com/sk_jp/resources/shop/account.template"),
                inputEncoding);
    }
    public void doPost(HttpServletRequest request,
                       HttpServletResponse response)
                throws ServletException, IOException {

        HttpSession session = request.getSession(false);
        if (session == null) {
            errorSessionInvalid(response);
            return;
        }
        Map args = new MapByHttpSession(session);

        print(response, formatter, args, outputEncoding);
    }
}

ソースコードはこれだけです。表示内容は(CLASSPATH上の)/com/sk_jp/resources/shop/account.templateに記述することになります。このtemplateファイルの内容は以下のようになります。

[account.template]
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<HTML>
<HEAD>
<META http-equiv="Content-Style-Type" content="text/css">
<TITLE>アカウント情報の入力</TITLE>
</HEAD>
<BODY>
<DIV style="text-align:center">
<H1>アカウント情報の入力</H1>
<HR>
<FORM method="post" action="com.sk_jp.servlet.shop.RegisterAccount">
<P>アカウント情報を入力してください</P>
<TABLE border="1" cellpadding="3">
  <TBODY>
    <TR>
      <TD>E-mail address</TD>
      <TD>@##mailAddress@</TD>
    </TR>
    <TR>
      <TD>氏名</TD>
      <TD><INPUT name="name"></TD>
    </TR>
    <TR>
      <TD>郵便番号</TD>
      <TD><INPUT name="zipCode"></TD>
    </TR>
    <TR>
      <TD>都道府県</TD>
      <TD><SELECT name="prefecture">
      <OPTION>北海道</OPTION>
      <OPTION>青森県</OPTION>
      <OPTION>岩手県</OPTION>
      <OPTION>宮城県</OPTION>
      <OPTION>秋田県</OPTION>
      <OPTION>山形県</OPTION>
      <OPTION>福島県</OPTION>
      <OPTION>茨城県</OPTION>
      <OPTION>栃木県</OPTION>
      <OPTION>群馬県</OPTION>
      <OPTION>埼玉県</OPTION>
      <OPTION>千葉県</OPTION>
      <OPTION selected>東京都</OPTION>
      <OPTION>神奈川県</OPTION>
      <OPTION>新潟県</OPTION>
      <OPTION>富山県</OPTION>
      <OPTION>石川県</OPTION>
      <OPTION>福井県</OPTION>
      <OPTION>山梨県</OPTION>
      <OPTION>長野県</OPTION>
      <OPTION>岐阜県</OPTION>
      <OPTION>静岡県</OPTION>
      <OPTION>愛知県</OPTION>
      <OPTION>三重県</OPTION>
      <OPTION>滋賀県</OPTION>
      <OPTION>京都府</OPTION>
      <OPTION>大阪府</OPTION>
      <OPTION>兵庫県</OPTION>
      <OPTION>奈良県</OPTION>
      <OPTION>和歌山県</OPTION>
      <OPTION>鳥取県</OPTION>
      <OPTION>島根県</OPTION>
      <OPTION>岡山県</OPTION>
      <OPTION>広島県</OPTION>
      <OPTION>山口県</OPTION>
      <OPTION>徳島県</OPTION>
      <OPTION>香川県</OPTION>
      <OPTION>愛媛県</OPTION>
      <OPTION>高知県</OPTION>
      <OPTION>福岡県</OPTION>
      <OPTION>佐賀県</OPTION>
      <OPTION>長崎県</OPTION>
      <OPTION>熊本県</OPTION>
      <OPTION>大分県</OPTION>
      <OPTION>宮崎県</OPTION>
      <OPTION>鹿児島県</OPTION>
      <OPTION>沖縄県</OPTION>
      </SELECT></TD>
    </TR>
    <TR>
      <TD>住所1(市町村)</TD>
      <TD><INPUT name="address1"></TD>
    </TR>
    <TR>
      <TD>住所2(アパート/マンション)</TD>
      <TD><INPUT name="address2"></TD>
    </TR>
  </TBODY>
</TABLE>
<P><INPUT type="submit" value="  次へ進む  "></P>
</FORM>
</DIV>
</BODY>
</HTML>

この画面では@##mailAddress@という部分のみが置き換え対象となっています。アカウント情報の変更に対応するためには各INPUTタグのvalue属性にそれぞれ@##keyword@を組み入れておけば、Servlet側で現在の情報を挿入することができます。

なお、ご存知の方であればこのtemplateを見てもわかる事ですが、このような置き換え等の表現に関する部分はJSPで記述すればよいという気がします。この点に関しては、@check@ページの脚注でも述べたように、筆者はJSPがロジックと表現を分離するのにベストな方法とは思っていません(便利で簡単ではありますが)。従って、Servletが出力するHTMLに関してもTextFormatterを用いて生成するように作っています。

アカウント情報入力画面は以下のようになります。

では、アカウント情報を格納する(上記テンプレートから生成されたHTMLから呼び出される)Servletを作成します。

// [RegisterAccount.java]
package com.sk_jp.servlet.shop;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.sk_jp.servlet.ServletUtility;

public class RegisterAccount extends ShoppingCartServlet {
    public void doPost(HttpServletRequest request,
                       HttpServletResponse response)
                throws ServletException, IOException {

        HttpSession session = request.getSession(false);
        if (session == null) {
            errorSessionInvalid(response);
            return;
        }

        ServletUtility.insertParametersToSession(
                request, session, outputEncoding);

        response.sendRedirect("com.sk_jp.servlet.shop.Select");
    }
}

このdoPostメソッドでは、セッションのチェックを行った後、POSTのパラメタで与えられた全ての情報(key/value)をそのままHttpSessionに格納するということだけです。後は、次のSelect Servletにリダイレクトして続きの処理を行っています。なお、リクエストパラメタをHttpSessionに格納するというServletUtility#insertParametersToSession()は、http://www.sk-jp.com/java/library/utility/からソースコードを取得できます。
本当のシステムでは、ここで入力された個人情報をサーバのデータベースなどに記録しておき、次回のセッション時に活かせるようにするところでしょう。
また、Login.javaと同様に入力値のチェックも行う必要がありますが、省略しています。これは実際にはチェックを行うべきキーと条件、エラー時のメッセージをテーブル化しておいて一気にチェックするように作ればよいでしょう。

ここまでで得られる情報は以下の通りです。

表5-1
HttpSessionのキー
mailAddress メイルアドレス
name 氏名
zipCode 郵便番号
prefecture 都道府県
address1 住所1(市町村)
address2 住所2(アパート/マンション)

さて、次は商品一覧の表示です。

商品一覧では先にtemplateの方を示しておきます。

[book_productlist.template]
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<HTML>
<HEAD>
<TITLE>商品一覧</TITLE>
<SCRIPT type="text/javascript">
<!--
function inc(unitprice, count, price, increase) {
    num = eval(count.value);
    if (increase < 0 && num == 0) {
        return;
    }
    count.value = num + increase;
    price.value = unitprice.value * count.value;
    document.form.total.value =
            eval(document.form.total.value) + increase * unitprice.value;
}
//-->
</SCRIPT>
</HEAD>
<BODY>
<H1>おかいもの おかいもの</H1>
<P>買いたい書籍を選んで最後に「買うぞ」ボタンを押してちょ</P>
<FORM name="form" method="POST" action="com.sk_jp.servlet.shop.Buy">
<TABLE border="2">
  <TBODY>
    <TR>
      <TH>出版社</TH>
      <TH>書名</TH>
      <TH>著者</TH>
      <TH>ISBNコード</TH>
      <TH>初版発刊日</TH>
      <TH>ページ数</TH>
      <TH>単価</TH>
      <TH>購入部数</TH>
      <TH>小計</TH>
    </TR>
    <!--@#while productList@-->
    <TR>
      <TD>@##publisher@</TD>
      <TD>@##title@</TD>
      <TD>@##author@</TD>
      <TD>@##isbn@</TD>
      <TD>@##issued@</TD>
      <TD align="right">@##page@</TD>
      <TD><INPUT type="text" name="@##name@_unitprice" value="@##unitPrice@"
                 size="6" readonly>円
      </TD>
      <TD><INPUT type="text" name="@##name@" value="@##count@"
                 size="3" readonly>
      </TD>
      <TD><INPUT type="text" name="@##name@_price" value="@##price@"
                 size="6" readonly>円
      </TD>
      <TD><INPUT type="button" value="増やす"
                 onclick="inc(@##name@_unitprice, @##name@, @##name@_price, 1)">
      </TD>
      <TD><INPUT type="button" value="減らす"
                 onclick="inc(@##name@_unitprice, @##name@, @##name@_price, -1)">
      </TD>
    </TR>
    <!--@#endwhile@-->
  </TBODY>
</TABLE>
<P>
全部で<INPUT type="text" name="total" value="0" size="7" readonly>円
のお買い上げ予定です。
</P>
<INPUT type="submit" value=" 買うぞ "></FORM>
</BODY>
</HTML>

購入商品個数の増減はJavaScriptを用いてクライアント側だけで行ってしまう仕様としました。

商品数は可変で、実行時にListオブジェクトとして与えられるものとします。そのために@#while productList@で繰り返される部分を囲っているのですが、このあたりは後で出てくる送信メッセージの説明とWebページ上のTextFormatter仕様の説明をご参照下さい。

では、Select Servletのソースコードを示します。

// [Select.java]
package com.sk_jp.servlet.shop;

import java.io.IOException;
import java.io.File;
import java.io.FilenameFilter;
import java.io.InputStream;
import java.io.FileInputStream;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Properties;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.sk_jp.text.Formatter;
import com.sk_jp.text.TextFormatter;
import com.sk_jp.servlet.MapByHttpSession;

public class Select extends ShoppingCartServlet {
    private String productsDir;
    private Formatter formatter;

    protected void initSub() throws IOException {
        productsDir = properties.getProperty("productsDir");

        formatter = new TextFormatter(
                getClass().getResourceAsStream(
                    "/com/sk_jp/resources/shop/book_productlist.template"),
                inputEncoding);
    }

    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response)
                throws ServletException, IOException {
        HttpSession session = request.getSession(false);
        if (session == null) {
            errorSessionInvalid(response);
            return;
        }

        Map args = new MapByHttpSession(session);

        args.put("productList", createProducts());

        print(response, formatter, args, outputEncoding);
    }

    private List createProducts() {
        List productList = new ArrayList();
        File dir =
                new File(productsDir + "com/sk_jp/resources/shop/products/");
        String[] list = dir.list(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.endsWith(".properties");
            }
        });
        Arrays.sort(list);

        Properties product;
        InputStream in;
        for (int i = 0; i < list.length; i++) {
            product = new Properties();
            try {
                in = new FileInputStream(new File(dir, list[i]));
                try {
                    product.put("count", "0");
                    product.put("price", "0");
                    product.load(in);
                } finally {
                    in.close();
                }
                productList.add(product);
            } catch (IOException e) {
                getServletContext().log(e, "Got Exception:");
            }
        }
        return productList;
    }
}

仕様を簡略化しているとはいえ、あまり長くないですね。定型処理をベースとなるShoppingCartServletなどにまとめた結果です。
doGet()メソッドを見てみましょう。このServletが行うのは、HttpSessionオブジェクトのkey/valueの組をMapでラッピングして、それに"productList"のエントリを追加してフォーマット出力を行う事です。ラッピングはMapByHttpSessionというクラスを使って行っています。MapByHttpSessionはjava.util.AbstractMapを継承して、put()/get()をHttpSessionのputValue()/getValue()に対応させただけのものです。ソースコードはhttp://www.sk-jp.com/java/library/utility/から取得できます。

あとは"productList"のエントリとなるcreateProducts()で生成されるListですね。まず、このServletが呼ばれる段階ではHttpSessionには表5-1のエントリが格納されているわけです。これらは単なるStringのエントリでしたが、"productList"にはListオブジェクトを格納します。これによって、TextFormatterがList中の各要素に対してフォーマットを行ってくれるのです。

createProducts()では、特定のディレクトリ上の".properties"ファイルの一覧を得て(それをファイル名順にソートして)その各Propertiesファイルを読み込んでListオブジェクトに順次格納していくという処理を行っています。java.util.Propertiesはjava.util.Mapの実装クラスですので、TextFormatterは@#while productList@内部では、このPropertiesオブジェクトのエントリを参照するようになります。

なお、initSub()メソッドで"productsDir"としてリソースのルート階層へのパスを得ています。ここでShoppingCartServletでロードされているshopping.propertiesの内容も示しておきます。

# [shopping.properties]
inputEncoding = ISO-2022-JP
outputEncoding = ISO-2022-JP

productsDir = /home/shin/java/skdemo/resources/

smtpHost = localhost
from = postmaster@localhost

fee = 1000

ちなみにディレクトリとファイルの階層は以下のようになっています。

CLASSPATHは/home/shin/java/skdemo/resources/と/home/shin/java/skdemo/class/

***************** Servletの初期値であるPropertiesファイル *****************************
/home/shin/java/skdemo/resources/com/sk_jp/resources/shop/shopping.properties
***************** テンプレートファイル群 **********************************************
/home/shin/java/skdemo/resources/com/sk_jp/resources/shop/account.template
/home/shin/java/skdemo/resources/com/sk_jp/resources/shop/book_productlist.template
/home/shin/java/skdemo/resources/com/sk_jp/resources/shop/book_receipt.template
/home/shin/java/skdemo/resources/com/sk_jp/resources/shop/book_receipt_mail.template
***************** 商品データであるPropertiesファイル群 *********************************
/home/shin/java/skdemo/resources/com/sk_jp/resources/shop/products/00001.properties
/home/shin/java/skdemo/resources/com/sk_jp/resources/shop/products/00002.properties
  :
************************************************************************************
/home/shin/java/skdemo/class/com/sk_jp/servlet/shop/クラスファイル群

商品を表す、00001.properties等は以下のようになっています。これはnative2asciiで変換された結果です。大したサンプルではないとはいえ、このようなファイルを作成して所定のディレクトリに配置すれば、簡単に新たな商品を追加できるようになっています(注:本当はデータベースを用いるところです)。

# [00001.properties]
name        = p1
title       = JavaMail \u5b8c\u5168\u89e3\u8aac
publisher   = \u79c0\u548c\u30b7\u30b9\u30c6\u30e0
author      = \u6728\u4e0b\u4fe1
unitPrice   = 3200
isbn        = 4-7980-XXXX-X
issued      = 2001/02
page        = 448

このServletが出力する商品選択画面は以下のようになります。

さて、商品選択画面で適当に購入部数が決定されて「買うぞ」が押下された時に呼び出されるのがBuy Servletです。やっとJavaMailの出番がやってきました^^。
しかししかし、メッセージ送信はSimpleSender#send()の一行で済んでしまいますし、その本文に関しても、今までに出てきたServletにおけるHTML出力とほとんど同じ処理でできてしまうので、JavaMailに関する部分自体はごくわずかなのですね。

ではBuy Servletを見てみましょう。

// [Buy.java]
package com.sk_jp.servlet.shop;

import java.io.IOException;
import java.util.Map;
import java.util.List;
import java.util.Iterator;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.mail.MessagingException;

import com.sk_jp.mail.SimpleSender;
import com.sk_jp.text.IndentedFormat;
import com.sk_jp.text.Formatter;
import com.sk_jp.text.TextFormatter;
import com.sk_jp.servlet.MapByHttpSession;

public class Buy extends ShoppingCartServlet {
    private String smtpHost;
    private String from;
    private int fee;
    private Formatter responseFormatter;
    private Formatter mailFormatter;
    private final IndentedFormat numberFormat = new IndentedFormat(8, 0);

    protected void initSub() throws IOException {
        smtpHost = properties.getProperty("smtpHost");
        from = properties.getProperty("from");
        try {
            fee = Integer.parseInt(properties.getProperty("fee"));
        } catch (NumberFormatException e) {
            throw new IOException("Init Parameter [fee] was invalid.");
        }

        responseFormatter = new TextFormatter(
                getClass().getResourceAsStream(
                    "/com/sk_jp/resources/shop/book_receipt.template"),
                inputEncoding);
        mailFormatter = new TextFormatter(
                getClass().getResourceAsStream(
                    "/com/sk_jp/resources/shop/book_receipt_mail.template"),
                inputEncoding);
    }

    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response)
                throws ServletException, IOException {

        HttpSession session = request.getSession(false);
        if (session == null) {
            errorSessionInvalid(response);
            return;
        }
        final Map args = new MapByHttpSession(session);

        count(request, args);

        print(response, responseFormatter, args, outputEncoding);

        try {
            sendMail(args);
        } catch (MessagingException e) {
            getServletContext().log(e,
                    "Sending to " + args.get("mailAddress") + " was failed. ");
        }
    }

    private void count(HttpServletRequest request, Map args) {
        final Iterator productsIterator =
                ((List)args.get("productList")).iterator();
        Map product;
        String key;
        int count;
        int unitPrice;

        while (productsIterator.hasNext()) {
            product = (Map)productsIterator.next();
            key = (String)product.get("name");                           // (1)
            count = Integer.parseInt(request.getParameter(key));
            if (count == 0) {
                productsIterator.remove();
            } else {
                unitPrice = Integer.parseInt((String)product.get("unitPrice"));

                product.put("unitPrice", numberFormat.format(unitPrice));
                product.put("count", new Integer(count));
                product.put("price", numberFormat.format(count * unitPrice));
            }
        }
        int orderTotal = Integer.parseInt(request.getParameter("total"));// (2)
        int tax = (orderTotal + fee) * 5 / 100;
        int total = orderTotal + fee + tax;

        args.put("fee",        numberFormat.format(fee));
        args.put("orderTotal", numberFormat.format(orderTotal));
        args.put("tax",        numberFormat.format(tax));
        args.put("total",      numberFormat.format(total));
    }

    private void sendMail(Map args) throws MessagingException {
        SimpleSender.send(
                smtpHost,
                (String)args.get("mailAddress"),
                from,
                "お買い上げありがとうございました",
                mailFormatter.format(args));
    }
}

Buyクラスの行うことは、リクエストパラメタで受け取った各商品ごとの購入申し込み部数を元に、最終的な金額を計算して購入内容の表示を行うことと、確認メイルを申込者に送信する事です(もちろん本当はその情報をデータベースに格納するなどの処理が必要です)。

まず、initSub()メソッド(念のためもう一度書いておきますが、これはスーパクラスであるShoppingCartServletのinit()メソッド中から呼ばれるメソッドです)では、Buy servletが用いるリソースと初期パラメタの取得を行っています。
つまり、送信に使用するホスト、送信するメッセージのFrom:アドレス、手数料(送料など。これは初期パラメタにするのは不自然ですがサンプルですので)をshopping.propertiesから取得し、Servletの出力をフォーマットするresponseFormatterと、送信するメッセージの本文をフォーマットするmailFormatterを作成しています。

doPost()での処理は商品選択画面で「買うぞ」ボタンが押下された時に行われるもので、パラメタの内容をTextFormatterの置き換えに用いるMapに適切に割り当てて、ブラウザへの出力+メイル送信を行います。
まずHttpSessionの獲得→チェック→Mapへのラッパオブジェクト生成までは、Select Servletと同じです。次にそのMapの内容を設定するcount()メソッドを呼び出します。count()では、Select ServletでHttpSessionに格納された"productList"の各商品データ毎に内容を補正していきます。

(1)の箇所は、book_productlist.templateを見ていただければ分かるように、商品選択HTMLのFORMの「購入部数」パラメタ名には、商品情報Propertiesファイルの"name"の値が使用されていますので、これをInteger#parseInt()で数値にして、0ならその商品のエントリを削除、そうでなければ単価や小計を文字列としてMapに格納するという処理です。なお、IndentedFormatというクラスは筆者の作成した、桁数を指定して左を空白で埋めるフォーマットを行うものです(*)。
(2)の箇所は手数料/注文合計額/税金/総計を計算して、それぞれフォーマットした文字列としてMapに格納しています。

*:例によって、http://www.sk-jp.com/java/library/utility/からソースコードとともに取得できます。

count()から復帰する時点でMap(HttpSession)の内容は以下のようになっています。

Map(HttpSession)のキー 値の内容
mailAddress メイルアドレス
name 氏名
zipCode 郵便番号
prefecture 都道府県
address1 住所1(市町村)
address2 住所2(アパート/マンション)
productList 複数のMapをListに格納したもの
それぞれのMapは以下のような内容
Mapのキー 値の内容
title 書名
publisher 出版社
author 著者
count 購入部数
unitPrice 単価
price 小計






× n
fee 送料&手数料
orderTotal 販売価格合計(税別)
tax 消費税
total 注文合計

このようなMapを後に記載するbook_receipt.templateや、book_receipt_mail.templateを利用するTextFormatterに与える事になります。

count()から復帰した後は、以下の部分でHTMLの出力とメイルの送信を行っています。

        print(response, responseFormatter, args, outputEncoding);

        try {
            sendMail(args);
        } catch (MessagingException e) {
            getServletContext().log(e,
                    "Sending to " + args.get("mailAddress") + " was failed. ");
        }

print()はShoppingCartServlet.javaに記載しているメソッドで、テンプレートを与えたTextFormatterオブジェクトとMapを与えてフォーマットを行った結果をServletのレスポンスとするものです。print()メソッドでは出力ストリームのclose()まで行っているため、このメソッドが復帰した段階でブラウザへの結果の出力は完了しています。出力を行った後にメイルの送信を行います。このときメイル送信に失敗した事を考えて、ブラウザへの出力に注意事項を含めるようにします。
なお、ブラウザへの出力前にメイル送信を行うのは無駄にレスポンス速度の低下を招きます。最寄りのサーバに送信できたかどうかのチェックを先に行うことは確かに可能ですが、メイルが相手まで届くかどうかは結局この段階では判定できませんので、まずはブラウザへのレスポンスを優先するのがよいでしょう。

さて、この章のメインであるはずのsendMail()メソッドの内容ですが、1行ですねぇ。

    private void sendMail(Map args) throws MessagingException {
        SimpleSender.send(
                smtpHost,
                (String)args.get("mailAddress"),
                from,
                "お買い上げありがとうございました",
                mailFormatter.format(args));
    }

3章で作成したSimpleSenderのstaticメソッドを使えばtext/plainのメソッドはこれだけあっさり書けてしまいます。本文についてもテンプレートと置き換え対象のMapオブジェクトがあればTextFormatterのformat()メソッド一発で完成します。同時に管理者にもメッセージを送信しておくようにしてもいいでしょうね(*)。

*:bcc:を用いて管理者に同時に送信を行うような場合はbcc:を指定できるようなSimpleSender#send()を別途定義すればよいでしょう。

以下にbook_receipt.templateとbook_receipt_mail.templateを記載しておきます。

[book_receipt.template]
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<HTML>
<HEAD>
<TITLE>購入商品を記録しました</TITLE>
</HEAD>
<BODY>
<H1>お買い上げありがとうございます</H1>
<P>@##name@ 様のお買い上げは以下の通りです。</P>
<TABLE border="2">
  <TBODY>
    <TR>
      <TH>出版社</TH>
      <TH>書名</TH>
      <TH>著者</TH>
      <TH>ISBNコード</TH>
      <TH>初版発刊日</TH>
      <TH>ページ数</TH>
      <TH>単価</TH>
      <TH>購入部数</TH>
      <TH>小計</TH>
    </TR>
    <!--@#while productList@-->
    <TR>
      <TD>@##publisher@</TD>
      <TD>@##title@</TD>
      <TD>@##author@</TD>
      <TD>@##isbn@</TD>
      <TD>@##issued@</TD>
      <TD align="right">@##page@</TD>
      <TD align="right">@##unitPrice@円</TD>
      <TD align="right">@##count@</TD>
      <TD align="right">@##price@円</TD>
    </TR>
    <!--@#endwhile@-->
  </TBODY>
</TABLE>
<HR>
<TABLE border="2" cellspacing="0">
  <TBODY>
    <TR>
      <TD>送料&手数料</TD>
      <TD align="right">@##fee@円</TD>
    </TR>
    <TR>
      <TD>販売価格合計(税別)</TD>
      <TD align="right">@##orderTotal@円</TD>
    </TR>
    <TR>
      <TD>消費税</TD>
      <TD align="right">@##tax@円</TD>
    </TR>
    <TR>
      <TD>注文合計</TD>
      <TD align="right">@##total@円</TD>
    </TR>
  </TBODY>
</TABLE>
<P>全部で<STRONG>@##total@円</STRONG>になります。<BR>
購入のご確認のメイルを送信させていただきました。<BR>
万が一ご確認のメイルが1時間以内に届かない場合は<A href="mailto:xxxx@yyyy">当社</A>に
ご連絡いただくか、再度ご注文を行って下さい。
それによってもしご確認のメイルが複数届くことがあっても、そのメイルに記載された「購入」操作は
一度だけ行うようにしてください。「購入」操作を行わなければ実際の発注は行われません。</P>
<P>お買い上げありがとうございました。</P>
</BODY>
</HTML>
[book_receipt_mail.template]
@##name@ 様

この度は、XXXXXにご注文いただき誠にありがとうございます。
お客様がブラウザ上でご注文された商品について確認のメイルを
差し上げております。

注:もし、身に覚えのない場合はこのまま何もしなければ商品の発送はなされません。
   ただ、他人が「なりすまし」て注文操作が行われてしまった可能性がある場合は、
   以下のページで「パスワード変更」を行ってください。

http://www.example.com/account

お客様は現在、以下の商品について、ご購入申し込みをされています。

ご請求先

@##name@<@##mailAddress@>
〒@##zipCode@
@##prefecture@@##address1@
@##address2@

@#while productList@-----------------------------------------------
書名  :@##title@
出版  :@##publisher@
著者  :@##author@
ISBN  :@##isbn@
購入部数:@##count@部
単価  :@##unitPrice@円
小計  :@##price@@#endwhile@
====================================================
送料&手数料:    :@##fee@円
販売価格合計(税別) :@##orderTotal@円
消費税        :@##tax@円
            -----------
注文合計       :@##total@円


上記内容に間違いが無ければ、以下のURLにアクセスし、
「購入します」ボタンを押下して下さい。
この操作を行わない限り、商品の配送はされません。

http://www.example.com/buy


XXXXXオンラインショップをご利用いただき、ありがとうございました。

@#exist advertisements@
----------------------------------------------------
@#foreach advertisements@
@##advertisements@
----------------------------------------------------
@#endforeach@@#endexist@

--
XXXXXオンラインサービス
http://www.example.com/
mailto:question@example.com

なお、book_receipt_mail.templateは、この章の始めでご紹介したtemplateファイルと全く同じです。ここでは与えるMapに"advertisements"というキーが存在しないため、広告は付加されませんが、"advertisements"に対してStringの配列等を与えるとbook_receipt_mail.templateの@##advertisements@の部分に挿入されるようになります。

出力結果の画面は以下のようになります。

そして、以下のようなメイルが届いています。