コントローラ層

コントローラ層は、クライアントからのリクエストを受け付け、ビュー層とドメイン層を調整します。コントローラ層は画面遷 移、セッション管理やメッセージの管理などを担当します。コントローラの実装はプレゼンテーション形態によって異なり ます。United Front 2 のプレゼンテーションは Web です。Web に関するクラスは org.unitedfront2.web 以下のパッケージに、コントローラの Web 実装は org.unitedfront2.web.controller に収録しています。

United Front 2 の Web コントローラ (HTML/XML) は Spring Web Flow をベースとしています。ウィジェットなどの画面の一部分に対するコントローラは Apache Tiles 2 を利用しています。ここでは Spring Web Flow や Tiles に関する詳細な説明は避け、サンプルソースコードを中 心に解説します。これらのライブラリの詳細な使用方法は本家のドキュメントを参照して下さい。

画面遷移

Web (HTML) の画面遷移は Spring Web Flow を採用しています。

サンプルのシナリオ

サンプルで登場するシナリオはメールアドレスの変更機能で、ユースケースは次のとおりです。メールアドレス変更時に パスワードを再発行しているのは、入力されたメールアドレスが正しいものであることを確認するためです。

主成功シナリオ
  1. システムはメールアドレスの変更フォームを表示する
    メールアドレス変更画面
  2. ユーザは新しいメールアドレスを入力する
  3. システムは認証フォームを表示する
    メールアドレス変更画面
  4. ユーザはログイン ID (旧メールアドレス)とパスワードを入力する
  5. システムはユーザのデータを新しいメールアドレスに変更し、パスワードも再発行する
  6. システムは再発行したパスワードが記載されたメールを、ユーザの新しいメールアドレスに送信する
  7. システムは処理が完了した旨を表示する
    メールアドレス変更画面
例外 a
2 で、ユーザの入力値が不正だった場合は 1 に戻り、エラーメッセージを出力する
例外 b
4 で、ユーザの入力値が不正だった場合は 3 に戻り、エラーメッセージを出力する
例外 c
4 で認証に失敗した場合は 3 に戻り、エラーメッセージを出力する
例外 d
5 で新しいメールアドレスが既に他のユーザによって使用されている場合は 1 に戻り、エラーメッセージを出力する
Scenario

アクションクラス

アクションクラスは次のような処理を担当します。

  • 入力値の検証
  • トランザクションの制御
  • ビューへ転送するのデータの作成
  • エラーメッセージ処理

トランザクション制御には、 宣言的トランザクション制御 を採用しているため、プログラマがトラ ンザクションに関するコードを書くことはほとんどありません。トランザクション制御の範囲は Action インターフェースの execute メソッドの開始から終わりまでです。アクションクラスのアクションメ ソッドが例外を放った時点でロールバックを実行します。

United Front 2 では入力フォーム処理用のアクションクラス org.unitedfront2.web.controller.FormAction を提供しています。このクラスは Spring オリジナルの org.springframework.webflow.action.FormAction を継承しており、さらに以下の機能を加えています。

  • ドメインファクトリを使ったフォームオブジェクトの生成
  • 設定した検証クラスをビューへ転送
  • メール送信フレームワーク

United Front 2 におけるフォーム処理は通常 United Front 2 の FormAction を継 承して実装します。

Note: Web MVC フレームワーク

Spring MVC は Struts (1.x) に代表される従来 の Web MVC フレームワークからいくつかの改良点が加えられています。その改良点の一つに、クライアントからのリク エストを直接 JavaBean オブジェクトにバインドする仕組みが挙げられます。この機能を利用すれば、United Front 2 のドメインオブジェクトをビュー層からドメイン層まで一貫して利用できます。また、Spring Web Flow を 採用する利点には次のようなものがあります。

  • セッション管理をフレームワークに任せることができる
  • 一つの URL で一連の画面遷移を実現できる
  • 画面遷移を再利用できる
  • Spring MVC の機能を利用できる(タグライブラリなど)

主要アクションメソッド

MultiAction が持つべき代表的なアクションメソッドを次に示します。アクションメソッドについては Spring Web Flow のドキュメ ント ActionState XML - multi action を参照ください。

メソッド名 説明
init フローの初期処理を行います。
setupForm フォーム画面表示前の処理を行います。例えば、フォーム画面に表示するデータをリクエストスコープに設定します。
bindAndValidate クライアントリクエストからドメインオブジェクトを作成(バインド)し、ドメインオブジェクトの検証を行います。実際のバインドのロジックおよび検証ロジックは、それぞれ doBind メソッド、 doValidator メソッドに記述します。
view 情報を画面に表示します。
sendMail メールを送信します。実際の送信ロジックは doSendMail メソッドに記述します。
finish フローの終了処理を行います。

サンプルコード

アクションクラスのサンプル MailAddrChangeAction を示します。initAction メソッドでは、 ドメインモデルの指定と e メール送信用テンプレート MailAddrChange-mail を設定しています。メールテンプ レートについてはビュー層で解説しています。完全なコードは MailAddrChangeAction のソースコード を参照してください。

// アノテーションベースの Spring Bean 定義。
// Bean 名は ${機能名}.${先頭が小文字のクラス名}
@Repository(value = "account.mailAddrChangeAction")
public class MailAddrChangeAction extends FormAction {

    @Autowired
    private AccountTable accountTable;

    @Override
    protected void initAction() {
        super.initAction();
        setFormObjectClass(Account.class);
        setMailTemplateName("MailAddrChange-mail");
    }

    /** メールアドレスの変更とパスワードの再発行 */
    public Event changeMailAddrAndReissuePassword(RequestContext context)
        throws MailAddrUsedByOtherException {

        int id = WebUtils.getAccount(context).getId();
        String newMailAddr = ((Account) getFormObject(context)).getMailAddr();
        String newPassword = Account.createRandomPassword(4, 8);

        Account newAccount = accountTable.get(id);
        newAccount.setMailAddr(newMailAddr);
        newAccount.setPassword(newPassword);
        newAccount.encrypt();
        try {
            newAccount.store();
        } catch (MailAddrUsedByOtherException e) {
            // メールアドレスが登録済みであるため、エラー処理
            // エラーメッセージを設定した後、例外をそのまま投げています。
            // 例外発生後の動作はフロー定義ファイル側で定義されます。
            logger.warn(e.getMessage());
            Errors errors = getBindingErrors(context, newAccount);
            errors.rejectValue("mailAddr",
                "account.validation.mailAddrUserdByOther",
                new Object[] {e.getMailAddr()}, e.getMessage());
            throw e;
        }

        // ビューへ転送するデータを設定
        context.getRequestScope().put("mailAddrAndPassword",
                new MailAddrAndPassword(newAccount.getMailAddr(), newPassword));

        return success();
    }

    // 検証クラスをインジェクション
    @Override
    @Resource(name = "account.mailAddrFormValidator")
    public void setValidator(Validator validator) {
        super.setValidator(validator);
    }
}
Warning

メールアドレスが既に使用されていた際に、error() メソッドではなく例外を発生させてメソッドを 終了している点に注意してください。これにより、トランザクションをロールバックしています。 error() メソッドを使用した場合、ロールバックは実行されません。

Warning: 必要に応じて initBinder をオーバーライドする

必要に応じて initBinder メソッドをオーバーライドし、クライアントからのリクエストを制限てくだ さい。Spring のデフォルトの設定では、クライアントから送信された全てのプロパティをドメインオブジェクトに設定しよ うと試みます。これは場合によってはセキュリティ上大変危険です。予期しない値がドメインオブジェクトのプロパティに設 定されないようにするためには、FormActioninitBinder メソッドを オーバーライドし、DataBinder に許可するプロパティを設定します。

@Override
protected void initBinder(RequestContext context, DataBinder binder) {
    super.initBinder(context, binder);
    binder.setAllowedFields(new String[] {"mailAddr"});
}

入力値検証クラス

画面入力項目の検証クラスはドメイン層の検証クラスを用いて実装します。画面入力項目の検証クラスは SpringValidator インターフェースを実装する必要があります。実装は SpringValidatorSupport を使って簡素化できます。

静的構造図

SpringValidator は United Front 2 のフォームアクション FormAction で使われています。FormAction は SpringValidator から オリジナルの検証クラスを取り出し、それをビューへ転送しています。

入力値検証クラス

サンプルソース

入力値検証クラスのサンプル MailAddrFormValidator を示します。MailAddrFormValidator はアカウントのメールアドレスの値の検証を行います。 完全なソースは MailAddrFormValidator のソースコード を参照してください。

@Repository(value = "account.mailAddrFormValidator")
public class MailAddrFormValidator
    extends SpringValidatorSupport<AccountValidator, Account> {

    @Override
    protected void doValidate(Account account, Errors errors) {
        try {
            getOriginalValidator().validateMailAddr(account);
        } catch (ValidationException e) {
            rejectValue("mailAddr", e, errors);
        }
    }

    @Override
    @Autowired
    public void setOriginalValidator(AccountValidator originalValidator) {
        super.setOriginalValidator(originalValidator);
    }
}

Flow 定義

画面遷移を含む処理の流れを Flow 定義ファイルに記述します。主要な Flow ID には次のようなものがあります。

ID 説明
init フローの初期処理実行状態に付与する ID です。通常 action-state タグに付与されます。通常アクションメソッド init を利用します。
form フォーム処理実行状態に付与する ID です。通常 action-state タグに付与されます。通常アクションメソッド setupFormbindAndValidate を利用します。
mailSend メール送信処理実行状態に付与する ID です。通常 action-state タグに付与されます。通常アクションメソッド sendMail を利用します。
finish フローの終了処理実行状態に付与する ID です。通常 end-state タグに付与されます。

サンプルコード

メールアドレス変更フォームから完了までの流れをフロー定義 mailAddrChange-flow.xml に記述しています。実際に認証を行っているのは認証サブフロー authentication-sub-flow.xml です。メールアドレスの変更とパスワードの再発行を行うアクションステート changeMailAddrAndReissuePassword では、メールアドレスが既に他者に使われてしまっ ている場合に生じる MailAddrUsedByOtherException をキャッチすると、再度フォームス テート form へ遷移します。次のサンプルは説明のため実際のコードから若干変更しています。

<start-state idref="form"/>

<!-- メールアドレス変更フォーム -->
<view-state id="form" view="account.MailAddrChangeForm">
  <render-actions>
    <action bean="account.mailAddrChangeAction" method="setupForm"/>
  </render-actions>
  <transition on="submit" to="authenticate">
    <action bean="account.mailAddrChangeAction" method="bindAndValidate"/>
  </transition>
</view-state>

<!-- 認証サブフローへ -->
<subflow-state id="authenticate" flow="account/authentication-sub-flow">
  <transition on="authenticated" to="change"/>
</subflow-state>

<!-- メールアドレス変更処理 -->
<action-state id="change">
  <action bean="account.mailAddrChangeAction" method="changeMailAddrAndReissuePassword"/>
  <transition on="success" to="mailSend"/>
  <transition on-exception="org.unitedfront2.domain.MailAddrUsedByOtherException" to="form"/>
</action-state>

<!-- e メールの送信 -->
<action-state id="mailSend">
  <action bean="account.mailAddrChangeAction" method="sendMail"/>
  <transition on="success" to="finish"/>
</action-state>

<!-- メールアドレス変更完了画面 -->
<end-state id="finish" view="account.MailAddrChangeSuccess"/>

この Web Flow を Spring IDE を使って図示すると 次のようになります。

メールアドレス変更の画面遷移

URL

URL は WEB-INF/flows フォルダの階層とフロー定義ファイル名で決まります。
WEB-INF/flows/account/mailAddrChange-flow.xml に配置されているメールアドレス変更画面遷移は次の URL になります。

http://HOST_NAME/unitedfront2/account/mailAddrChange.html

例外として、*-sub-flow.xml という名前のフロー定義ファイルはサブフローとみなされ、URL から直接アクセスす ることはできないようになっています。

Note: URL の命名

URL は mailAddrChange.html とすべきでしょうか?それとも changeMailAddr.html とすべきでしょうか ?前者は単語、後者は動詞を表現しているように思えます。URL - Uniform Resource Locator - は「リソースを指すもの」という考えから、United Front 2 では前者を採用しました。もしこの URL がメールアドレスに関して包括的に扱うページなら、単に mailAddr.html としても良いかもしれません。

Note: URL 生成の実装

URL の仕組みは United Front 2 の独自クラス HierarchyXmlFlowRegistryFactoryBeanUriFlowExecutorArgumentHandler を使って実現しています。

資源への識別子はパラメータではなく URL 本文に埋め込んでいます。ここでいう資源とは、Resource インターフェースを実装したドメインオブジェクトのことです。例えば、プロフィールへの URL はパラメータを用いて通常① のように表現できますが、United Front 2 では基本的に②のようにパラメータを埋め込んだ URL を使うようにし ています。

① http://HOST_NAME/unitedfront2/profile/view.html?userCode=${userCode}
② http://HOST_NAME/unitedfront2/profile/${userCode}/index.html

これによって、各資源へクエリなしでアクセスできるようになります。この実装は UrlRewriteFilter を使っています。設定ファイ ル urlrewrite.xml に次のように記述するだけで簡単に実装できます。

<rule>
  <from>profile/([a-zA-Z0-9]+)/index\.html</from>
  <to>profile/view.html?userCode=$1</to>
</rule>

なお、ディレクトリを直接参照した際には index.html ファイルへリダイレクトします。

Note

このルールは、ブラウザのアドレスバーに表示される - つまりユーザが認識できる - URL にのみ適用しています。 例えば、アクセスした後、他の URL にリダイレクトするような一時的に使われるだけの URL にはこのルールを適用し ていません。

タイトル

HTML の TITLE タグは、TitleInterceptor によってビュー名 - ビュー層の項で説明する Tiles 定義で属性 name に設定される値 - と一 致するメッセージコードのメッセージが選択されます。ただしこれはデフォルトの挙動で、コントローラ内でタイトルを変更 することも可能です。

RequestContext context..
context.getRequestScope().put(
        TitleInterceptor.TITLE_CODE_PARAM_NAME, "message.code");
context.getRequestScope().put(
        TitleInterceptor.TITLE_ARGS_PARAM_NAME, new Object[] {"argument"});

セキュリティ

システムロールのアクセス制御を Spring Security を使って実装しています。システムロールとは、アカウントの認証や管理などのシステム運用上必要となる権限のことで す。細かな承認処理に対するアクセス制御は各アクションクラスで実装します。システムロールには次の3種類が存在 します。

ロール名 説明
USER_ROLE アカウントを保持する一般ユーザ。
ADMIN_ROLE システム管理者。
ROLE_ANONYMOUS アカウントを持っていないか、持っていてもログインしていないユーザ。

このアクセス制御では、ユーザからのアクセスを URL レベルで制御できます。デフォルトの設定では、全ての HTML ファイルは ROLE_USER からのみのアクセスを許可しています。必要に応じて設定ファイル applicationContext-security.xml に定義されている filterInvocationInterceptor Bean のプロパティを編集します。次 の例では、URL '/informationUpdate.html' に対するアクセスをシステム管理者のみに制限しています。

<bean id="filterInvocationInterceptor"
    class="org.springframework.security.intercept.web.FilterSecurityInterceptor">
  <property name="authenticationManager" ref="authenticationManager"/>
  <property name="accessDecisionManager" ref="httpRequestAccessDecisionManager"/>
  <property name="objectDefinitionSource">
    <security:filter-invocation-definition-source>

      <!-- ... -->

      <security:intercept-url pattern="/informationUpdate.html" access="ROLE_ADMIN"/>

      <!-- ... -->

      <security:intercept-url pattern="/**" access="ROLE_USER"/>
    </security:filter-invocation-definition-source>
  </property>
</bean>
Note: 匿名ユーザからの不正アクセス

この設定では、匿名ユーザによる不正な HTML ファイルのアクセスは「アクセス拒否」となります。存在しない HTML ファイルにアクセスした際も「アクセス拒否」扱いとなります。このとき、「アクセス拒否」ページではなく、トップページへ 遷移しますが、これは Spring Security のデフォルトの動作になります。

エラーページ

「Page Not Found」や「Internal Server Error」といったページに遷移させたい場合、アクションクラスで指 定された例外を発生させることで簡単に実現できます。例外の対応表を次に示します。

United Front 2 が提供する例外クラスは org.unitedfront2.web.flow および org.unitedfront2.web の二つのパッケージに存在します。アクションクラスで例外を発生する際は前者を、それ以外は後者のパッケージを利 用します。アクションクラスにおける例外処理のサンプルコードを次に示します。

@Override
public Event doExecute(RequestContext context) throws PageNotFoundException {
    String code = context.getRequestParameters().get(codeParamName);
    String code = context.getRequestParameters().get("code");
    Blog blog = blogTable.findByCode(code);
    if (blog == null) {
        String message = "The blog [Code='" + code + "'] not found.";
        logger.warn(message);
        throw new PageNotFoundException(context, this, message);
    }

    // ...
}

Tiles

Apache Tiles 2View Preparers 機能を使って、画面の一部に小さな Web コントローラを設けることができます。

サンプルコード

公開中のプロフィールを、プロフィールウィジェットに渡す PublicProfileWidgetPreparer を示します。完全なコードは PublicProfileWidgetPreparer のソースコード を参照ください。実際のコードでは、キャッシュを使ってデータアクセスを効率化しています。

@Repository(value = "profile.publicProfileWidgetPreparer")
public class PublicProfileWidgetPreparer implements ViewPreparer {

    /** プロフィールテーブル */
    @Autowired
    private ProfileTable profileTable;

    @Override
    public void execute(TilesRequestContext tilesContext,
            AttributeContext attributeContext) throws PreparerException {
        attributeContext.putAttribute("users",
            new Attribute(profileTable.getPublicProfileOwners()));
    }
}

RSS

RSS の実装には Rome を利用しています。RSS のコン トローラは Spring によって管理されますが、Web Flow ではなく、Spring MVC のコントローラとして実装しま す。

サンプルコードとして、ブログの RSS 用コントローラ RssController の一部を紹介します。このコントローラでは、取得したブログドメインオブジェクトからフィードを生成し、ビューへ転送して います。フィードタイプやエンコードはビュー側で設定されます。また、setLink に設定している URL はプロトコル、ホスト名、コンテキスト名が設定されていませんが、これらもビュー側で正しく設定されるためここで は設定不要です。ビューは RSS 専用の RssView です。RssController の完全なコードは RssController のソースコード を参照してください。

@Repository(value = "blog.rssController")
public class RssController extends AbstractController {

    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        Blog blog = getBlog(request);
        SyndFeed feed = toFeed(blog, WebUtils.getLocale(request));
        ModelAndView mav = new ModelAndView(new RssView());
        mav.addObject(RssView.FEED_PARAM_NAME, feed);
        return mav;
    }

    private Blog getBlog(HttpServletRequest request) {
        // ...
    }

    private SyndFeed toFeed(Blog blog, Locale locale) {
        SyndFeed feed = new SyndFeedImpl();

        // 概要
        feed.setTitle(blog.getOverview().getSubject(locale));
        feed.setLink("/blog/" + blog.getCode() + "/index.html");
        feed.setDescription(blog.getOverview().getBody(locale));
        feed.setAuthor(HtmlUtils.htmlEscape(blog.getOwner().getName()));

        // 記事
        List<SyndEntry> entries
            = new ArrayList<SyndEntry>(blog.getEntries().size());
        for (BlogEntry be : blog.getEntries()) {
            SyndEntry entry = new SyndEntryImpl();
            entry.setTitle(be.getEntry().getSubject(locale));
            entry.setLink("/blog/" + blog.getCode() + "/entry-"
                    + be.getCode() + ".html");
            SyndContent description = new SyndContentImpl();
            description.setType("text/html");
            description.setValue(be.getEntry().getBody(locale));
            entry.setDescription(description);
            entry.setPublishedDate(be.getEntry().getRegistrationDate());
            entry.setUpdatedDate(be.getEntry().getLastUpdateDate());
            entries.add(entry);
        }
        feed.setEntries(entries);
        return feed;
    }
}

RSS を保持するページでは、RSS への参照を HEAD タグ内の LINK タグで 設定する必要があります。これには、対象ページのコントローラで RSS への参照を保持する _feedUrl 変数を定義します。

@Repository(value = "blog.viewAction")
public class ViewAction extends AbstractAction {

    @Override
    protected Event doExecute(RequestContext context) {

        // ...

        context.getRequestScope().put("_feedUrl", "/blog/" + blog.getCode()
            + "/index.xml");
        return success();
    }
}

最後に、applicationContext-servlet.xml にコントローラと URL をマッピングします。

<bean id="urlMapping"
    class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">

  <!-- ... -->

  <property name="mappings">
    <props>
      <prop key="/**/*.html">flowController</prop>
      <prop key="/blog/view.xml">blog.rssController</prop>
    </props>
  </property>
</bean>